C++反匯編第四講,反匯編中識別繼承關系,父類,子類,成員對象
講解目錄:
1.各類在內存中的表現形式 備注: 主要復習開發知識,和反匯編沒有關系,但是是理解反匯編的前提.
2.子類繼承父類
2.1 子類中有虛函數,父類中有虛函數 : 都有的情況下
2.2 子類中沒有虛函數,父類中有虛函數 : 子類沒有,父類有的情況 2.1 2.2的情況都是一樣的.
2.3 子類中有虛函數,父類中沒有虛函數 : 子有父沒有的的情況下
2.4 子類父類都沒有虛函數的情況下
第二專題大總結. 熟悉反匯編可以直接看這個總結,
3.結合第二專題的成員對象有無虛表行為
3.1成員對象有虛表的情況
3.2成員對象沒有虛表的情況
第三專題大總結
4.重載運算符的識別
5.純虛函數的反匯編
6.模版識別.
一丶各類在內存中的表現形式(復習開發知識)
講解之前,我們首先要明白C/C++中的類的內存結構.繼承之后的內存結構
普通類的內存結構:
高級代碼:
class MyTest { public: MyTest(); ~MyTest(); public: int m_int; }; MyTest::MyTest(){} MyTest::~MyTest(){} int main(int argc, char* argv[]) { MyTest test; //定義對象 return 0; }
對應內存結構圖

這是普通的一個類的內存結構圖,因為我們只有一個成員,大小是一個4字節的,所以初始化為CC
總結: 普通類根據成員進行申請內存.
帶有虛關鍵字的類(可能有虛函數或者虛構造)
PS: 類聲明同上,但是析構前邊加上了virtual 關鍵字,變為了虛析構
內存結構圖:

可以看出,申請了八個字節,啟動前4個字節是虛表指針,指向了虛表
后四個字節才是真正的為成員申請的內存.
總結: 帶有虛函數(虛關鍵字)的時候,內存中會把前4個字節當做虛表指針,並且在構造的時候初始化.
子類繼承父類,(都有虛函數的情況下)重要:
高級代碼:
class MyFather
{ public: MyFather(); virtual ~MyFather(); public: int m_int; }; MyFather::MyFather(){} MyFather::~MyFather(){} class MyChild : public MyFather //繼承 { public: MyChild(); virtual ~MyChild();
float m_flt;
}; MyChild::MyChild(){} MyChild::~MyChild(){} int main(int argc, char* argv[]) { MyChild test; //定義對象 return 0; }
內存結構圖

總共申請了12個字節,前4個字節是虛表指針,后4個字節是父類的m_int成員,在后面才是子類的真正的成員.
說到這里我們就要說下復寫虛表指針的操作.
首先我們知道: 子類構造的時候,會先構造父類,也就是說,父類的內存會先申請,並且把虛表指針填寫到前4個字節位置, 而構造完畢父類之后,構造自己的時候,這時候虛表指針又寫入子類的虛表指針了.產生了覆蓋了.
流程圖:

看上面圖可以知道,我們子類繼承父類,並且填寫了虛表指針為子類的,此時 則可以寫成 父類指針指向子類 例如: Myfather *pFa = new MyChild; pfa指向的位置就是父類區域的起始位置,
而且不會超過父類區域,所以是安全的,此時因為構造完畢,虛表指針是子類的,所以調用虛函數的時候,則是調用子類的虛函數了.
而且也說明了 為什么子類指針不能指向父類.這樣會產生越界問題.
總結:
子類繼承父類時候,有虛函數的時候,會先把頭4字節申請出來填寫為虛表指針, 而且會產生復寫(重復寫入). 第一次, 構造父類,填寫為父類指針,第二次 構造完父類則會填寫為子類的虛表指針.
二丶子類繼承父類反匯編中的結構
2.1 子類中有虛函數,父類中有虛函數 : 都有的情況下
高級代碼:
class MyFather { public: MyFather(); virtual ~MyFather(); public: int m_int; }; MyFather::MyFather(){} MyFather::~MyFather(){} class MyChild : public MyFather { public: MyChild(); virtual ~MyChild(); float m_flt; }; MyChild::MyChild(){} MyChild::~MyChild(){} int main(int argc, char* argv[]) { MyChild test; //定義對象 return 0; }
Debug下的反匯編
PS: 代碼太多,只說明這個反匯編在哪個函數中
1.main函數中找到構造

2.構造中生成的反匯編

可以看出,構造中又有一個Call,這個Call是構造父類的,構造完畢之后填寫自己的虛表指針.
3.父類構造

父類構造填寫虛表指針,也就是對象的前4個字節修改為父類的虛表指針.而后通過第二步,得出,當構造完父類之后,其前4個字節會被子類重新寫入.也就產生了復寫過程
總結:
1.子類構造的時候會先構造父類,父類構造中先填寫虛表指針.
2.父類構造完成之后,子類會重新寫入虛表指針.
3..子類繼承父類,都有虛函數的情況下,會產生復寫行為, 對象首地址4個字節處填寫虛表.
2.2 子類中沒有虛函數,父類中有虛函數 : 子類沒有,父類有的情況
PS: 高級代碼中,子類類聲明去掉了虛函數
Debug下的反匯編代碼:
1.main函數下構造的反匯編

2.構造內部反匯編

看到這一步我們明白了,首先構造父類,因為父類有虛函數,所以肯定會有虛表指針填寫,而下方也填寫了一次虛表指針.由此得出
父類有虛函數,子類沒有虛函數則子類也會有虛表.也會產生復寫行為.
總結:
父有,子沒有,子類也會有虛表,而且也會產生虛表指針復寫行為.
且只要父類有虛函數,不管子類有沒有虛函數,子類都會產生虛表,且會復寫虛表指針.
2.3 子類有虛函數,父類沒有虛函數
高級代碼: 子類中定義了虛函數,父類則把虛函數去掉了.
Debug下的反匯編代碼
1.main函數下構造

2.構造內部

看其內部得出,父類沒有虛函數的情況下,其對象 +4位置,跳過前邊的4個字節,來構造父類,構造完畢之后填寫子類虛表指針.
3.父類構造內部

父類構造內部沒有產生虛表指針填寫行為
總結:
子類有虛表,父類沒有,則會跳過虛表指針的位置來構造父類,當構造完畢父類之后前4個字節填寫子類的虛表指針.
2.4 子類,父類都沒有虛函數的情況下
直接構造內存,沒有虛表,也不會產生虛表指針復寫,可以當做結構體還原.
第二專題大總結
1.父類有虛函數,子類不管有沒有虛函數,都會有虛表
2.父類有虛函數構造的時候會填寫虛表指針,且子類也會填寫虛表指針,兩者會產生虛表指針復寫行為
3.子類中有虛函數,父類沒有,則會跳過虛表指針來構造父類,其子類會在構造完畢父類之后填寫虛表指針,不會產生虛表指針復寫行為.
三丶結合第二專題的成員對象有無虛表行為
3.1成員對象沒有虛表的情況下
高級代碼:
class MyMemberObj //成員對象 { public: MyMemberObj(){} ~MyMemberObj(){} }; class MyFather //父類 { public: MyFather(); ~MyFather(); public: int m_int; }; MyFather::MyFather(){} MyFather::~MyFather(){} class MyChild : public MyFather //子類繼承父類 { public: MyChild(); virtual ~MyChild(); MyMemberObj m_memberobj; //成員對象 float m_flt; }; MyChild::MyChild(){} MyChild::~MyChild(){} int main(int argc, char* argv[]) { MyChild test; //定義對象 return 0; }
Debug下的反匯編
1.main函數下的構造

2.構造內部

1.構造父類,因為父類沒有虛函數,所以+4構造一下,且父類有一個成員,所以申請了4個字節空間
2.成員變量的構造+8的位置開始構造,父類構造完畢之后構造,且此時成員對象沒有虛函數.
3.子類在自己的頭4個字節位置處填寫虛表指針.
3.成員對象構造內部

成員對象內部不會產生寫虛表的行為.
總結:
成員對象沒有虛函數的情況下,會在合適偏移位置處進行構造,注意合適位置處的用語,如果你是子類的成員對象,肯定會先構造父類,父類成員很多,則你的偏移位置則不固定.
3.2成員對象有虛表的情況下.
Debug下的匯編代碼:
因為其類之加了一個虛關鍵字,析構變為了虛析構,產生了虛表的動作.所以其匯編代碼1,2步沒有改變,同上.
不同的是構造的時候,成員對象有了虛函數,構造的時候則會填寫虛表.

總結:
1.有成員對象的時候其成員對象內部沒有虛表產生,則會在合適位置構造成員對象.
2.有成員對象的時候,其成員對象內部有虛表產生,則在合適位置填寫虛表指針,並且構造成員對象.
四丶反匯編中重載運算符的識別
在說重載運算符的時候,我們首先熟悉一下運算符重載的高級代碼:
簡單的運算符重載
函數類型 operator 運算符名稱 (形參表列)
{
// 對運算符的重載處理
}
高深一點的可以參考博客,這里不再重復講解.復習開發知識可以參考博客鏈接 http://c.biancheng.net/cpp/biancheng/view/215.html
高級代碼:
int operator+(MyChild& a,MyFather& b) { return (int)a.m_flt + b.m_int; } int main(int argc, char* argv[]) { MyChild a; //定義對象 MyFather b; cout << a + b << endl; return 0; }
在反匯編中,其實運算符重載就是調用函數.只不過換了一種函數的認知方式.

其實不難.當做函數還原就好.
說道這里,我們可以說下運算符重載的額外認知.
比如我們熟悉的
1.數學中的中綴式 a + b / c - d * e 這種表達式就是中綴表達式
2.波蘭式 -+a/bc*de 中綴轉化為了波蘭式,我們學習數據結構的樹的時候就學習過這種方式,這個是編譯原理中的.適用於計算機的識別.
怎么轉換的
Sub(add(a,Div(b,c),Imul(d,e); 轉為匯編代碼,比如a + b /c 我們則寫成 add(a,div(b,c),然后轉為匯編表達式即可.最終的結果則是上面寫的波蘭式.只不過按照語義,變為符號化了.
五丶純虛函數的反匯編
我們知道,純虛函數是為了子類實現了,自己不能實現,但是反匯編代碼中其實實現了,只不過里面調用了提示錯誤的API.就是為了你不小心調用的時候提示不能創建xxx對象的實例.等等一些列的錯誤.
高級代碼:
class MyFather //父類 { public: MyFather(); ~MyFather(); virtual void show() = 0; //純虛函數 }; MyFather::MyFather(){} MyFather::~MyFather(){} class MyChild : public MyFather //子類繼承父類 { public: MyChild(); virtual ~MyChild(); virtual void show(); }; MyChild::MyChild(){} MyChild::~MyChild(){} void MyChild::show() { cout << 1 << endl; } int main(int argc, char* argv[]) { MyChild a; //定義對象 a.show(); return 0; }
Debug下反匯編
我們直接看純虛函數內部了,在子類構造的時候父類會構造,父類構造自己的時候會填寫虛表指針,我們直接找父類的虛表指針即可.然后定位虛表中的第二項.
第一項是父類的虛析構,第二項才是我們的.

純虛函數在低版本就是19h,並且調用__amsg_exit,且如果弄了簽名,則是__purecall
高版本不太一樣,高版本不是簡單的這樣調用了(vs系列)它會保存當時的寄存器信息啊,什么的,然后寫日志用的.反正結果是一樣的.
高版本自己可以試試看一看有什么不同.
六丶模版識別.
模版和運算符重載一樣,都是函數,編譯為反匯編的代碼都是函數調用.而且函數和函數的重載不同,它生成的反匯編代碼有多處.
高級代碼:
template <typename T> T MySub(T a,T b) { return a - b; } int main(int argc, char* argv[]) { printf("%d\r\n",MySub(1,2));
printf("%f\r\n",MySub(3.0f,1.0f));
printf("%lf\r\n",MySub(8.3,4.3));
return 0;
}
運行結果:

Debug下反匯編.

雖然都是一樣調用,但是其內部是不同的.每個函數都有自己的匯編代碼.
