函数栈帧的创建与销毁

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。

以上是关于函数栈帧的创建与销毁的主要内容,如果未能解决你的问题,请参考以下文章

图解函数栈帧 - 函数的创建与销毁

函数栈帧的创建和销毁

函数栈帧的创建和销毁

函数栈帧的创建与销毁

函数栈帧的创建与销毁

函数栈帧的创建与销毁,带你了解代码底层原理