C语言函数调用底层机制
的有关信息介绍如下:c语言函数调用,参数的传递是基于栈来实现的。但是,函数的调用的具体实现的步骤又是怎样呢?接下来将深入到c语言函数的调用及返回的流程,由于函数调用方式不同,具体实现略有差异,所以我们以__cdecl 调用方式来做分析。
先简单的看个例子,如下所示:
#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得到返回值,函数调用完全结束,可以分析此时栈的情况和调用前完全一样。
函数调用的具体实现还有些细节的东西,但是大体的流程是相似的。可见函数调用其实要额外增加很多指令,但是可以减少指令空间。