C語言函式呼叫底層機制?

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得到返回值,函式呼叫完全結束,可以分析此時棧的情況和呼叫前完全一樣。

函式呼叫的具體實現還有些細節的東西,但是大體的流程是相似的。可見函式呼叫其實要額外增加很多指令,但是可以減少指令空間。

注意事項

函式呼叫增加額外指令,最好不要迴圈呼叫函式,而將迴圈放到函式內部,減少額外指令。

不同的呼叫方式,引數的傳遞順序及返回時棧的恢復物件不同,但大致是相似。

相關問題答案