某个函数运行时,机器需要分配一定内存去进行函数内的各种操作,这个过程包括将数据和控制从代码的一部分传递到另一部分,分配的这部分就是栈帧,栈帧其实就是是一段有界限的内存空间,它为函数局部变量分配空间,退出该函数时释放这些空间,回到调用它的地方执行下一条指令。
栈的作用:传递参数、局部变量分配、保存调用的返回地址、保存寄存器以供恢复上一栈帧
了解程序在内存中分布的都知道,栈是从高地址向低地址延伸的,每个函数的调用,都有它自己独立的一个栈帧
栈帧主要是有下面2个指针工作
ebp:帧指针(保存了最高地址)
esp:栈指针(保存了最低地址)
另外列举一些用到了的寄存器及定义
寄存器 | |
eax | 累加(Accumulator)寄存器,常用于函数返回值 |
ecx | 计数器(Counter)寄存器,常用作字符串和循环操作中的计数器 |
edi | 目的变址寄存器 |
ebx | 基址(Base)寄存器,以它为基址访问内存 |
当程序执行时,栈指针(栈顶)可以移动,因此大多数信息的访问都是相对于帧指针的。
栈帧空间如下,这里顶部为高地址
下面我们根据一个实际的例子使用vs2013执行来说明栈帧在函数执行过程中是如何变化的……
#include <stdio.h>
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 1;
int b = 2;
int c = Add(a, b);
printf("%d\n", c);
return 0;
}
首先我们反汇编一下,得到下面的汇编代码
1、main函数栈帧的创建
int main()
{
01361410 push ebp #1 旧ebp压入栈
01361411 mov ebp,esp # 新函数esp指向ebp,栈顶变栈底(形成新栈)ebp = 0x0073fd94
01361413 sub esp,0E4h #2 esp往低地址移动0xe4,开辟空间
01361419 push ebx # 压栈
0136141A push esi # 压栈
0136141B push edi #3 压栈 此时 esp = 0x0073fca4
0136141C lea edi,[ebp-0E4h] #4 下4句为把ebp开始的长度为ox39大小内存初始化0xCCCCCCCC
01361422 mov ecx,39h
01361427 mov eax,0CCCCCCCCh
0136142C rep stos dword ptr es:[edi] # 重复拷贝ecx次,为0x39
int a = 1;
0136142E mov dword ptr [a],1 # 赋值1到ebp前16个字节
int b = 2;
01361435 mov dword ptr [b],2 # 赋值1到ebp前16个字节
int c = Add(a, b);
0136143C mov eax,dword ptr [b] # b放到exa中
0136143F push eax # 压栈
01361440 mov ecx,dword ptr [a] #5 a放到exa中
01361443 push ecx # 压栈
01361444 call _Add (013610E1h) # call指令调用,先要压栈call下一句指令,即01361449
01361449 add esp,8
对应堆栈如下图(底部为高地址):
2、add函数栈帧的创建
int Add(int x, int y)
{
013913C0 push ebp # main ebp压栈 此时esp=0x0073fc94
013913C1 mov ebp,esp
013913C3 sub esp,0CCh
013913C9 push ebx
013913CA push esi
013913CB push edi #此时esp=0x0073fbbc,ebp=0x0073fc94
013913CC lea edi,[ebp-0CCh]
013913D2 mov ecx,33h
013913D7 mov eax,0CCCCCCCCh
013913DC rep stos dword ptr es:[edi] # 都和main差不多的操作
int z = 0;
013913DE mov dword ptr [z],0 #0赋值到z中,即为ebp前16个字节中低8位赋值0
z = x + y;
013913E5 mov eax,dword ptr [x]
013913E8 add eax,dword ptr [y] # 获取形参a、b的值,相加
013913EB mov dword ptr [z],eax # 保存在c中
return z;
013913EE mov eax,dword ptr [z] #结果保存在eax中,通过寄存器带回
}
013913F1 pop edi #出栈
013913F2 pop esi #出栈
013913F3 pop ebx #出栈
013913F4 mov esp,ebp #ebp赋值给esp
013913F6 pop ebp # 出栈,回到main栈帧
013913F7 ret
对应栈帧变化如下,其实和main差不多(底部为高地址):
3、add函数返回main
01391444 call _Add (013910E1h)
01391449 add esp,8 #add返回后到这句指令,esp向下移动2位,就到push edi那句指令的栈帧了
0139144C mov dword ptr [c],eax # 下面调用其他也和add类似了
printf("%d\n", c);
0139144F mov esi,esp
01391451 mov eax,dword ptr [c]
printf("%d\n", c);
01391454 push eax
01391455 push 1395858h
0139145A call dword ptr ds:[1399114h]
01391460 add esp,8
01391463 cmp esi,esp
01391465 call __RTC_CheckEsp (0139113Bh)
return 0;
0139146A xor eax,eax
}
这样,main函数调用子函数栈帧的生命周期就算结束了,函数的调用就算来来回回操作。
整体栈如下: