C++函數調用過程深刻分析函數
原文地址:http://blog.csdn.net/dongtingzhizi/article/details/6680050#reply.net
0. 引言指針
函數調用的過程實際上也就是一箇中斷的過程,那麼C++中究竟是怎樣實現一個函數的調用的呢?參數入棧、函數跳轉、保護現場、回覆現場等又是怎樣實現的呢?本文將對函數調用的過程進行深刻的分析和詳細解釋,並在VC 6.0環境下進行演示。分析不到位或者存在錯誤的地方請批評指正,請與做者聯繫。blog
首先對三個經常使用的寄存器作一下說明,EIP是指令指針,即指向下一條即將執行的指令的地址;EBP爲基址指針,經常使用來指向棧底;ESP爲棧指針,經常使用來指向棧頂。ip
看下面這個簡單的程序並在VC 6.0中查看並分析彙編代碼。內存
圖1get
1. 函數調用編譯器
g_func函數調用的彙編代碼如圖2:編譯
圖2變量
首先是三條push指令,分別將三個參數壓入棧中,能夠發現參數的壓棧順序是從右向左的。這時咱們能夠查看棧中的數據驗證一下。如圖3所示,從右邊的實時寄存器表中咱們能夠看到ESP(棧頂指針)值爲0x0012FEF0,而後從中間的內存表中找到內存地址0x0012FEF0處,咱們能夠看到內存中依次存儲了0x00000001(即參數1),0x00000002(即參數2),0x00000003(即參數3),即此時棧頂存儲的是三個參數值,說明壓棧成功。
圖3
而後能夠看到call指令跳轉到地址0x00401005,那麼該地址處是什麼呢?咱們繼續跟蹤一下,在圖4中咱們看到這裏又是一條跳轉指令,跳轉到0x00401030。咱們再看一下地址0x00401030處,在圖5中能夠看到這纔是真正的g_func函數,0x00401030是該函數的起始地址,這樣就實現了到g_func函數的跳轉。
圖4
圖5
2. 保存現場
此時咱們再來查看一下棧中的數據,如圖6所示,此時的ESP(棧頂)值爲0x0012FEEC,在內存表中咱們能夠看到棧頂存放的是0x00401093,下面仍是前面壓棧的參數1,2,3,也就是執行了call指令後,系統默認的往棧中壓入了一個數據(0x00401093),那麼它到底是什麼呢?咱們再看到圖3,call指令後面一條指令的地址就是0x00401093,實際上就是函數調用結束後須要繼續執行的指令地址,函數返回後會跳轉到該地址。這也就是咱們常說的函數中斷前的「保護現場」。這一過程是編譯器隱含完成的,其實是將EIP(指令指針)壓棧,即隱含執行了一條push eip指令,在中斷函數返回時再從棧中彈出該值到EIP,程序繼續往下執行。
圖6
繼續往下看,進入g_func函數後的第一條指令是push ebp,即將ebp入棧。由於每個函數都有本身的棧區域,因此棧基址也是不同的。如今進入了一箇中斷函數,函數執行過程當中也須要ebp寄存器,而在進入函數以前的main函數的ebp值怎麼辦呢?爲了避免被覆蓋,將它壓入棧中保存。
下一條mov ebp, esp 將此時的棧頂地址做爲該函數的棧基址,肯定g_func函數的棧區域(ebp爲棧底,esp爲棧頂)。
再往下的指令是sub esp, 48h,指令的字面意思是將棧頂指針往上移動48h Byte。那爲何要移動呢?這中間的內存區域用來作什麼呢?這個區域爲間隔空間,將兩個函數的棧區域隔開一段距離,如圖7所示。而該間隔區域的大小固定爲40h,即64Byte,而後還要預留出存儲局部變量的內存區域。g_func函數有兩個局部變量x和y,因此esp需移動的長度爲40h+8=48h。
圖7
接下來的幾行指令(以下)是將剛纔留出的48h的內存區域賦值爲0CCCCCCCCh。
00401039 lea edi,[ebp-48h]
0040103C mov ecx,12h
00401041 mov eax,0CCCCCCCCh
00401046 rep stos dword ptr [edi] 。
接下來三條壓棧指令,分別將EBX,ESI,EDI壓入棧中,這也是屬於「保護現場」的一部分,這些是屬於main函數執行的一些數據。EBX,ESI,EDI分別爲基址寄存器,源變址寄存器,目的變址寄存器。
3. 執行子函數
繼續往下看,接下來是局部變量的x和y的賦值,看彙編指令中是怎樣去計算x和y的內存地址的呢?如圖8所示,是基於ebp去計算的,分別是[ebp-4]和[ebp-8]。咱們查看內存表能夠看到相應的內存區域已經存入了0x11111111和0x22222222。
圖8
此時咱們對整個內存區域中存儲的內容應該很是清晰了(如圖9所示)。
圖9
4. 恢復現場
這時子函數部分的代碼已經執行完,繼續往下看,編譯器將會作一些過後處理的工做(如圖10所示)。首先是三條出棧指令,分別從棧頂讀取EDI,ESI和EBX的值。從圖9的內存數據分佈咱們能夠得知此時棧頂的數據確實是EDI,ESI和EBX,這樣就恢復了調用前的EDI,ESI和EBX值,這是「恢復現場」的一部分。
圖10
第四條指令是mov esp, ebp 即將ebp的值賦給esp。那這是什麼意思呢?看看圖9的內存數據分佈,咱們就能很明白了,這條語句是讓ESP指向EBP所指的內存單元,也就是讓ESP跳過了一段區域,很明顯跳過的剛好是間隔區域和局部數據區域,由於函數已經退出了,這兩個區域都已經沒有用處了。實際上這條語句是進入函數時建立間隔區域的語句 sub esp, 48h的相反操做。
再往下是pop ebp,咱們從圖9的內存數據分佈能夠看出此時棧頂確實是存儲的前EBP值,這樣就恢復了調用前的EBP值,這也是「恢復現場」的一部分。該指令執行完後,內存數據分佈如圖11所示。
圖11
再往下是一條ret指令,即返回指令,他會怎麼處理呢?注意在執行ret指令前的ESP值和EIP值(如圖12所示),ESP指向棧頂的0x00401093,EIP的值是0x0040105C(即ret指令的地址)。
圖12
執行ret指令後咱們來查看ESP和EIP值(如圖13所示),此時ESP爲0012FEF0,即往下移動了4Byte。顯然此處編譯器隱含的執行了一條pop指令。再來看一下EIP的值,變爲了0x00401093,這個值怎麼這麼熟悉呢!它實際上就是棧頂的4Byte數據,因此這裏隱含執行的指令應該是pop eip。而這個值就是前面講到過的,在調用call指令前壓棧的call的下一條指令的地址。從圖13中能夠看出,正是由於EIP的值變成了0x00401093,因此程序跳轉到了call指令後面的一條指令,又回到了中斷前的地方,這就是所謂的恢復斷點。
圖13
尚未徹底結束,此時還有最後一條指令add esp, 0Ch。這個就很簡單了,從圖13中能夠看出如今棧頂的數據是1,2,3,也就是函數調用前壓入的三個實參。這是函數已經執行完了,顯然這三個參數沒有用處了。因此add esp, 0Ch就是讓棧頂指針往下移動12Byte的位置。爲何是12Byte呢,很簡單,由於入棧的是3個int數據。這樣因爲函數調用在棧中添加的全部數據都已清除,棧頂指針(ESP)真正回到了函數調用前的位置,全部寄存器的值也恢復到了函數調用以前。
結束!