C++反匯編第二講,不同作用域下的構造和析構的識別


               C++反匯編第二講,不同作用域下的構造和析構的識別

目錄大綱:

  1.全局(靜態)對象的識別,(全局靜態全局一樣的,都是編譯期間檢查,所以當做全局對象看即可.)

    1.1 探究本質,理解構造和析構的生成,以及調用方式(重要,如果不想知道,可以看總結.)

  2.對象做函數參數的識別 

  3.返回值為對象的識別

  4.對象為靜態局部的識別

  5.堆中對象識別

      5.1. malloc和new的區別,free 和delete的區別

  6.對象數組

     6.1, delete對象和 delete[] 對象數組的區別 

一丶全局對象的識別

  對於全局對象,以及全局變量等等.這些初始化,都是在ininterm中初始化的,和全局變量初始化的位置一樣,如果不太懂,請看.以前博客鏈接:

http://www.cnblogs.com/iBinary/p/7912427.html

建立高級代碼,查看其調用棧.(最后會總結)

高級代碼:

  

class MyTest
{
    MyTest();
    ~MyTest();

};

MyTest::MyTest()
{
    printf("111\r\n");
}

MyTest::~MyTest()
{
    printf("222\r\n");

}
MyTest test();              //創建對象在全局
int main(int argc, char* argv[])
{
    
    return 0;
}

查看調用棧回朔:

  

調用棧的順序依次是

  initterm  -> E4(代理) - > E1(代理)  ,熟悉完探究原理和本質的時候再來講解E4 和E1代理是干啥用的.

1.1探究原理,追求本質.構造和析構的生成,構造的調用和析構的調用

1.熟悉 ininterm原理

我們以前講過 ininterm函數里面的原理和本質(不熟悉看下方圖)它會根據函數的起始和結束地址,循環遍歷並且調用同一接口的函數進行初始化動作.

那么現在E4代理函數就是統一接口的,也就是說, ininterm函數循環的函數指針調用,都是調用E4代理函數

2.熟悉構造函數何時調用,E1代理, E3代理函數.

現在我們知道了ininterm函數為了統一接口,所以弄出來了一個E4代理函數,為了統一接口

E4代理函數內部:

那么E4代理里面做了什么事情.

 

可以看出,E4代理里面調用了E1代理和E3代理

關於E1代理,我們知道,它是為了統一參數而生成的一個代理,其內部調用我們的真正代碼,(也就是構造函數)

E1函數代理內部

  

E3代理,E3代理稍后講解,我們要知道E3是干什么用的要先知道一個C庫函數的作用.

3.E3代理內部,以及C庫函數作用

 C庫函數,atexit  注冊函數回調,main函數結尾的時候進行收尾動作(也就是釋放資源的動作)

這個C庫函數在C語言時代就是釋放資源的.

看下MSDN聲明.

注冊一個C約定的函數回調即可.看下程序例子:

  高級代碼:

void Abc()
{
    printf("1234\r\n");
}
int main(int argc, char* argv[])
{
    atexit(Abc);          //注冊 C約定函數指針,當main函數結束的時候操作系統調用這個函數.
    return 0;
}

運行程序結果

正文:

  atexit可以注冊多個回調,而這些會是一個線性表,里面儲存了你注冊的函數地址.當main函數結束的時候會調用

而內部

do exit函數內部會執行核心代碼:

  

代碼含義,一開始沒有注冊的時候, 線性表的頭和尾都是一樣的位置

當你注冊了那么線性表則會增加4個字節存儲你注冊的函數回調地址.

可以看出上面代碼邏輯

  從后往前調用,執行函數指針, 而這個函數則是你注冊的函數回調.

E3代理含義:

  明白其上面的 atexit函數的原理,那么現在看看其E3內部的實現

E3內部其實是將E2函數注冊進了atexit函數,當結束的時候則會調用E2

那么現在看看E2

E2函數內部:

  

E2函數內部則會調用析構函數,有人會說,為什么不直接將析構注冊為函數回調,這樣直接調用atexit不就在釋放的時候,從后往前依次調用析構的了嗎.

答:

  因為atexit的參數的c約定回調,而析構是thiscall,調用約定,所以內部必須包含一層才可以.

 

總結:

  當為全局對象的時候

  1.會在ininterm里面進行初始化動作

  2.會產生代理函數,這個代理函數是為了使ininterm函數的代碼正常初始化而產生的一個統一接口的函數,暫且稱為E4 (名字可能不一樣)

  3.E4函數代理是為了統一接口,其內部又調用了 構造函數代理 (E1),和析構函數代理(E3)

  4.E1代理函數是為了統一參數用的,其內部是調用構造的,如果是有參數構造,則在E1代理函數內部可以看到傳參的.

  5.E3代理函數是為了注冊析構函數的,為了使atexit函數正常運行而注冊的(atexit和ininterm類似,一個從前往后,一個從后往前)

  6.E2是E3內部給atexit函數注冊的回調,這樣在析構的時候則調用E2即可.

  7.E2函數內部是真正的調用析構的.

調用流程圖:

  

實戰中反匯編查找全局對象

既然我們知道了atexit函數會調用析構,那么我們在IDA中搜索atexit函數,看看誰引用了它,則可以把全局對象一網打盡.

 

 二丶對象作為函數參數的識別

高級代碼:

  PS: 為了節省篇幅,類的定義不在重復截圖,重復定義了.

void foo(MyTest test)
{
    printf("333\r\n");
}

int main(int argc, char* argv[])
{
    MyTest t;                   //定義對象
    foo(t);                     //對象當做參數傳遞
    return 0;
}

Debug下的匯編:

  

很明顯的特征

  1.函數調用前會調用一次構造

  2.調用函數

  3.函數結束之前調用析構. (foo函數內部,為了節省篇幅,和Release)

  4.函數結束之后繼續調用構造

Release版本匯編:

  

上面包含了 1 2 4步,其中第三步是在 foo函數內部調用的析構

foo 函數內部

內部會有個Jmp來調用析構

 

總結: 

  當函數參數為對象的時候.

  1.會先在函數外部進行構造一次

  2.調用函數

  3.函數內部調用一次析構

  4.函數結束之后的外面調用一次析構函數.

PS: 注意,局部對象和傳參的區別,局部對象會在函數內部進行調用構造,而傳參的時候是在函數外面進行的初始化動作

 三丶返回值為對象的識別

當返回值為對象的時候,會有兩種情況

1.定義的時候產生拷貝動作

2.使用的時候產生臨時對象

例如:

  MyTest t = Getobj(); 定義t的同時,接受Getobj返回的對象,則會產生拷貝構造

  t = Getobj():    定義完obj然后使用t接受Getobj()則會產生臨時對象.不產生拷貝構造

以上都是C++語言,不熟悉的同學復習一下構造析構以及拷貝構造的內容即可.

 

1.拷貝動作的時候其返回對象的識別.

  高級代碼:

    

MyTest Getobj()
{
    MyTest obj;
    return obj;
}
int main(int argc, char* argv[])
{
    MyTest t = Getobj();          //定義同時,接受返回對象         
    return 0;
}

Debug下的匯編代碼:

  

1.調用的時候,當做參數傳遞給Getobj

3.函數結束之后調用析構

2.函數內部調用構造和析構

(其中2在Getobj里面,看Release版本)

Release下的匯編

上面是第一步和第三步

第二步函數內部:

  

其內部調用構造和析構

 

總結:

  1.this指針會當做參數傳遞給函數, Mytest t = Getobj() t會當做參數傳遞

  2.其函數內部開始的時候會調用構造函數,結束之前調用析構

  3.函數結束之后,外部會調用析構函數.

 PS: 當代嗎為引用的時候,其作用域跟着引用走  Mytest &t = Getobj();

2.使用的時候產生臨時對象的情況下

  高級代碼:

  

MyTest Getobj()
{
    MyTest obj;
    return obj;
}
int main(int argc, char* argv[])
{
    MyTest t ;
    t = Getobj();      //定義完畢之后使用             
    return 0;
}

Debug下的匯編

  

1.  T變量進行構造

2.產生臨時對象,調用GetObj, 其中Getobj內部會構造和析構,然后返回臨時一般來那個

3.返回的臨時變量給棧變量保存,然后 mov edx,[ecx] 給edx賦值

4.臨時變量拷貝給t

5.臨時變量析構

6.main結束前局部變量析構

Release下的匯編

Release匯編和Debug一樣,減少了變量,進行了優化.

 

總結:

  使用時獲得對象則產生臨時對象

  1.局部對象進行構造

  2.調用函數的時候產生臨時對象,其內部產生構造和析構

  3.返回的時候返回值給使用的對象賦值

  4.臨時對象析構

  5.main結束時局部對象析構.

 

 

 

MyTest Getobj()
{
    MyTest obj;
    return obj;
}
int main(int argc, char* argv[])
{
    MyTest *t ;
    t = &Getobj();      //定義完畢之后使用       
    t->m_dwNumber = 1;
    return 0;
}

 

 

 

 

Debug下反匯編

我們會發現

返回的臨時對象會給t保存

但是緊接着析構了,但是此時指針調用了臨時對象里面的成員,並且給它賦值了.所以以后寫代碼要注意,這種錯誤編譯器檢測不出來.雖然支持這個語法.但是肯定會出錯,而且是莫名其妙的錯誤

 

 四丶對象為靜態局部的識別

  高級代碼:

  

int main(int argc, char* argv[])
{
    static MyTest t ;
    
    
    return 0;
}

Debug下的匯編

會生成一個檢查標志,根據這個標志判斷,是否調用構造和析構

會跳過一個 構造和注冊析構的一塊區域

 

總結:

   生成檢查標志,跳過構造和注冊析構代理.

 

五.堆中對象識別

高級代碼:

  

MyTest *t = new MyTest ;

Debug下的匯編:

new 和malloc是一樣的,new是對malloc的一個封裝. 只會申請空間,但是會產生額外的代碼,中間會判斷標志,申請成功的返回值為0或者為1,如果為0則不構造,如果為1則構造

但是注意:這里的額外代碼只是判斷是否進行構造,你自己也要進行判斷.

Delete語法

Delete語法會調用析構,也會生成額外語法.

 當Delete的時候會傳入1, 這個是按位來的, 如果最低位為1,則是代表釋放內存,那么就調用析構並且釋放,如果為0,則僅僅代表了調用析構.

為什么會這樣:

  在早期,硬件資源匱乏,內存想重復利用.

所以會有人顯示的調用構造(vc6.0中可以)然后顯示的調用析構進行管理,示例:

加上類域則可以調用構造了,那么析構我們是顯示調用,所以看看匯編代碼,會傳入0,不會釋放內存的.

 

總結:

  1.new 和malloc 一樣,new是對malloc的一個封裝,但是會產生額外代碼,用來判斷是否進行構造

  2.delete的時候,會傳入0 和1來判斷是否是 調用析構並釋放內存(1) ,或者 只調用析構(0)

 

時間關系,明天繼續補充. 剩下了對象數組,可以提前看一下.

 


免責聲明!

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



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