- 说起函数调用,我们可能很快就想到:程序从main函数走起,遇到调用函数的语句,就跳转到此函数所在的语句块执行此函数,执行完之后再返回main函数继续执行程序。但是这只是笼统的描述,其实在函数内部,函数调用要经过一系列的复杂的过程,下面为大家一一详细叙述。
- 说到函数调用,我们不可避免的要说到栈帧的创建和销毁。函数调用过程要为函数开辟空间,用于本次函数的调⽤用中临时变量的保存、现场保护。这块栈空间,我们称之为函数栈帧。首先应该明白,栈是从高地址向低地址延伸的。而说到函数栈帧,我们不可避免的要说到两个寄存器:寄存器ebp和寄存器esp。寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(低地址)。这两个寄存器用于维护函数栈帧。
- 介绍完基本概念,我们使用一个具体的例子来看看函数的调用过程及栈帧的创建和销毁具体是怎么实现的。首先看代码:
#include<stdio.h>
#include<stdlib.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("ret = %d\n", ret);
system("pause");
return 0;
}
这个代码时main函数调用了一个简单的Add函数,但足以说明问题,下面我们来分析一下。首先看下它的汇编代码
00401060 push ebp
00401061 mov ebp,esp
00401063 sub esp,4Ch
00401066 push ebx
00401067 push esi
00401068 push edi
00401069 lea edi,[ebp-4Ch]
0040106C mov ecx,13h
00401071 mov eax,0CCCCCCCCh
00401076 rep stos dword ptr [edi]
12: int a = 10;
00401078 mov dword ptr [ebp-4],0Ah
13: int b = 20;
0040107F mov dword ptr [ebp-8],14h
14: int ret = Add(a, b);
00401086 mov eax,dword ptr [ebp-8]
00401089 push eax
0040108A mov ecx,dword ptr [ebp-4]
0040108D push ecx
0040108E call @ILT+0(_Add) (00401005)
00401093 add esp,8
00401096 mov dword ptr [ebp-0Ch],eax
15: printf("ret = %d\n", ret);
00401099 mov edx,dword ptr [ebp-0Ch]
0040109C push edx
0040109D push offset string "ret = %d\n" (00424024)
004010A2 call printf (00401200)
004010A7 add esp,8
16: system("pause");
004010AA push offset string "pause" (0042401c)
004010AF call system (004010f0)
004010B4 add esp,4
17: return 0;
004010B7 xor eax,eax
18: }
004010B9 pop edi
004010BA pop esi
004010BB pop ebx
004010BC add esp,4Ch
004010BF cmp ebp,esp
004010C1 call __chkesp (00401280)
004010C6 mov esp,ebp
004010C8 pop ebp
004010C9 ret
我们来文字具体分析一下它的步骤
一、main函数执行之前首先要执行mainCRTStart函数,为其开辟一块内存空间。
二、执行main函数
- push ebp 将ebp压入栈
- 将esp移向栈顶
- mov ebp esp 将esp赋给ebp,即将ebp移到esp所指向的位置,这时esp,ebp指向同一块内存空间。
- esp向上移动,为main函数开辟空间,此时esp和ebp维护一块新的空间,即main函数的空间。
- 向main函数的栈帧里压入ebx,esi,edi三个寄存器,同时esp移向栈顶。
- 为main函数开辟的空间进行初始化。
- 将栈底指针减4,向上移动4个字节,为变量a开辟空间。即创建变量a,并将a的值放入内存。
- 将栈底指针再向上移动4个字节,为变量b开辟空间。即创建变量b, 并将b的值放入内存。
- 将栈底指针再向上移动4个字节,为变量ret开辟空间,即创建变量ret,并将ret的值放入内存。
- 将ebp-8,即将变量b的值放入eax寄存器,这一块空间即是实参b的临时拷贝。之后将esp移向栈顶。
- 将ebp-4,即将变量a的值放入eax寄存器,这一块空间即是实参a的临时拷贝。之后将esp移向栈顶。
三、调用Add函数
1.push ebp 将ebp压入栈中
2.将esp移向栈顶
3.mov ebp,esp.将esp赋给ebp,即将ebp移动到esp所指向空间。此时esp和ebp指向同一块空间。
4.esp向上移动,为Add函数欲开辟空间,此时esp和ebp维护一块新的空间,即Add函数的空间。
5.为Add函数开辟的空间进行初始化。
6.将栈底指针ebp-4,即栈底向上移动4个字节,为变量z开辟空间,创建变量z.
7.将栈底指针ebp+8,即ebp向下移动8个字节,此时ebp指向exa寄存器,即刚才所存放的变量a的值的空间。
8.将栈底指针ebp+12,即ebp向下移动12个字节,此时ebp指向exa寄存器即刚才所存放的变量b的值的空间。
9.将形参x和形参y进行相加。
10.将相加的结果放入exa寄存器。
11.将寄存器edi,esi,ebx弹出栈,esp向下移动。
12.将ebp赋给esp,弹出ebp,esp与ebp回到原来维护main函数的空间。
Add函数栈帧被销毁。
13.返回main函数。
14.esp+8,销毁刚才创建的存放变量a和变量b的值的空间,即销毁刚才创建的形参。
15.将刚才存放结果的寄存器放入变量ret所指向的空间,即将函数所返回的结果放入变量ret的空间。
至此,Add函数调用过程即相关函数栈帧的创建于销毁完成。
下面是函数调用过程的简单步骤图