函数的栈帧
定义
在函数的调用过程中为函数开辟的栈空间用于本次函数的调用中临时变量的保存,现场保护。这块栈空间被称为栈帧。
创建函数
为了了解函数调用栈空间的详细过程,我们先写一段代码,通过代码的反汇编来观察函数的调用过程中汇编指令都做了什么。首先,写入下面的函数:
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int ret = 0;
Add(a, b);
ret = Add(a, b);
printf("%d ",ret);
return 0;
可以看出上面的代码中主要包含了一个main函数和一个Add函数,在main函数中调用了Add函数。所以在申请栈空间的先后顺序上来说应该是先为main函数申请再为Add函数申请。
并将该代码转为反汇编指令来观察:
main函数部分反汇编内容
00401060 push ebp //用ebp来存放栈底的地址,方便后面恢复
00401061 mov ebp,esp //用esp和ebp两个寄存器来维护main函数的空间
00401063 sub esp,4Ch //给esp加上4ch后为main函数开辟空间
00401066 push ebx
00401067 push esi
00401068 push edi //把ebx,esi.edi三个数压栈,esp的位置向栈顶的方向移动
00401069 lea edi,[ebp-4Ch]
0040106C mov ecx,13h
00401071 mov eax,0CCCCCCCCh
00401076 rep stos dword ptr [edi] //对main函数的内部空间初始化为0xCCCCCCCC
11: int a = 10;
00401078 mov dword ptr [ebp-4],0Ah
12: int b = 20;
0040107F mov dword ptr [ebp-8],14h
13: int ret = 0;
00401086 mov dword ptr [ebp-0Ch],0 //创建a,b,c三个局部变量
14: Add(a, b);
0040108D mov eax,dword ptr [ebp-8]
00401090 push eax //把b的值传给eax,生成形参y并压栈
00401091 mov ecx,dword ptr [ebp-4]
00401094 push ecx //把a的值传给ecx,生成形参x并压栈
00401094 push ecx
00401095 call @ILT+0(_Add) (00401005) //调用函数,把call指令的下一条指令的地址存入栈顶,以便后面返回到函数
0040109A add esp,8
以上为main函数的创建栈空间和创建变量以及调用Add函数的过程,每个函数都要在栈上开辟一块空间由esp和ebp这两个寄存器来维护。 下面按步骤来分析这个过程中具体汇编语言都需要做哪些事情:
(1)把ebp的地址存放起来
(2)esp指向main函数空间的顶部。
(3)把ebp指向esp,然后esp-4ch,也就是为main函数开辟了4ch个字节的空间。
(4)在main函数中放入ebx,esi,edi三个数据,esp继续向上移动3个数据。
(5)对main函数内部数据进行初始化,在VC6.0上初始化为0xCCCCCCCC。
(6)向内存中申请空间存放三个局部变量。
(7)建立临时变量存放函数的参数,产生形参,与实参数值相同,地址不同。形参的存放顺序为从右到左。
(8)调用Add函数,把call指令下一条指令的地址存入栈顶,方便后面返回。
Add函数的部分反汇编
012C13C0 push ebp //用ebp来存放栈底的地址,方便后面恢复
012C13C1 mov ebp,esp //把esp赋值给ebp
012C13C3 sub esp,0CCh //esp向上移动创建新的函数空间
012C13C9 push ebx
012C13CA push esi
012C13CB push edi
012C13CC lea edi,[ebp-0CCh]
012C13D2 mov ecx,33h
012C13D7 mov eax,0CCCCCCCCh
012C13DC rep stos dword ptr es:[edi] //将Add函数内部全初始化为0xCCCCCCCC
012C13DE mov dword ptr [z],0 //创建局部变量z
012C13E5 mov eax,dword ptr [x] // 形参需要函数通过ebp加上12和8在main函数中去找
012C13E8 add eax,dword ptr [y] //将获取的形参相加
012C13EB mov dword ptr [z],eax
012C13EE mov eax,dword ptr [z] 把相加的结果保存到eax寄存器当中,通过寄存器带回函数的返回值
012C13F1 pop edi
012C13F2 pop esi
012C13F3 pop ebx //让edi,esi,ebx出栈
012C13F4 mov esp,ebp //把ebp的值给esp,即esp下移
012C13F6 pop ebp //把存放在main栈顶的ebp的值赋给ebp寄存器,ebp和esp重新返回main函数空间,z被销毁
012C13F7 ret
对Add函数的反汇编部分做出如下总结
(1)在main函数顶部存入ebp的地址方便函数返回使用
(2)与main函数的创建类似,主要是esp和ebp创建空间,并进行数据初始化和局部变量的创建。
(3)通过ebp的位置在main函数中找到形参,并且使用形参进行运算。
(4)把运算所得的结果存入z内,然后再放入寄存器eax中。
(5)三次出栈后,把ebp赋值给esp,esp下移,再popmain函数顶部存放的ebp之后,ebp和esp重新返回main函数空间。z被销毁。
(6)把a和b形参的结果消除,eax把结果带回给ret。
图解
数据入栈顺序以及esp和ebp的指向位置
main函数跟Add函数在栈空间中的存放大致关系