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