(C語言內存十三)一個函數在棧上到底是怎樣的?


引言

函數的調用和棧是分不開的,沒有棧就沒有函數調用,本節就來講解函數在棧上是如何被調用的。

棧幀/活動記錄

當發生函數調用時,會將函數運行需要的信息全部壓入棧中,這常常被稱為棧幀(Stack Frame)或活動記錄(Activate Record)。活動記錄一般包括以下幾個方面的內容:

1) 函數的返回地址,也就是函數執行完成后從哪里開始繼續執行后面的代碼。例如:

int a, b, c;
func(1, 2);
c = a + b;

站在C語言的角度看,func() 函數執行完成后,會繼續執行c=a+b;語句,那么返回地址就是該語句在內存中的位置。
注意:C語言代碼最終會被編譯為機器指令,確切地說,返回地址應該是下一條指令的地址,這里之所以說是下一條C語言語句的地址,僅僅是為了更加直觀地說明問題。
2) 參數和局部變量。有些編譯器,或者編譯器在開啟優化選項的情況下,會通過寄存器來傳遞參數,而不是將參數壓入棧中,我們暫時不考慮這種情況。

3) 編譯器自動生成的臨時數據(運行時)。例如,當函數返回值的長度較大(比如占用40個字節)時,會先將返回值壓入棧中,然后再交給函數調用者。當返回值的長度較小(char、int、long 等)時,不會被壓入棧中,而是先將返回值放入寄存器,再傳遞給函數調用者。
4) 一些需要保存的寄存器,例如 ebp、ebx、esi、edi 等。之所以要保存寄存器的值,是為了在函數退出時能夠恢復到函數調用之前的場景,繼續執行上層函數。

實例

下圖是一個函數調用的實例:

上圖是在Windows下使用VS2010 Debug模式編譯時一個函數所使用的棧內存,可以發現,理論上 ebp 寄存器應該指向棧底,但在實際應用中,它卻指向了old ebp。
在寄存器名字前面添加“old”,表示函數調用之前該寄存器的值。
當發生函數調用時:
實參、返回地址、ebp 寄存器首先入棧;
然后再分配一塊內存供局部變量、返回值等使用,這塊內存一般比較大,足以容納所有數據,並且會有冗余;
最后將其他寄存器的值壓入棧中。

需要注意的是,不同編譯器在不同編譯模式下所產生的函數棧並不完全相同,例如在VS2010下選擇Release模式,編譯器會進行大量優化,函數棧的樣貌盪然無存,不具有教學意義,所以本教程以VS2010 Debug模式為例進行分析。

關於數據的定位

由於 esp 的值會隨着數據的入棧而不斷變化,要想根據 esp 找到參數、局部變量等數據是比較困難的,所以在實現上是根據 ebp 來定位棧內數據的。ebp 的值是固定的,數據相對 ebp 的偏移也是固定的,ebp 的值加上偏移量就是數據的地址。

例如一個函數的定義如下:

void func(int a, int b){
    float f = 28.5;
    int n = 100;
    //TODO:
}

調用形式為:

func(15, 92);

那么函數的活動記錄如下圖所示:

這里我們假設兩個局部變量挨着,並且第一個變量和 old ebp 也挨着(實際上它們之間有4個字節的空白),如此,第一個參數的地址是 ebp+12,第二個參數的地址是 ebp+8,第一個局部變量的地址是 ebp-4,第二個局部變量的地址是 ebp-8。


免責聲明!

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



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