当我们在学习C语言的时候,写代码一定会用到函数(main函数),但函数在使用过程中是如何调用的,当我们从汇编的角度来剖析函数的调用,会让我们对函数的认识刚深一层。
注:使用的工具是VS2017
下面我将通过一段代码来剖析函数是如何调用的
#include <stdio.h>
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int ret = Add(a, b);
printf("%d\n", ret);
return 0;
}
函数在调用的过程中,是需要为函数来开辟栈空间,用于本次函数的调用过程中临时变量的保存、现场保护,这块空间就被称为函数栈帧。
首先我们来研究一下main函数的汇编代码:
int main()
{
01061810 push ebp
01061811 mov ebp,esp
01061813 sub esp,0E4h
01061819 push ebx
0106181A push esi
0106181B push edi
0106181C lea edi,[ebp-0E4h]
01061822 mov ecx,39h
01061827 mov eax,0CCCCCCCCh
0106182C rep stos dword ptr es:[edi]
int a = 10;
0106182E mov dword ptr [a],0Ah
int b = 20;
01061835 mov dword ptr [b],14h
int ret = Add(a, b);
0106183C mov eax,dword ptr [b]
0106183F push eax
01061840 mov ecx,dword ptr [a]
01061843 push ecx
01061844 call _Add (0106110Eh)
01061849 add esp,8
在还没有调用main函数之前,当_tmianCRTStartup在还没有开始调用main函数之前, esp 和 edp 一起维护同一块空间
开始调用main函数
刚开始的三行代码是为了main函数俩开辟栈帧空间的,其中 esp 存放指向函数栈帧栈顶的地址, ebp 存放指向函数栈帧栈底的地址。
01061810 push ebp
push 表示压栈,从栈顶压入。
01061811 mov ebp,esp
mov 表示移动,表示的就是将 esp 移动到 ebp 的位置
01061813 sub esp,0E4h
sub 表示将esp减去 0E4h(0x000000e4),将 esp 上移,因为在计算机中上面为低地址,下面为高地址。
01061819 push ebx
0106181A push esi
0106181B push edi
这三行 push 就是将 ebx 、 esi 、 edi 先后压入栈顶,并且 esp 将会上移
0106181C lea edi,[ebp-0E4h]
01061822 mov ecx,39h
01061827 mov eax,0CCCCCCCCh
0106182C rep stos dword ptr es:[edi]
这四条语句就是,将 eax 的内容重复 ecx 次,重复是从 edi 所指向的地址开始。
int a = 10;
0106182E mov dword ptr [a],0Ah
表示将10(0Ah)放入a中,放在 ebp - 8 的位置
int b = 20;
01061835 mov dword ptr [b],14h
表示将20(14h)放入b中,放在 ebp - 20 的位置
下面开始调用Add函数
int ret = Add(a, b);
0106183C mov eax,dword ptr [b]
0106183F push eax
01061840 mov ecx,dword ptr [a]
01061843 push ecx
01061844 call _Add (0106110Eh)
01061849 add esp,8
0106184C mov dword ptr [ret],eax
0106183C mov eax,dword ptr [b]
0106183F push eax
01061840 mov ecx,dword ptr [a]
01061843 push ecx
上述四行代码就是先将b压栈,然后再将a压栈,实际上a,b就是传给Add函数的形参
int Add(int x, int y)
{
01061700 push ebp
01061701 mov ebp,esp
01061703 sub esp,0CCh
01061709 push ebx
0106170A push esi
0106170B push edi
0106170C lea edi,[ebp-0CCh]
01061712 mov ecx,33h
01061717 mov eax,0CCCCCCCCh
0106171C rep stos dword ptr es:[edi]
int z = 0;
0106171E mov dword ptr [z],0
z = x + y;
01061725 mov eax,dword ptr [x]
01061728 add eax,dword ptr [y]
0106172B mov dword ptr [z],eax
return z;
0106172E mov eax,dword ptr [z]
}
01061731 pop edi
01061732 pop esi
01061733 pop ebx
01061734 mov esp,ebp
01061736 pop ebp
01061737 ret
前面创建栈帧的过程和main函数一样,其中第一步是先将main函数的 ebp 压栈,目的是为了在返回时能够返回到main函数的栈底。
01061725 mov eax,dword ptr [x]
01061728 add eax,dword ptr [y]
0106172B mov dword ptr [z],eax
return z;
0106172E mov eax,dword ptr [z]
将两数之和放入z中,并最后将z的值放在寄存器eax中
01061731 pop edi
01061732 pop esi
01061733 pop ebx
01061734 mov esp,ebp
01061736 pop ebp
01061737 ret
接下来将会执行 pop 操作,也就是出栈,按照顺序依次将 edi 、 esi 、 ebx 出栈。然后将 ebp 赋值给 esp , ebp 出栈。注: ret 指令会使得出栈一次,并将出栈的内容当做地址,将程序跳转到该地址处。将Add栈帧销毁。
接下来就会从call指令处继续向下执行
01061849 add esp,8
0106184C mov dword ptr [ret],eax
将 esp 向下移,将形参销毁,把 eax放入ret( ebp-20h) 中。
下面就是对main栈帧的销毁,和Add栈帧的销毁一样。