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