参考
背景
栈帧
栈帧本质是一种栈,只是专门用于保存函数调用过程中的参数、返回地址等信息,逻辑上说,每个函数都有自己的栈帧,栈帧是一个函数执行的环境。
寄存器 ebp 内存放栈帧基址,寄存器 esp 内存放栈指针,二者之间的区域称为栈帧。
调用
将主调函数(当前函数)称为 Caller,被调用的函数称为 Callee。
调用时,需要知道:
- Caller 需要知道 Callee 返回值存在哪里
- Callee 需要知道Caller传入的参数存在哪里
- Callee 执行后,返回地址在哪里
- 返回后,ebp、esp寄存器值应该恢复到调用前的状态
实例
代码
#include <stdio.h>
int Sub(int x,int y)
{
int t = 0;
t = x-y;
return t;
}
int main()
{
int a = 10;
int b = 20;
int c = 0;
c = Sub(a,b);
return 0;
}
对应汇编如下
int main()
{
003113F0 push ebp
003113F1 mov ebp,esp
003113F3 sub esp,0E4h
003113F9 push ebx
003113FA push esi
003113FB push edi
003113FC lea edi,[ebp-0E4h]
00311402 mov ecx,39h
00311407 mov eax,0CCCCCCCCh
0031140C rep stos dword ptr es:[edi]
int a = 10;
0031140E mov dword ptr [a],0Ah
int b = 20;
00311415 mov dword ptr [b],14h
int c = 0;
0031141C mov dword ptr [c],0
c = Sub(a,b);
00311423 mov eax,dword ptr [b]
00311426 push eax
00311427 mov ecx,dword ptr [a]
0031142A push ecx
0031142B call Sub (31108Ch)
00311430 add esp,8
00311433 mov dword ptr [c],eax
return 0;
00311436 xor eax,eax
}
00311438 pop edi
00311439 pop esi
0031143A pop ebx
0031143B add esp,0E4h
00311441 cmp ebp,esp
00311443 call @ILT+315(__RTC_CheckEsp) (311140h)
00311448 mov esp,ebp
0031144A pop ebp
0031144B ret
执行
main函数也是被 _mainCRTStartup 调用的函数,开辟main的栈帧过程如下:
- ebp esp各自有初始值。
- ebp值压栈,压到esp的位置,然后esp值赋给ebp。
- esp值增加,开辟空间
开辟后,3个push压栈了三个寄存器的值,esp 向低地址方向增长3,
,然后用 edi 保存了esp在3个push前的位置, 即初始位置。然后初始化开始时开辟的空间。
初始化局部变量a b c,从ebp的位置往低地址入栈保存,即保存到main函数的栈帧中。
调用sub函数:
- 形参从右到左入栈:拷贝b和a的值,push到esp的位置,esp增长2。
- call 指令将 main函数中本应该执行的下一条指令的位置压栈,然后跳转执行函数Sub。
- 开辟栈帧,同上。
计算:
- 知道参数有2个,所以ebp+12字节的位置存放第一个参数b,ebp+8字节的位置存放第二个参数a。
- 作运算,结果存到eax寄存器。
调用结束后返回:
- 先进行三次出栈,将栈顶值还原到对应edi, esi, ebx寄存器。
- 移动esp到ebp位置,然后pop ebp,就是取出esp位置的值赋到ebp,值就是之前main函数的ebp,这样ebp就还原到main的栈帧了。
- ret指令pop了call时压栈的指令位置,程序计数器从此处开始继续执行。
- esp+8,释放形参a, b。
- main函数从eax中取出结果,保存到变量c的位置,即接收返回值。