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)
時間關系,明天繼續補充. 剩下了對象數組,可以提前看一下.