函数栈帧的创建与销毁
Posted 不倒翁*
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了函数栈帧的创建与销毁相关的知识,希望对你有一定的参考价值。
1.函数栈帧的概念
函数栈帧:使用每一个函数都要在栈区开辟一块空间.栈帧也叫过程活动记录,是编译器用来实现过程/函数调用的 一种数据结构。
对于栈,我们都知道栈是由高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧,每个栈帧中维持着所需要的的各种信息。寄存器ebp指向当前栈帧的底部(高地址),寄存器esp指向当前栈帧的顶部(低地址)。
这样我们就了解了寄存器ebp和寄存器esp中存放的是地址,这两个地址是用来维护函数栈帧的。比如:调用main函数, 我们为main函数分配栈帧空间, 那么栈帧维护如下:
2.函数栈帧的创建
下面我们通过一段代码分析一下,函数栈帧创建和销毁的过程:(栈帧这部分内容在不同的编译器上实现存在差异, 但是思想大致都是一致的。本文是在vs2013编译器下实现的)
#include <stdio.h>
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main(void)
{
int a = 10;
int b = 20;
int ret = 0;
ret = Add(a, b);//计算a+b
printf("%d\\n", ret);
return 0;
}
我们打开调试,选中调用堆栈窗口可以看到,main函数也是被其他函数调用的。
可以看出,main函数是在__tmainCRTStartup函数内部被调用的,而__tmainCRTStartup函数又是在mainCRTStartup函数内部调用的。
为了能更加清楚的看到栈帧创建和销毁的过程,我们转到上面代码对应的反汇编代码:
![int main(void)
{
009D3F40 push ebp //将edp压入栈帧
009D3F41 mov ebp,esp //将esp的值赋给edp
009D3F43 sub esp,0E4h //esp-0E4h
009D3F49 push ebx
009D3F4A push esi
009D3F4B push edi
009D3F4C lea edi,\\[ebp+FFFFFF1Ch\\]
009D3F52 mov ecx,39h
009D3F57 mov eax,0CCCCCCCCh
009D3F5C rep stos dword ptr es:\\[edi\\]]
int a = 10;
009D3F5E mov dword ptr [ebp-8],0Ah
int b = 20;
009D3F65 mov dword ptr [ebp-14h],14h
int ret = 0;
009D3F6C mov dword ptr [ebp-20h],0
ret = Add(a, b);//计算a+b
009D3F73 mov eax,dword ptr [ebp-14h]
009D3F76 push eax
009D3F77 mov ecx,dword ptr [ebp-8]
009D3F7A push ecx
009D3F7B call 009D11F9
009D3F80 add esp,8
009D3F83 mov dword ptr [ebp-20h],eax
printf("%d\\n", ret);
009D3F86 mov esi,esp
009D3F88 mov eax,dword ptr [ebp-20h]
009D3F8B push eax
009D3F8C push 9D5860h
009D3F91 call dword ptr ds:[009D9118h]
009D3F97 add esp,8
009D3F9A cmp esi,esp
009D3F9C call 009D1140
return 0;
009D3FA1 xor eax,eax
}
009D3FA3 pop edi
009D3FA4 pop esi
009D3FA5 pop ebx
009D3FA6 add esp,0E4h
009D3FAC cmp ebp,esp
009D3FAE call 009D1140
009D3FB3 mov esp,ebp
009D3FB5 pop ebp
009D3FB6 ret
2.1 main函数函数栈帧的创建过程
009D3F40 push ebp //将edp压入栈帧
009D3F41 mov ebp,esp //将esp的值赋给edp
009D3F43 sub esp,0E4h //esp-0E4h 将esp向上(低地址方向)移动4个字节
009D3F49 push ebx //接下来三行是将 ebx esi edi 压入栈顶
009D3F4A push esi
009D3F4B push edi
009D3F4C lea edi,\\[ebp+FFFFFF1Ch\\] //然后将main函数的函数栈帧初始化为0cccccccch
009D3F52 mov ecx,39h
009D3F57 mov eax,0CCCCCCCCh
009D3F5C rep stos dword ptr es:\\[edi\\]]
第一步是进行push命令,将ebp压入栈顶 将ebp压入栈顶后,esp的地址也会随之改变。
第二步进行move指令,将esp的值给ebp
第三步进行sub指令,将esp指向的地址减去0E4h(十进制228)
随后分别把ebx,esi,edi压入栈顶
随后四句指令,把从edi(ebp-0E4h)开始的ecx(39h)个 空间改成eax(0CCCCCCCCh)。这么做是为了给main函数栈帧初始化
2.2 main函数中创建变量
int a = 10;
009D3F5E mov dword ptr [ebp-8],0Ah //把0Ah赋值给内存地址为ebp-8中的双字节的空间
int b = 20;
009D3F65 mov dword ptr [ebp-14h],14h //把14h赋值给内存地址为ebp-14h中的双字节的空间
int ret = 0;
009D3F6C mov dword ptr [ebp-20h],0 //把0赋值给内存地址为ebp-20h中的双字节的空间
2.3 Add函数函数栈帧的创建
当abc变量创建好了之后,就要开始调用add函数。
随后分别将ebx(20)和ecx(10)压入栈顶。
这两个指令实际上是在为Add函数传参
ret = Add(a, b);//计算a+b
009D3F73 mov eax,dword ptr [ebp-14h]
009D3F76 push eax
009D3F77 mov ecx,dword ptr [ebp-8]
009D3F7A push ecx
009D3F7B call 009D11F9
009D3F80 add esp,8
随后call指令是调用函数指令,它会把call指令的下一条指令的地址压入栈顶。这样做的目的是等Add函数调用结束后,就会回到call指令的下一条指令继续执行。
进入Add函数后,前面的几条指令跟进入main之前的几条指令一样,是为了给函数准备栈帧和对其进行初始化。
随后在ebp-8的空间创建临时变量z并初始化为0,再通过ebp+8和
ebp+0Ch找到main函数中传递的a,b参数作为形参x,y,相加得到的值赋给eax,再由eax把值赋给z。
2.4 Add函数栈帧的销毁
随后ret指令找到之前已经压入栈的call指令的下一条指令的地址 ,就回到主函数的call指令的下一条指令。
再将esp指向的地址加8,把形参的内存释放。
在通过ebp-20h找到main函数的c变量,然后再把eax的值赋给c,实现了把Add函数计算结果带回到main。
以上是关于函数栈帧的创建与销毁的主要内容,如果未能解决你的问题,请参考以下文章