C++函數調用過程解析


編譯環境:Windows 10 + VS2015。

0、引言

函數調用的過程實際上也就是一個中斷的過程,本文演示和深入分析參數入棧、函數跳轉、保護現場、恢復現場等函數調用過程。

首先對三個常用的寄存器進行說明:

  • EIP:指令指針,即指向下一條即將執行的指令的地址。
  • EBP:基址指針,常用來指向棧底。
  • ESP:棧指針,常用來指向棧頂。

先看簡單程序,並在Visual Studio 2015中查看並分析匯編代碼。

圖 1

1、函數調用

g_fun函數調用的匯編代碼如圖2所示,調用g_fun函數(call指令)之前,EBP保存main函數棧基地址:0x0113FC28。

圖2

調用call指令之前,需要執行三條push指令,分別將三個參數壓入棧中。執行三條push指令指揮,可以查看棧中的數據進行驗證(從匯編指令可以看出,參數壓棧順序為從右向左)。如圖3所示,從右邊的實時寄存器表中可以看到ESP(棧頂指針)值為0x0113FB50,然后從內存表中找到內存地址0x005BFD08處,可以看到內存依次存儲了0x00000001(即參數a),0x00000002(即參數b),0x00000003(即參數c),此時棧頂存儲的是三個參數的值,說明壓棧成功。

圖3

然后可以看到call指令跳到地址0x02C1302。繼續執行,可以看到指令調轉到0x02C1700。此時,EBP值依然是0x0113FC28(main函數棧基址),說明仍然運行main函數中的指令,暫未跳轉至g_fun函數基址0x0C1700。

圖4

執行jmp指令后,調轉到了g_fun函數內部,圖5顯示0x0C1700確實是g_fun函數起始地址,如此實現了到g_fun函數的跳轉。

圖5

2、保存現場

此時,查看棧中數據,如圖6所示,此時ESP(棧頂)值為0x0113FB4C,在內存表中可以看到棧頂存放的地址是0x002C1769,下面還是前面壓棧的參數(1,2,3)。也就是執行call指令后,系統默認的往棧中壓入了一個數據(0x002C1769),再看圖3,call指令后面一條指令的地址就是0x002C1769,實際上就是調用函數結束后需要繼續執行的指令地址,函數返回后會跳轉到該地址。這就是我們常說的函數中斷前的“保護現場”。這一過程是編譯器隱含完成的。實際上是將EIP(指令指針)壓棧,即隱含執行了一條push eip指令,在中斷函數返回時,再從棧中彈出該值到EIP,程序繼續往下執行。

圖6

繼續往下執行,進入g_fun函數后第一條指令是push ebp,即將epb入棧。因為每一個函數都有自己的棧區域沒所以基地址也是不一樣的。現在進入了一個中斷函數,函數執行過程中也需要ebp寄存器,而在進入函數之前的main函數的ebp值怎么辦呢?為了不覆蓋,將它壓入棧中保存。執行push ebp指令后,查看寄存器和內存中數據顯示EBP存放的地址(main函數基地址0x0113FC28)確實壓入ESP所指向的棧頂地址0x0113FB48。

圖7

下一條mov ebp, esp將此時的棧頂地址作為該函數的棧基址,確定g_gunc函數的棧區域,EBP棧底地址為0x0113FB48,ESP棧頂地址為0x0113FB48。

圖8

再往下的指令是sub esp,D8h,指令的字面意思是將棧頂指針往上移動D8h Byte。這個區域為間隔區域,將兩個函數的棧區域隔開一段距離。如圖8所示。而該間隔區域大小固定為D0h,即208Byte,然后還要預留出存儲局部變量的內存區域。g_fun函數有兩個局部變量x和y,所有esp需要移動的長度為D0h+8h=D8h。

圖9

執行sub esp,D8h指令后,EBP棧基址不變為,仍為0x0113FB48。ESP棧頂地址在0x0113FB48基礎上往上移動往上移動D8h Byte,變為0x0113FA70。如圖10所示:

圖10

接下來的三條壓棧指令,分別將EBX,ESI,EDI壓入棧中,這也是屬於保護現場的一部分,這些是屬於main函數的一些數據。EBX,ESI,EDI分別為基址寄存器,源變址寄存器,目的變址寄存器。

圖11

接下來的幾條指令(如下)是剛才留出的D8h的內存區域賦值為0x0CCCCCCCh。

002C170C  lea         edi,[ebp-0D8h]  
002C1712  mov         ecx,36h  
002C1717  mov         eax,0CCCCCCCCh  
002C171C  rep stos    dword ptr es:[edi]

如圖12所示:

圖12

3、執行函數

繼續往下看,接下來是局部變量x和y的賦值,匯編指令中怎樣去計算x和y的地址呢?如圖13所示,是基於ebp去計算的,分別是[ebp-4-4]和[epb-4-8],為什么需要多每次計算多要先行上移4個Byte呢?應該是變量之間增加間隔區域(固定值為4 Byte),保護變量之間互不影響,跟函數間隔區域類似。查看內存表可以看到響應的內存區域已經存入了0x11111111和0x22222222。

圖13

此時我們對整個內存中存儲的內容應該非常清晰了。如圖14所示:

圖14

4、恢復現場

這時,子函數部分的代碼已經執行完畢,繼續往下看,編譯器會做一些事后處理工作,如圖15所示。首先是三條出棧指令,分別從棧頂讀取EDI,ESI和EBX值。從圖9的內存數據分別我們可以得知此時棧頂的數據確實是EDI,ESI和EBX,這樣就恢復了調用前的EDI,ESI和EBX值。這是“恢復現場”的一部分。

圖15

第四條指令是mov esp,ebp即將epb的值賦給esp。什么意思呢?看看圖14的內存數據分布就明白了,這條語句是讓ESP指向EBP所指向的內存單元,也就是讓ESP跳過一段區域,很明顯掉過的區域恰好是間隔區和局部數據區域,因為函數已經退出了,這兩個區域都已經沒有用處了。實際上這條語句是進入函數時創建間隔區的語句sub esp,D8h的相反操作。這也剛好說明了調用函數時在棧上自動申請內存,調用結束后自動釋放內存的操作。

圖16

再往下是pop ebp,我們從圖14的內存數據分布可以看出此時棧頂確實是存儲的前EBP值,這個就恢復了調用前的EBP值(0x0113FC28),這也是“恢復現場”的一部分。該指令執行完后,內存數據分布如圖17和圖18所示。

圖17

圖18

再往下是一條ret指令,即返回指令。注意再執行指令前ESP值和EIP值(如圖19所示),ESP指向棧頂地址0x0113FB4C存放的是地址0x002C1769(調用g_fun函數call指令的下一個指令地址)。

圖19

執行ret指令后,查看ESP和EIP值(如圖20所示),此時ESP為0x0113FB50,即往下移動了4Byte。顯然此處編譯器隱含執行了一條pop指令。這個值怎么這么熟悉呢!它實際上就是棧頂的4Byte數據,所以這里隱含執行的指令應該是pop eip。而這個值就是前面講到過的,在調用call指令前壓棧的call的下一條指令的地址。從圖20中可以看出,正是因為EIP的值變成了0x002C1769,所以程序跳轉到了call指令后面的一條指令,又回到了中斷前的地方,這就是所謂的恢復斷點。

 圖20

還沒有完全結束,此時還有最后一條指令add esp, 0Ch。這個就很簡單了,從圖20中可以看出現在棧頂的數據是1,2,3,也就是函數調用前壓入的三個實參。這是函數已經執行完了,顯然這三個參數沒有用處了。所以add esp, 0Ch就是讓棧頂指針往下移動12Byte的位置,ESP地址由0x0113FB50變成0x0113FB5C。為什么是12Byte呢,很簡單,因為入棧的是3個int數據。這樣由於函數調用在棧中添加的所有數據都已清除,棧頂指針(ESP)真正回到了函數調用前的位置,所有寄存器的值也恢復到了函數調用之前。如圖21所示:

圖21

到處為止,函數調用結束。 


免責聲明!

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



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