C++反匯編第四講,反匯編中識別繼承關系,父類,子類,成員對象


 

              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下反匯編.

 

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM