全网最详细函数栈帧的创建和销毁(超万字含图解)


 电脑中的任何指令都是在CPU上的运行的,但是CPU本身只负责运算不负责存储,数据一般都是存储在内存和寄存器(储存最常用的数据)。
 想要理解函数栈帧的创建和销毁,首先必须了解四个知识点:内存地址布局、寄存器、常用汇编指令及内存模型。

一、基础知识

1.内存地址布局

 首先要了解在我们的内存中地址是由高到低存放的,而且由有数据要占用内存空间时先用高地址再用低地址,我们在操作系统中称这个过程为压栈。每一个函数的调用,都要在内存地址中分配空间,函数调用结束后,函数所占的内存空间会返回给系统。

 栈帧就是一个函数执行的环境:函数参数、函数的局部变量、函数执行完后返回到哪里等等。
首先应该明白,栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(低地址)。

 观察内存布局,建议不要使用太高级的编译器,越高级的编译器越不容易学习与观察汇编过程。

2.寄存器的种类与功能

 寄 存 器 名 称寄 存 器 功 能
eax累加寄存器,相对于其他寄存器,在运算方面比较常用。
ebx基地址寄存器,在内存寻址时存放基地址。
ecx计数寄存器,用于循环操作,比如重复的字符存储操作,或者数字统计。
edx作为EAX的溢出寄存器,总是被用来放整数除法产生的余数。
esi源变址寄存器,主要用于存放存储单元在段内的偏移量。通常在内存操作指令中作为“源地址指针”使用。
edi目的变址寄存器,主要用于存放存储单元在段内的偏移量。
eip控制寄存器,存储CPU下次所执行的指令地址(存放指令偏移地址)
esp栈顶指针,堆栈的顶部是地址小的区域,压入堆栈的数据越多,esp也就越来越小。在32位平台上,esp每次减少4字节。栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。是CPU机制决定的,push、pop指令会自动调整esp的值
ebp基址指针,指栈的栈底指针。基址指针寄存器(extended base pointer),一般与esp配合使用,可以存取某时刻的esp,这个时刻就是进入一个函数内后,CPU会将esp的值赋给ebp,此时就可以通过ebp对栈进行操作,比如获取函数参数,局部变量等。其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部

3.常见汇编指令

    为了方便各位理解,这里都采用小例子来帮助说明。

  • push指令: 函数也是被其他函数所调用的(__tmainCRTstartup()函数),因此需要为main函数开辟内存空间。在32位平台上,esp每次减少4个字节。

       

       说明:因为ebp是栈底指针,push即在esp栈顶指针处放入所要调用的函数的ebp指针。

  • pop指令: 可以理解为出栈操作,即之前压的栈先从低地址处出栈,依次类推。只有在函数调用结束时才有pop指令。

       

       说明:因为edi是最后进行压栈操作的,因此edi最先出栈。

  • sub指令:减操作指令,从寄存器中减去<shifter_operand>表示的数值,并将结果保存到目标寄存器中。

       

       说明:将反汇编中执行sub指令的上一条最终结果的地址减去0E4H的结果保存在esp中。

  • move指令:用于将一个数据从源地址传送到目标地址,源操作地址的内容不变。

    说明:将esp的地址传给ebp,而esp的地址不改变。

  • lea指令:是“load effective address”的缩写,简单的说,lea指令可以用来将一个内存地址直接赋给目的操作数。

       说明:将ebp-24h的地址直接赋给edi。

  • rep指令:重复前缀指令,英文缩写 repeat。能够引发其后字符串指令被重复。

  • stos指令:串存储指令,英文缩写 store string。

       

       说明:通常这四条语句都是连在一起运行。
       rep指令:重复其上面的指令,ecx的值是重复的次数,每执行一次,ecx 减 1,直到 ecx 减至0。
       stos指令:将 eax中的值拷贝到es:[edi]指向的地址。
       dword:一个word代表两个字节,dword即doubleword代表四个字节。
       ptr:pointer缩写 即指针。
       [ ]:[ ]里的数据是一个地址值,这个地址指向一个双字型数据。一次拷贝双字(4个字节)的数据到目的地址。

       es:[edi]指向目的串。

      多种指令合在一起意思是从ebp-24h的地址处向高地址的内存处存放0CCCCCCCCh的内容,重复39h次,每次赋予四个字节的空间。

  • call指令:将程序下一条指令的位置的IP地址压入堆栈中,并转移到调用的子程序中。

       

  • add指令:用于将两个运算子相加,并将执行add指令的上一步最后结果加上多少个字节写入第一个运算子。

       

       说明:上一条最后操作的结果是esp,给 esp 加8,也就是 esp向高地址方向移动 8字节 。

  • ret指令:用于终止当前函数的执行,将运行权交还给上层函数。也就是,当前函数的帧将被回收。

       

       说明:执行这条命令之后,就自动返回刚才call指令的下一行。

       

4、内存模型

在这里插入图片描述


二、演示函数栈帧的创建与销毁

    声明:此次演示的电脑是windows 10、编译环境 为vs2013(debug、Win32)。
    以下代码为演示使用代码:

#include <stdio.h>
int Add(int x, int y)
{
	int z = 0;
	z = x + y;
	return z;
}

int main()
{
	int a = 10;
	int b = 20;
	int c = 0;

	c = Add(a, b);

	printf("%d\n", c);
	return 0;
}

    将光标放在int main下面的大括号处,按下F10,在编译器上方的调试处,选择窗口,再选择调用堆栈。

    继续按F11去运行,当箭头指向return 0处再按F11,我们会跳转到以下画面,并且调用堆栈中出现了调用main函数的函数。这里我们可以证明main函数的确也是被其它函数所调用的,此时main函数已经调用完成即已经出栈,可见栈中已经没有main函数。

在这里插入图片描述
    在此页面往上翻,可以找到调用main函数的函数与调用main函数的函数的函数,这里是嵌套调用,并且能观察出main函数是被__tmainCRTStartup函数调用的,而 __tmainCRTStartup又是被mainCRTStartup调用的。


1.调用main函数的准备

    将光标放在main函数下面的大括号中重新开始调试,先不要按F10,此时在代码页面右击鼠标点击转入反汇编,进入到如下页面,该页面我已截图并且后面加上每一行指令所执行的步骤的具体分析(这只是其中一部分):

在这里插入图片描述

    如果要观察压栈出栈的全过程,我将以画图的形式进行展示。按上面的执行顺序执行。注:将最后四步都归于第七步来分析。

    第一步:说明:因为main函数是被__tmainCRTstartup函数所调用的,因此__tmainCRTstartup函数在栈中的地址肯定是比main函数高的,直线下方为main函数的函数栈帧。(因为画图空间有限,__tmainCRTstartup函数只说明是在main函数下方,我们只要对main函数进行探讨)
    push是压栈操作,放入ebp。

在这里插入图片描述

    第二步:说明:将esp的地址传给ebp。

在这里插入图片描述
    第三步:将ebp的地址减少0E4H传给esp,ebp的地址不改变。

在这里插入图片描述
    注:如果是第一次点入反汇编,可能减去的字节不是0E4h,而是FFFFFF1Ch。这里我们可以鼠标右键,选择“显示符号名”即可。

    第四、五、六步:说明:都是进行进行压栈操作。

在这里插入图片描述

    第七步:说明:因为画图空间有限,内存内容被改变为CCCCCCCC有39h次(16进制),每次四个字节,更改内容的起始位置到末尾改变的内容只画个大概,但始到末准确。

在这里插入图片描述

2.局部变量的创建

    继以上的图,开始创建局部变量,为了避免图过于混乱,先将图中赋值的内容清空。

在这里插入图片描述

    说明:以上带有h的数值都是16进制数字,0Ah转化为10进制为10;14h转化为10进制为20,20h转化为10进制为32。每一个矩形代表4个字节。
    注:此处如果只是显示将值赋给a或b或c,仍是双击鼠标点击“显示符号名”来解决。

在这里插入图片描述

3.调用Add函数的准备

    此时为调用Add函数做准备,我将过程分为三步。

在这里插入图片描述

    第一步:压入eax并赋值。

在这里插入图片描述
    第二步:压入ecx并赋值。

在这里插入图片描述
    第三步:将IP地址压入栈中。

在这里插入图片描述

    注:当执行完第三步后会直接跳转到以下页面,再按一次F11才能进入调用Add函数的反汇编中,当调用完Add函数后才执行第三步下面的执行命令。

在这里插入图片描述

4.为Add函数开辟栈帧

在这里插入图片描述

    第一步:压入ebp。
在这里插入图片描述

    第二步:

在这里插入图片描述
    第三步:压入ebx,esi,edi。

在这里插入图片描述

    第四步:赋值操作。

在这里插入图片描述

    经过上述的详解,我们可见Add函数的调用与main函数的调用的形式大相径庭,必须要对函数调用非常熟悉与理解。

5.在Add()函数中创建变量并运算

在这里插入图片描述
    第一、二、三步:

在这里插入图片描述
    第四、五步:eax与ebp-8的地址都被赋值30。

在这里插入图片描述

6.Add函数栈帧的销毁

在这里插入图片描述

    第一步:出栈操作。

在这里插入图片描述

    第二步:将ebp的地址赋给esp。
在这里插入图片描述
    第三步:ebp出栈。
在这里插入图片描述

7.返回main函数的栈帧

在这里插入图片描述

    第一步:在执行add指令前已经回收了IP地址00B310E1的内容,回收IP地址内容后esp的地址增加了4个字节,执行add指令后esp的地址再增加八个字节,因为编译器原因,实际上esp与edi的地址不相等,我们这里可以不用深度去研究。
在这里插入图片描述
    第二步:eax的值赋给ebp-20h的地址处,而eax经过Add函数的处理现在已经为30。而ebp-20h地址处正好为变量c的地址。
在这里插入图片描述

8.总结

    我们通过例子来观察函数栈帧的创建与销毁,相信你对它已经有了非常深刻的了解。我们学习完后,对以下问题都能够回答吗?
比如:
1.局部变量是怎么创建的?
2.为什么局部变量的值是随机值?
3.函数是怎么传参的?传参的顺序是怎样的?
4.形参和实参是什么关系?
5.函数调用是怎么做的?
6.函数调用是结束后怎么返回的?

如果这篇文章让你对了解函数栈帧的创建与销毁有帮助,麻烦点个赞支持一下谢谢,原创不易,侵权必究!

  • 25
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 21
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zjruiiiiii

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值