函数调用过程

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Tian_Luo_Girl/article/details/78938177

每一个未运行完的函数都对应着一个栈帧,系统为单个函数分配的那部分栈空间就叫做栈帧,栈帧保存了函数的信息。
以下面的代码为例,通过汇编代码的运行过程介绍栈帧创建和销毁的过程

#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 = Add(a, b); 
 printf("c = %d\n", c); 
 return 0;
} 

从main函数创建自己的栈帧开始(其他内容先忽略),初始状态:
这里写图片描述
ebp和esp所表示的这一段内存空间叫栈帧,也就是粉色框框区域

10:    int a = 10;
00401078   mov         dword ptr [ebp-4],0Ah
11:    int b = 20;
0040107F   mov         dword ptr [ebp-8],14h
12:    int c = 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
13:    printf("c = %d\n", c);

mov dword ptr [ebp-4],0Ah
a的值是10,也就是0Ah,将a赋值到ebp所指向空间的下一个空间,dword ptr指明大小是双字(4个字节,也就是int的大小)
mov dword ptr [ebp-8],14h
同理把b的值放到ebp的下下个空间
mov eax,dword ptr [ebp-8]
把b赋值给eax
push eax
eax入栈,即刚刚的b入栈
push有两个步骤:
(esp)=(esp)-4;
(esp)=(eax);
所以esp向下移动,b是栈顶第一个元素
mov ecx,dword ptr [ebp-4]
push ecx

同理a入栈,现在a成了栈顶元素

call @ILT+0(_Add) (00401005)
@ILT+0(_Add) (00401005)是一个标号,这里标记着Add函数的入口

call word ptr 内存单元地址
相当于进行:
push eip
jmp word ptr 内存单元地址

即先将当前指令的下一条指令的地址入栈,再跳转至指定内存单元

执行这一步就进入Add函数内部了

3:    {
00401020   push        ebp
00401021   mov         ebp,esp
00401023   sub         esp,44h
00401026   push        ebx
00401027   push        esi
00401028   push        edi
00401029   lea         edi,[ebp-44h]
0040102C   mov         ecx,11h
00401031   mov         eax,0CCCCCCCCh
00401036   rep stos    dword ptr [edi]
4:     int z = 0;
00401038   mov         dword ptr [ebp-4],0
5:     z = x + y;
0040103F   mov         eax,dword ptr [ebp+8]
00401042   add         eax,dword ptr [ebp+0Ch]
00401045   mov         dword ptr [ebp-4],eax
6:     return z;
00401048   mov         eax,dword ptr [ebp-4]
7:    }
0040104B   pop         edi
0040104C   pop         esi
0040104D   pop         ebx
0040104E   mov         esp,ebp
00401050   pop         ebp
00401051   ret

push ebp
第一步先将main函数的ebp入栈,保存调用函数的栈底指针
mov ebp,esp
sub esp,44h

将esp赋值给ebp(也就是说被调用函数的栈底指针其实是调用函数的栈顶指针),这里修改了栈底指针ebp
然后esp=esp-44h,44h即为Add函数栈帧的大小,esp向下移动了44h个字节,现在Add函数的栈帧形成了
mov dword ptr [ebp-4],0
相当于在ebp下面开辟了一个新的空间,并赋值为0
mov eax,dword ptr [ebp+8]
add eax,dword ptr [ebp+0Ch]
mov dword ptr [ebp-4],eax

将a+b赋值给刚刚定义的z
现在地址空间结构如下:
这里写图片描述
mov eax,dword ptr [ebp-4]
接着就要将返回值返回了,先将返回值赋值给eax
mov esp,ebp
pop ebp
ret

将ebp赋值给esp,然后弹出栈顶元素赋值给ebp

ebp = main的ebp;
esp = esp+4;
ret
ret其实就是 pop eip
弹出栈顶元素赋值给eip,现在的栈顶元素是 call压入的返回地址,同时esp上移
现在的地址空间结构如下:
这里写图片描述
然后回到了main函数
add esp,8
esp向上移动两个int型大小,释放掉了函数参数x和y
mov dword ptr [ebp-0Ch],eax
ebp-0Ch是b再往下的一块空间,即此时定义了c变量,并同时把Add函数的返回值赋值给c
这里写图片描述
现在main函数的栈帧恢复到刚开始的状态,开始执行后面的代码
函数Add的调用过程结束了,它所使用的空间也全部释放掉了

在查阅资料的过程中,我发现对于栈帧大家都有不同的理解,我也没能找到最标准的定义,有人认为函数的栈帧是从形参的内存分配开始,也有人认为栈帧是从ebp的指向开始
我比较认可第二种,所以我觉得函数的参数是不在函数自己的栈帧中的,而是在调用这个函数的函数中。

归纳起来,创建:
1.形参实体化
2.返回地址入栈,进入函数
3.保存栈底指针
4.修改栈底指针和栈顶指针,形成新栈帧
5.初始化局部变量

。。。。过程。。。。

销毁:
1.修改esp指向和ebp同一位置
2.修改ebp为原先保存的ebp
3.pop eip 准备回到记录的返回地址
4.恢复esp

猜你喜欢

转载自blog.csdn.net/Tian_Luo_Girl/article/details/78938177