C/C++ 棧幀筆記

原創鏈接

非原創, 有刪改,僅做個人筆記


1.我們在調用它的時候系統做了什麼?

2.main函數中如果還有另一個函數,在跳轉後運行完這個函數時,編譯器怎麼知道下一行執行哪個語句呢?會不會又從頭執行了?

3.函數在結束之後(運行到反花括號“}”處),系統又是怎麼處理的?

4.不同的語言對函數形參內存的處理都是一樣的嗎?

5.函數的返回值有哪些類型?都是怎麼從函數中返回回來的呢?

示例代碼:

#include <stdio.h>
 
int sum (int a,int b)
{
    int temp = 0;
    temp = a + b;
    return temp;
}
    
int main ()
{
    int a = 10;
    int b = 20;
    int ret = 0;
    ret = sum(a,b);
    printf("ret = %d\n",ret);
    return 0;
}

運行時,從main開始執行。在調用main函數時,首先要做的就是創建main函數的棧幀。也就是說,調用一個函數之前肯定是先給他開闢棧幀,存放函數中的變量以及其它的東西。具體有什麼,我們就來看看main函數的棧幀吧

圖一
1

這是main函數最開始的樣子,其實也可以說是每個函數最開始的樣子。ebp和esp是兩個寄存器,不過它們都是在保存棧的地址,作用和指針沒什麼差別,我們就把它們看成指針。其中,ebp是棧底指針,處於高地址處,esp是棧頂指針處於低地址處。在調用了函數從左邊的花括號開始,函數就開闢好了自己的空間。esp指向一塊區域同時也是棧底

進入main之後繼續往下走,是a,b,ret三個局部變量。系統就將它們都存入main函數的棧幀中, 通過ebp減去偏移量的方式來訪問內存。

現在,我們將a、b、ret都存入棧了,現在棧長這樣:

圖二
2

接下來我們繼續執行。執行到這一句:ret = sum(a,b);

我們現在要進入sum函數去了,sum 函數有參數, 此時參數以從右向左的順序依次入棧, 訪問的話通過 ebp減去偏移量訪問, 執行完 sum 函數需要返回到當前上下文以繼續執行, 需要將 sum 函數的下一行代碼的地址記錄到棧中.

現在的棧如下:

圖三
3

該正式地調用sum函數了,對代碼進行反彙編可以看到此時執行了一個jmp指令。我們在講虛擬地址空間的時候說過給函數分配的地址是一個偏移量,偏移量加上pc寄存器的值才能跳轉到函數真實的地址。所以在調用call時,先會跳轉到jmp表確定函數的地址,再進入函數。

同時,esp發生了變化,向低地址方向移動了。這說明,在跳轉到jump這裏時,sum函數的下一行指令地址入棧了, 用於回到代碼原來位置的。現在的棧如下已經接下來sum函數的彙編代碼如下:

圖四
在這裏插入圖片描述

圖五
在這裏插入圖片描述

我們進入到了sum函數裏。不過,在往sum函數存它自帶的東西之前我們當然要先開闢sum的棧幀啦!和main函數類似,我們就藉此機會瞭解一下棧幀開闢的具體過程。

首先調用push ebp,根據上圖我們此時的ebp指向的是main函數的棧底地址。push ebp即將這個地址入棧,這就是便於sum函數執行完畢之後能讓ebp回退到main函數棧底的方法。所以這塊內存存的就是調用方函數的棧底地址。我們一開始說main函數最底下那塊“神祕區域”現在你明白裏面存的什麼了嗎?沒錯,就是調用main函數的函數(mainCRTStartup)函數的棧底指針,main函數結束後ebp會回退到mainCRTStartUp()函數的棧底!

接下來mov ebp,esp 將esp賦給ebp,即讓ebp指向esp指向的那塊區域。由於剛纔入棧了一次main函數的棧底地址,所以現在棧已經成了這樣:

圖六
5

接下來進行的操作 sub esp 0CCh 將esp+=0CC 即將棧頂指針上移了0CC,作爲sum函數的棧頂。也意味着sum函數棧幀的大小 就是0CC。接下來的對edi,eci,ecx寄存器的操作我們都不用關心,因爲它們在這例子裏什麼都沒做,只是入棧又出棧。我們要重點關心的是這兩句:

mov eax 0CCCCCCCCh

rep stos dword ptr es:[edi]

這兩句的意思是先將CCCCCCCC存進eax,再用rep stos指令(類似循環拷貝)對我們開闢的棧進行初始化賦值。於是棧變成了這樣:

圖七
圖6

這時,sum函數的棧幀就完全申請好了。main函數在創建的時候,也會經歷這樣一串固定的流程。說句題外話,0CCCCCCCC對應的漢字就是“燙”,所以我們平常如果看見一串“燙燙燙燙…”你就應該明白了,你打印的值指向的是棧中沒有人爲初始化的部分!(另一種情況是堆中未人爲初始化,顯示的是“屯”)

接下來就是存入temp這個局部變量了,當然地址是ebp-4。這時候我們的棧幀就是完全狀態了!

圖八
圖7

現在執行sum函數, 將裏面的局部變量temp等壓入棧中

這時,我們的sum函數已經執行完了,迎來了它生命的終結:“}”反花括號。那麼開闢了的棧幀要怎麼回退呢?

圖九
在這裏插入圖片描述

我們看到,首先是edi,esi,ebx這三個我們說過的沒起作用的寄存器出棧。然後mov esp,ebp即將esp的指向從棧頂直接拉到棧底,放棄棧的空間。此時回到了圖六的狀態。

然後pop ebp,這行指令的意思是先將此時棧頂的元素賦值給ebp,再將棧頂出棧。那麼這時候ebp就回到了main函數的棧底,迴歸到圖五的狀態。

接下來是ret指令。這個指令又具體幹了什麼呢?ret和pop很像,也是將當前棧頂元素(sum下一行指令的地址)賦給一個寄存器然後出棧。賦給誰呢?賦給專門存下一行運行地址的 pc寄存器了。

現在梳理一下狀況,ebp已經回到main函數的棧底,esp也由於出棧操作退回到了sum函數的兩個實參處,eax寄存器中保存着sum函數的運算結果:temp=a+b,pc寄存器中保存着sum下一行的指令。現在棧的情況是圖三.

現在sum函數已然消失,傳入的形參a和b自然也應該人去樓空了。編譯器執行add esp,8,將esp向高地址移動8位,退還a,b所佔的棧幀。此時棧狀態是圖二.

接下來,我們要將eax中存的計算結果賦給main函數中的ret變量了:

mov dword ptr [ebp-0Ch],eax

至此,sum函數正式結束。接下來就是打印函數了。當打印函數也完成後,我們的main函數也遇到了它的反花括號。重複上述的函數退棧過程,棧底指針迴歸mainCRTStartUp()中。這便是這份簡單的源代碼運行的背後的流程。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章