函數幀
這標題一念出來我立刻想到了一個名人:白素貞……當然, 此女與本文無關,下面進入正題:
其實程序運行就好比一幀一幀地放電影,每一幀是一次函數調用,電影放完了,我們就看到結局了。
我們用一個遞歸求解階乘的程序來看看這個放映過程(fac.c):
#include <stdio.h>
int fac(int n)
{
if(n <= 1)
return 1;
return n * fac(n-1);
}
int main()
{
int n = 3;
int ans = fac(n);
printf("%d! = %d\n", n, ans);
return 0;
}
main 幀
首先 main 函數被調用(程序可不是從 main 開始執行的):
|
main 函數創建了一幀:
- 從 esp 到 ebp + 4
- 上邊是本次調用的返回地址、舊的 ebp 指針
- 然後是 main 的局部變量 n、ans
- 最下邊是參數的空間,右上圖顯示的是 main 中調用 printf 前的棧的使用情況
進入 main 函數,前 4 條指令開闢了這片空間, 在退出 main 函數之前的 leave ret 回收了這片空間 (C++ 在回收這片空間之前要析構此函數中的所有局部對象)。 在 main 函數執行期間 ebp 一直指向 幀頂 - 4 的位置, ebp 被稱為幀指針也就是這個原因。
調用慣例
調用函數的時候,先傳參數,然後 call, 具體這個過程怎麼實現有相關規定,這樣的規定被稱為調用慣例, C語言中有多種調用慣例,它們的不同之處在於:
- 參數是壓棧還是存入寄存器
- 參數壓棧的次序(從右至左 | 從左至右)
- 調用完成後是調用者還是被調用者來恢復棧
各種調用慣例《程序員的自我修養》——鏈接、裝載與庫 這本書中有簡要介紹,我照抄後在本文後面列出。C語言默認的 調用慣例是 cdecl:
- 參數從右至左壓棧
- 調用完成後調用者負責恢復棧
可以從 printf("%d! = %d\n", n, ans); 的調用過程 中看出。
雖然 VC、gcc 都默認使用 cdecl 調用慣例, 但它們的實現卻各有風格:
- VC 一般是從右至左 push 參數,call,add esp, XXX
- 而 gcc 在給局部變量分配空間的時候也給參數分配了足夠的空間, 所以只要從右至左 mov 參數, XXX(%esp),call 就可以了, 調用者根本不用去恢復棧,因為傳參數的時候並沒有修改棧指針 esp。
fac 幀
說完調用慣例我們接著來看第一次調用 fac:
|
fac(3) 開闢了第一個 fac 幀:
- 從 esp 到 ebp + 4(fac 還能"越界"地讀到參數 n)
- 上邊是 返回地址、舊的 ebp 指針(指向 main 幀)
- fac 沒有局部變量,又浪費了很多字節
- 參數佔了最下邊的 4 字節(需要遞歸時使用)
這時還不滿足遞歸終止條件,於是fac(3)又遞歸地調用了fac(2),
fac(2)又遞歸的調用了fac(1),到這個時候棧變成了如下情況:
上圖的箭頭的含義很明顯: 從 ebp 可回溯到所有的函數幀, 這是由於每個函數開頭都來兩條 pushl %ebp、movl %esp, %ebp造成的。
參數總是調用者寫入,被調用者來讀取(被調用者修改參數毫無意義), 這是一種默契^_^。
程序繼續運行:
- fac(1) 滿足了遞歸終止條件,fac(1) 返回 1,fac(1)#3 幀消亡
- 繼續執行 fac(2),fac(2) 返回 1*2,fac(2)#2 幀消亡
- 繼續執行 fac(3),fac(3) 返回 2*3,fac(1)#1 幀消亡
- 繼續執行 main,printf 結果,返回 0,main 幀消亡
- 繼續執行 ???(且聽下回分解)
最終程序結束(進程僵死,一會兒後操作系統會來收屍 (回收內存及其他資源))。
小結
函數幀保存的是函數的一個完整的局部環境, 保證了函數調用的正確返回(函數幀中有返回地址)、 返回後繼續正確地執行,因此函數幀是 C語言 能調來調去的保障。
主要的調用慣例
調用慣例 | 出棧方 | 參數傳遞 | 名字修飾 |
---|---|---|---|
cdecl | 函數調用方 | 從右至左的順序壓參數入棧 | 下劃線+函數名 |
stdcall | 函數本身 | 從右至左的順序壓參數入棧 | 下劃線+函數名+@+參數的字節數, 如函數 int func(int a, double b)的修飾名是 _func@12 |
fastcall | 函數本身 | 頭兩個 DWORD(4字節)類型或者更少字節的參數 被放入寄存器,其他剩下的參數按從右至左的順序入棧 | @+函數名+@+參數的字節數 |
pascal | 函數本身 | 從左至右的順序入棧 | 較為複雜,參見pascal文檔 |