c語言函式呼叫,引數的傳遞是基於棧來實現的。但是,函式的呼叫的具體實現的步驟又是怎樣呢?接下來將深入到c語言函式的呼叫及返回的流程,由於函式呼叫方式不同,具體實現略有差異,所以我們以__cdecl 呼叫方式來做分析。
工具/原料
windows 7
c語言
VC++6.0
方法/步驟
先簡單的看個例子,如下所示:
#include "stdafx.h"
int add(int a, int b)
{
return a + b;
}
int main(int argc, char* argv[])
{
int a, b, c;
a = 10;
b = 20;
c = add(a, b);
return 0;
}
直接轉到以上程式碼的彙編:
13: int a, b, c;
14: a = 10;
00401068 mov dword ptr [ebp-4],0Ah
15: b = 20;
0040106F mov dword ptr [ebp-8],14h
16: c = add(a, b);
00401076 mov eax,dword ptr [ebp-8]
00401079 push eax
0040107A mov ecx,dword ptr [ebp-4]
0040107D push ecx
0040107E call @ILT+0(add) (00401005)
00401083 add esp,8
00401086 mov dword ptr [ebp-0Ch],eax
17: return 0;
00401089 xor eax,eax
其中00401076 00401079 0040107A 0040107D是通過暫存器向棧中壓入引數1,2;0040107Ecall指令,在執行這條指令同時會將call下面的指令地址壓入棧,然後在跳轉到add函式中,這很好理解保證函式呼叫返回能正常執行接下來的程式碼。
由於在2中跳轉到了add函式,所以開始分析add的彙編:
6: int add(int a, int b)
7: {
00401020 push ebp
00401021 mov ebp,esp
00401023 sub esp,40h
00401026 push ebx
00401027 push esi
00401028 push edi
00401029 lea edi,[ebp-40h]
0040102C mov ecx,10h
00401031 mov eax,0CCCCCCCCh
00401036 rep stos dword ptr [edi]
8: return a + b;
00401038 mov eax,dword ptr [ebp+8]
0040103B add eax,dword ptr [ebp+0Ch]
9: }
0040103E pop edi
0040103F pop esi
00401040 pop ebx
00401041 mov esp,ebp
00401043 pop ebp
00401044 ret
在add函式中,00401020指令的作用呢?ebp擴充套件基址指標暫存器(extended base pointer) 其記憶體放一個指標,該指標指向系統棧最上面一個棧幀的底部,即儲存呼叫函式者的基址指標,也是為函式返回時恢復工作做準備。00401021指令即得到當前棧幀值,方便函式呼叫棧中的區域性變數,00401023-00401028為區域性變數分配空間並且儲存函式呼叫前暫存器的值,當然這和儲存ebp的作用是一樣的,在函式返回前肯定是要恢復的。
8: return a + b;
00401038 mov eax,dword ptr [ebp+8]
0040103B add eax,dword ptr [ebp+0Ch]
9: }
0040103E pop edi
0040103F pop esi
00401040 pop ebx
00401041 mov esp,ebp
00401043 pop ebp
00401044 ret
我們繼續分析,在做好了暫存器的備份及變數空間的分配,我們可以執行add中的程式碼了,00401038-0040103B進行a+b指令,並將結果儲存在eax中,所以在函式返回後,我們可以通過exa暫存器得到返回值。在return時我們將進行很多工作,如暫存器的恢復0040103E-00401040,00401041釋放區域性變數,00401043恢復棧幀地址,此時棧中的情況如下:
地地址----------高地址
函式返回地址 引數1 引數2
所以ret就是將指令轉移到“函式返回地址”即我們回到了main中call指令下面那條指令。
接下來分析main函式
0040107E call @ILT+0(add) (00401005)
00401083 add esp,8
00401086 mov dword ptr [ebp-0Ch],eax
函式卻是返回了,但是此時棧的情況如何了
地地址----------高地址
引數1 引數2
引數1和引數2還沒有出棧,看指令00401083,進行這條指令後將有什麼後果了,即棧頂地址增加了8,也不就是相當於引數1和引數2出棧了。這樣出棧的方法還是很有效率的。最後00401086得到返回值,函式呼叫完全結束,可以分析此時棧的情況和呼叫前完全一樣。
函式呼叫的具體實現還有些細節的東西,但是大體的流程是相似的。可見函式呼叫其實要額外增加很多指令,但是可以減少指令空間。
注意事項
函式呼叫增加額外指令,最好不要迴圈呼叫函式,而將迴圈放到函式內部,減少額外指令。
不同的呼叫方式,引數的傳遞順序及返回時棧的恢復物件不同,但大致是相似。