函數調用棧淺析


基本函數調用棧

相關寄存器

ebp:存儲當前棧幀的基地址
esp:存儲當前棧幀的棧頂地址
eip:存儲程序計數器值
eax:存儲函數返回值

函數調用棧結構圖

入棧過程

1、將調用者函數的ebp入棧
2、將調用者函數的棧頂指針esp賦值給被調用函數的ebp
3、按從右到左的順序將被調用函數的參數入棧
4、按聲明的順序將被調用函數的局部變量入棧
5、將調用函數的下一個指令地址作為返回地址入棧
6、將被調用函數的第一條指令地址賦值給eip寄存器
7、開始執行被調用函數指令

ebp寄存器處於一個非常重要的位置,該寄存器中存放的地址可以作為基准,向棧底方向可以獲取返回地址,傳入參數值,向棧頂方向可以獲取函數的局部變量。而esp所指向的內存中又存放着上一層函數調用的ebp值。

出棧過程

1、將函數返回值存入eax寄存器中

2、執行leave指令

1、將ebp的值賦給esp
2、將esp所指向的棧頂(上一層函數調用的ebp值)賦值給ebp,同時esp增加4。

3、執行ret指令

1、將esp所指向的棧頂(返回地址)賦值給eip,同時esp增加4。
2、修改了程序計數器eip,因此跳轉到返回地址處繼續執行。

帶異常回退的函數調用棧

棧展開

棧展開(unwinding)是指當前的try...catch...塊匹配成功或者匹配不成功異常對象后,從try塊內異常對象的拋出位置,到try塊的開始處的所有已經執行了各自構造函數的局部變量,按照構造生成順序的逆序,依次被析構。如果當前函數內對拋出的異常對象匹配不成功,則從最外層的try語句到當前函數體的起始位置處的局部變量也依次被逆序析構,實現棧展開,然后再回退到調用棧的上一層函數內從函數調用點開始繼續處理該異常。

catch語句如果匹配異常對象成功,在完成了對catch語句的參數的初始化(對傳值參數完成了參數對象的copy構造)之后,對同層級的try塊執行棧展開。

相關數據結構

struct UNWINDTBL {
	int	nNextIdx;
	void (*pfnDestroyer)(void *this);
	void 	*pObj;	
};

struct CATCHBLOCK {
	//...
	
	type_info	*piType;
	void		*pCatchBlockEntry;
}

struct TRYBLOCK {
	//...

	int 		nBeginStep;
	int		nEndStep;
	CATCHBLOCK 	tblCatchBlocks[];	
};

struct EHDL {
	//...
	
	UNWINDTBL	tblUnwind[];
	TRYBLOCK	tblTryBlocks[];
	
	//...	
};

struct EXP {
	EXP 	*piPrev; //成員指向鏈表的上一個節點,它主要用於在函數調用棧中逐級向上尋找匹配的 catch 塊,並完成棧回退工作。
	EHDL 	*piHandler; //成員指向完成異常捕獲和棧回退所必須的數據結構(主要是兩張記載着關鍵數據的表:“try”塊表:tblTryBlocks 及“棧回退表”:tblUnwind)。
	int 	nStep; //成員用來定位 try 塊,以及在棧回退表中尋找正確的入口。
};

調用棧示意圖

棧展開過程

nStep 變量用於跟蹤函數內局部對象的構造、析構階段。再配合編譯器為每個函數生成的 tblUnwind 表,就可以完成退棧機制。表中的 pfnDestroyer 字段記錄了對應階段應當執行的析構操作(析構函數指針);pObj 字段則記錄了與之相對應的對象 this 指針偏移。將 pObj 所指的偏移值加上當前棧框架基址(EBP),就是要代入 pfnDestroyer 所指析構函數的 this 指針,這樣即可完成對該對象的析構工作。而 nNextIdx 字段則指向下一個需要析構對象所在的行(下標)。

在發生異常時,異常處理器首先檢查當前函數棧框架內的 nStep 值,並通過 piHandler 取得 tblUnwind[] 表。然后將 nStep 作為下標帶入表中,執行該行定義的析構操作,然后轉向由 nNextIdx 指向的下一行,直到 nNextIdx 為 -1 為止。在當前函數的棧回退工作結束后,異常處理器可沿當前函數棧框架內 piPrev 的值回溯到異常處理鏈中的上一節點重復上述操作,直到所有回退工作完成為止。


免責聲明!

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



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