《C++反匯編與逆向分析技術揭秘》之十——構造函數


對象生成時會自動調用構造函數。只要找到了定義對象的地方,就找到了構造函數調用的時機。不同作用域的對象的生命周期不同,如局部對象、全局對象、靜態對象等的生命周期各不相同,只要知道了對象的生命周期,便可以推斷出構造函數的調用時機

  • 局部對象

反匯編:

獲取對象首地址並調用構造函數:

對象的地址為:

進入構造函數,先是push一堆寄存器:

 

還原ecx寄存器,並初始化:

 構造函數屬於成員函數,在調用時要用到this指針。

如何識別?1、構造函數時這個對象在作用域內調用的第一個成員函數,根據this指針可以區分每個對象;2、返回this指針是構造函數的特征之一(這是識別局部變量構造函數的必要條件)

 

  • 堆對象

堆對象的識別重點在於識別堆空間的申請與使用

舉例:

new以及new的大小:

 返回的地址:

調用構造函數:

先判斷new是否從成功,如果失敗就跳過構造函數:

 

執行構造函數初始化對象:

定位到對象的第一個成員變量,並賦值為0xb:

new對象返回對象的首地址時,也檢查了是否為NULL,進行了一次判斷,如果失敗就跳過構造函數,所以可以從這一點入手,找到堆對象的構造函數

 考慮下我們這里有兩個成員變量的情況:

new的大小變了:

給第二個成員變量賦值時的定位變了:

 

  • 參數對象

當對象作為函數參數時,調用一個特殊的構造函數——拷貝構造函數。拷貝構造函數只有一個參數,即為對象的引用

例子如下:

 

 1、初始化一個MyString對象,先調用了一個autoclassinit函數:

 

類對象的地址為:

構造函數內部在初始化成員m_pString時調用了memset函數:

這里的ecx就是類對象的首地址。

2、調用構造函數:

ecx保存的是類對象,給成員賦值:

3、調用成員函數SetString:

里邊會先獲取字符串長度:

然后再去拷貝:

參數:

返回:

 4、Show(MyString)利用了拷貝構造函數(本小節重點)

先調用一個拷貝構造函數,拷貝MyString。即一個新的CMyString對象調用拷貝構造函數,這個拷貝構造函數的參數是前面的MyString對象。

拷貝構造函數的調用,且新的對象的地址就是esp的值

拷貝構造函數中,會用this作為返回值

 

其中ecx是新的CMyString對象的首地址:

eax是前面定義的、這里被拷貝的MyString對象的地址:

返回的是生成的新對象的地址,也是esp的值:

調用完拷貝構造函數后緊接着調用Show函數,並沒有push操作來壓入參數:

 

其參數就是新返回的對象的成員(這里我們發現,剛剛返回的新的CMyString的地址就是esp的值):

 

驗證:

如果CMyString中有兩個成員變量,那么在給Show傳參的時候,就壓入了兩個數據:

傳給Show的參數其實並不是通過push操作壓入的參數,而是通過拷貝構造函數幫忙壓入的。因為拷貝構造函數新構造的那個對象的地址就是esp所指向的位置,所以拷貝構造函數執行完成之后生成的數據就直接放在了棧頂位置

  • 返回對象 

返回時需要對返回對象進行拷貝,因此同樣會使用到拷貝構造函數

舉例:

 

 

 除了那個autoclassInit函數之外,其實就調用了GetMyString這么一個函數:

 那么可以說明給MyString對象賦值的情況是在GetMyString中完成的。而我們可以知道,GetMyString中並沒有刻意實現給MyString賦值的功能,那么究竟是如何做到的呢?

GetMyString()並沒有參數,但是還是push了一個eax,這個eax是由autoclassInit函數返回的僅僅是一個地址,也就是MyString的地址(但是沒有調用MyString的構造函數進行初始化):

隨后以這個eax為參數調用了GetMyString,進入其中我們來到return MyString的部分,這里調用了一個拷貝構造函數:

寄存器的值為:

也就是說,是函數外面創建的那個對象,復制了函數內部的臨時的對象。並返回12FF5C:

這里我們可以看出,並不是GetMyString得到一個對象后,再去拷貝給某個定義好的等待接收的變量,而是在GetMyString過程中,就給main中的MyString變量構建好了(借助傳遞地址參數)

 

  • 全局對象與靜態對象

 我們必須清楚的知道全局對象與靜態對象構造的時機。所有的全局對象都會在同一地點調用構造函數進行初始化,即_cinit函數。_cinit的_initterm函數中逐一初始化了全局對象。

定位方法一——直接定位法:mainCRTStartup->_cinit->_initterm->構造代理函數

定位方法二——利用棧回溯:全局對象的地址固定,可以先對這些數據下讀寫斷點。

定位方法三——定位atexit:對atexit下斷點。

 

進入:

先找到:

在下面這個循環中,會調用構造代理函數:

 進入這個call eax函數中,跟蹤找到了一個構造函數,但並不一定是我們定義的那全局對象的構造函數:

 繼續找,找到第一個全局對象,一般先是調用構造函數再有一個注冊析構函數

構造函數內部:

構建時先是初始化一塊地址:

返回值:

 調用了帶一個參數的構造函數:

其中賦值:

繼續找找到第二個全局對象:

考慮,如果是一個全局變量數組,那么構造函數是如何被調用的呢?比如:

那么不會是分別調用三次call eax函數,每次構造一個對象。而是調用一次call eax函數,里邊調用一個構造代理函數來分別構建三個對象:

進入構造代理函數內部,發現下面的循環逐一調用構造函數:

其邏輯類似於書上P249所述。

  •  每個對象都有默認構造函數嗎

舉例:

這里不會為CMyString提供默認的構造函數:

這里采用的做法是給ecx寄存器傳入一個地址,給SetInt傳入一個參數,然后把這個參數的值寫入對應的地址:

但是如果你在類中定義了構造函數,哪怕是什么也沒做,也會調用構造函數的,因為你一旦自己定義了一個構造函數,就不會被提供任何默認構造函數了

所以我們一直要討論的,就是沒有提供構造函數的情況下,編譯器是否會給你提供默認構造函數。

以下幾種情況會提供默認的構造函數:

1、本類中存在虛函數

默認的構造函數里進行了虛表的初始化操作:

虛表中的內容如下:

2、本類中定義的成員對象有虛函數

如果類的成員對象只是普通的成員函數,則該類沒有構造函數:

但是如果成員對象由虛函數:

構造函數內部調用了成員對象的構造函數,其目的顯然是為了初始化虛表

 

3、父類中存在虛函數(道理同上)

 4、父類中定義的對象帶有構造函數

5、本類中定義的對象帶有構造函數

 


免責聲明!

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



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