我们通过一段简单的代码来探究一下函数的调用过程:
#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 c = 0;
c = add(a, b);
printf("c = %d\n", c);
return 0;
}
我们通过查看它的【调用堆栈】看看在main函数的调用:
我们可以发现main函数是在mainCRTStartup被调用的
每一次函数调用都是一个过程,这个过程通常称之为函数的调用过程。
这个过程要为函数开辟栈空间,用于本次函数的调用过程中的临时变量的保存、现场保护,这块栈空间被称之为函数栈帧
栈帧的维护中我们要了解两个寄存器ebp和esp
ebp存放了指向函数栈帧栈底的地址
esp存放了指向函数栈帧栈顶的地址
我们研究函数调用的过程,必须得对应汇编代码:
int main()
{
00D21800 push ebp //push 将 ebp 压栈处理
00D21801 mov ebp,esp //使esp的值赋给ebp,产生新的ebp
00D21803 sub esp,0E4h //给esp减去一个16进制数字,产生新的esp
00D21809 push ebx
00D2180A push esi
00D2180B push edi
00D2180C lea edi,[ebp+FFFFFF1Ch] //lea(load effective address)加载有效地址
00D21812 mov ecx,39h
00D21817 mov eax,0CCCCCCCCh
00D2181C rep stos dword ptr es:[edi] //从edi位置开始连续存储ecx次,存储内容为eax
int a = 10;
00D2181E mov dword ptr [ebp-8],0Ah
int b = 20;
00D21825 mov dword ptr [ebp-14h],14h
int c = 0;
00D2182C mov dword ptr [ebp-20h],0
我们可以在内存中看到他的局部变量的创建:
当不给局部变量值的时候,会产生一个随机值
接下来是add 函数的调用,参数传递过程:
在这里我们要简单了解一下函数调用约定(这个代码先传的参数是b,然后在传a):
函数调用约定,是指当一个函数被调用时,函数的参数会被传递给被调用的函数和返回值会被返回给调用函数。函数的调用约定就是描述参数是怎么传递和由谁平衡堆栈的,当然还有返回值。
参数传递顺序
1.从右到左依次入栈:__stdcall,__cdecl,__thiscall,__fastcall
2.从左到右依次入栈:__pascal
c = add(a, b);
00D21833 mov eax,dword ptr [ebp-14h] //[ebp-14h]就是b,将b给eax
00D21836 push eax //将eax压栈
00D21837 mov ecx,dword ptr [ebp-8]
00D2183A push ecx
00D2183B call 00D2111D //call 调用,先要压栈call指令下一条指令的地址,然后跳转到add函数的地方
00D21840 add esp,8
00D21843 mov dword ptr [ebp-20h],eax
跳到add函数
按F11(VS环境)进入函数内部:
int add(int x, int y)
{
00D217B0 push ebp
00D217B1 mov ebp,esp
00D217B3 sub esp,0CCh
00D217B9 push ebx
00D217BA push esi
00D217BB push edi
00D217BC lea edi,[ebp+FFFFFF34h]
00D217C2 mov ecx,33h
00D217C7 mov eax,0CCCCCCCCh
00D217CC rep stos dword ptr es:[edi]
int z = 0;
00D217CE mov dword ptr [ebp-8],0
z = x + y;
00D217D5 mov eax,dword ptr [ebp+8] //[ebp+8] 获取形参a的值
00D217D8 add eax,dword ptr [ebp+0Ch] //[ebp+0Ch] 获取形参b的值,然后将a,b相加之后放到eax中
00D217DB mov dword ptr [ebp-8],eax //将eax放到[ebp-8]的位置,也就是z的位置
return z;
00D217DE mov eax,dword ptr [ebp-8] //将结果存在eax寄存器中,通过寄存器带回函数的返回值
}
int z = 0;
z = x + y;
函数的返回部分:
00D217E1 pop edi //pop出栈
00D217E2 pop esi
00D217E3 pop ebx
00D217E4 mov esp,ebp //将ebp赋给esp
00D217E6 pop ebp //出栈,将出栈的内容保存到ebp回到main函数的栈帧
00D217E7 ret //ret 指令会使得出栈一次,并将出栈的内容当做地址。将程序执行跳转到该地之处
mov esp,ebp
pop ebp 将出栈的内容当做地址 (00D21840),之后回到main函数内部
执行完ret 之后:
00D21840 add esp,8
00D21843 mov dword ptr [ebp-20h],eax
printf("c = %d\n", c);
00D21846 mov eax,dword ptr [ebp-20h]
printf("c = %d\n", c);
00D21849 push eax
00D2184A push 0D27B30h
00D2184F call 00D21339
00D21854 add esp,8
return 0;
00D21857 xor eax,eax
}
00D21859 pop edi
00D2185A pop esi
00D2185B pop ebx
00D2185C add esp,0E4h
00D21862 cmp ebp,esp
00D21864 call 00D21127
00D21869 mov esp,ebp
00D2186B pop ebp
00D2186C ret
add esp,8
mov dword ptr [ebp-20h],eax
将eax寄存器里面的值赋给[ebp-20h],也就是c,然后输出。
在这里要注意:
栈帧这部分内容在不同的编译器上实现存在差异,但是思想是一致的。
这里我用的是vs2017,如果在vc上面它里面的局部变量是连续存放的,而在VS中,有可能中间还隔了几个地址。
我将每个函数栈帧图片放出来,供大家参考: