1. 程式人生 > >C++ 虛指標、成員變數與類物件的偏移地址

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偽造出來然後想往裡面填啥就填啥。