C++ 虛指標、成員變數與類物件的偏移地址
先給出一段程式碼實現
【原始碼輸出】#include <iostream> using namespace std; class animal { protected: int age; public: virtual void print_age(void) = 0; }; class dog : public animal { public: dog() {this -> age = 2;} ~dog() { } virtual void print_age(void) {cout<<"Wang, my age = "<<this -> age<<endl;} }; class cat: public animal { public: cat() {this -> age = 1;} ~cat() { } virtual void print_age(void) {cout<<"Miao, my age = "<<this -> age<<endl;} }; int main(void) { cat kitty; dog jd; animal * pa; int * p = (int *)(&kitty); int * q = (int *)(&jd); p[0] = q[0]; pa = &kitty; pa -> print_age(); return 0; }
程式碼輸出是 Wang, my age = 1;
如果將 p[0] = q[0]; 換為 p[1] = q[1]; 則輸出為 Miao, my age = 2。
【原始碼分析】
首先,這是一個取巧的改變虛表指標的辦法,它利用了C++的物件模型的特點。我們知道,一個類有了虛擬函式後,它會有一個虛表來維護虛擬函式和一個虛表指標__vptr來指向它,而這個程式利用的即是改變虛指標的指向。它首先&kitty,並且轉換為int*,獲得cat類的虛表首地址,同樣&jd獲得dog類的虛表地址,而p[0] = q[0]令指向cat的虛表首地址,一下就變成了指向dog類的虛表首地址,然後基類獲取到了這個指向dog類的kitty,呼叫虛方法則自然呼叫到了dog的print_age,然後這裡的age則依然保留的是cat的,因為你只是改變了虛指標指向的虛表地址,不影響member
data。
重中之重,記住一個點:類物件的首地址是虛擬函式指標地址,其次是變數地址;改變物件指標型別,將改變實函式,改變物件指標變數,將改變虛擬函式與成員變數。
其次,這程式碼不但依賴某些C++編譯器的行為,還依賴平臺的指標寬度是32位。
int * p = (int *)(&kitty);
int * q = (int *)(&jd);
p[0] = q[0];
這幾句不應該用int*,而應該用intptr_t*才對。這樣才能保證拷貝的是一個指標寬度的資料,而不是一個int寬度的資料。
在32位平臺上,int通常是32位,而指標是32位,所以正好匹配了,程式能正常執行;
在64位平臺上,如果是流行的LP64模型,int是32位而指標是64位,這裡實際上只拷貝了指標的一半,程式能否正常執行就看運氣了。
如果是在一個64位且小端(little endian)的平臺上,那這程式碼拷貝的是指標的低32位。很可能會運氣好能正常執行,因為dog類與cat類的vtable可能正好在記憶體裡處於很近的位置,它們的地址的高32位可能正好相同,地址不同的地方都在低32位,這樣這個程式就運氣好能正常執行。
如果是在一個64位且大端(big endian)的平臺上,那這段程式碼拷貝的是指標的高32位,那就完全達不到效果了。
最後,這種題還有很多玩法。例如說一種簡單的玩法是像這樣:
#include <iostream>
using namespace std;
class animal
{
protected:
int age;
public:
virtual void print_age(void) = 0;
};
class dog : public animal
{
public:
dog() {this -> age = 2;}
~dog() { }
virtual void print_age(void) {cout<<"Wang, my age = "<<this -> age<<endl;}
};
class cat: public animal
{
public:
cat() {this -> age = 1;}
~cat() { }
virtual void print_age(void) {cout<<"Miao, my age = "<<this -> age<<endl;}
};
int main(void)
{
cat kitty;
dog jd;
animal * pa;
int * p = (int *)(&kitty);
int * q = (int *)(&jd);
p[0] = q[0];
pa = &kitty;
pa -> print_age();
return 0;
}
直接整個vtable偽造出來然後想往裡面填啥就填啥。