以前學彙編,很清楚函數是怎麼調用的,但是久不用之,又忘了~~不知其他人有沒有這種經曆,寫c/c++程式時如果瞭解許多編譯器底層細節,是很爽的;否則,有時會很沮喪。雖然是比較簡單的內容,讓我們也來回憶一下...
我們知道,函數調用最通常的傳遞參數的方式莫過於使用堆棧;函數的局部變數也是在棧上建立。具體怎麼做呢?
假如我們有這麼一個小小的程式:
void Test(int a){
int b;
}
int main(int argc, char* argv[])
{
int a;
Test(a);
return 0;
}
在VC6.0的調試版中反編譯結果如下(加了些注釋):
6: void Test(int a){;函數Tes
00401020 push ebp ;儲存主調函數棧幀指標
00401021 mov ebp,esp ;啟用當前棧幀指標
00401023 sub esp,44h ;為Test留出44h的“私人空間”:44h=4*17=4*11h(聯絡下面)
00401026 push ebx ;儲存寄存器值
00401027 push esi
00401028 push edi
00401029 lea edi,[ebp-44h]
0040102C mov ecx,11h ;11h=17為迴圈次數
00401031 mov eax,0CCCCCCCCh
00401036 rep stos dword ptr [edi] ;迴圈,對“私人空間”填充(int)3
7: int b;
8: }
.......
10: int main(int argc, char* argv[])
11: {
00401050 push ebp;
00401051 mov ebp,esp ;啟用當前棧幀指標
00401053 sub esp,44h ;為main函數留出44h的“私人空間”
00401056 push ebx
00401057 push esi
00401058 push edi
00401059 lea edi,[ebp-44h]
0040105C mov ecx,11h
00401061 mov eax,0CCCCCCCCh
00401066 rep stos dword ptr [edi]
12: int a;//變數定義沒有對應的彙編代碼,編譯器會以EBP-4來表示(int)a
13: Test(a);
00401068 mov eax,dword ptr [ebp-4] ;dword ptr [ebp-4]也即main函數中int a的值
0040106B push eax ;int a 作為參數壓棧
0040106C call @ILT+0(Test) (00401005) ;調用Test函數
00401071 add esp,4 ;因為在call之前壓入了一個參數int a,佔四個位元組,Test函數退出後esp要回退到未壓入參數前的位置。
14: return 0;
00401074 xor eax,eax
15: }
00401076 pop edi
00401077 pop esi
00401078 pop ebx
00401079 add esp,44h
0040107C cmp ebp,esp
0040107E call __chkesp (004010a0)
00401083 mov esp,ebp
00401085 pop ebp
00401086 ret
在堆棧中,每個函數都有一個相關的棧楨(stack frame)來儲存它所有的局部對象和運算式計算過程中用到的臨時對象。通常編譯器使用EBP寄存器來指示當前活動的棧楨。編譯器在編譯時間將所有局部對象解析成相對於棧楨指標(EBP)的固定位移,函數則通過棧楨指標來間接訪問局部對象。
編譯器編譯一個函數時,會在它的開頭添加一些代碼來為其建立並初始化棧楨,這些代碼被稱為序言(prologue);同樣,它也會在函數的結尾處放上代碼來清除棧楨,這些代碼叫做尾聲(epilogue)。
一般情況下,序言是這樣的:
Push EBP ; 把原來的棧楨指標儲存到棧上
Mov EBP, ESP ; 啟用新的棧楨
Sub ESP, 10 ; 減去一個數字,讓ESP指向棧楨的末尾
第一條指令把原來的棧楨指標EBP儲存到棧上;第二條指令通過讓EBP指向主調函數的EBP的儲存位置來啟用被調函數的棧楨;第三條指令把ESP減去了一個數字,這樣ESP就指向了當前棧楨的末尾,而這個數字是函數要用到的所有局部對象和臨時對象的大小。編譯時間,編譯器知道函數的所有局部對象的類型和“體積”,所以,它能很容易的計算出棧楨的大小。
尾聲所做的正好和序言相反,它必須把當前棧楨從棧上清除掉:
Mov ESP, EBP
Pop EBP ; 啟用主調函數的棧楨
Ret ; 返回主調函數
它讓ESP指向主調函數的棧楨指標的儲存位置(也就是被調函數的棧楨指標指向的位置),彈出EBP從而啟用主調函數的棧楨,然後返回主調函數。
一旦CPU遇到返回指令,它就要做以下兩件事:把返回地址從棧中彈出,然後跳轉到那個地址去。返回地址是主調函數執行call指令調用被調函數時自動壓棧的。Call指令執行時,會先把緊隨在它後面的那條指令的地址(被調函數的返回地址)壓入棧中,然後跳轉到被調函數的開始位置。主調函數把被調函數的參數也壓進了堆棧,所以參數也是棧楨的一部分。函數返回後,主調函數需要移除這些參數,它通過把所有參數的總體積加到ESP上來達到目的。
如果有一個函數調用鏈,foo()->bar()->widget();則其堆棧如下: