函数栈帧的创建和销毁(图解)


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

基础知识介绍

1. 寄存器的种类与功能

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

2. 常用汇编指令

  • push指令:它首先减少esp的值,再将源操作数复制到栈地址,在32位平台上,esp每次减少4字节。
    image-20210810154859463
    解释:首先esp的值减少4字节,再将ebp的值压入栈中。
  • pop指令:它首先把esp指向的栈元素内容复制到一个操作数中,再增加esp的值。在32位平台上,esp每次增加4字节。
    image-20210810183117747
    解释:首先将esp所指地址处的值赋给edi,再将esp的值减少4字节。
  • mov指令:用于将一个数据从源地址传送到目标地址,源操作地址的内容不变。

    解释:将esp 值赋给ebp,这里并不是将esp所指向的内存空间的值赋给 ebp
  • sub指令:减操作指令,从寄存器中减去<shifter_operand>表示的数值,并将结果保存到目标寄存器中。

    解释:esp-0E4h字节的结果保存在esp中。

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

    解释:将ebp-0E4h的值直接赋给edi,而不是把ebp-0E4h内存地址里的数据赋给eax。

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

  • stos指令:串存储指令,英文缩写 store string。
    image-20210810164731501
    解释
    上述几条指令通常一起使用,
    rep指令重复其上面的指令,ecx的值是重复的次数,每执行一次,ecx 减 1,直到 ecx 减至0。
    stos指令eax中的值拷贝到es:[edi]指向的地址。
    dword双字 就是四个字节。
    ptrpointer缩写 即指针
    [ ]里的数据是一个地址值,这个地址指向一个双字型数据
    一次拷贝双字(4个字节)的数据到目的地址。
    es:[edi]指向目的串
    解释:合起来的意思就是,将栈上从 ebp-0E4h开始的位置,向高地址方向的内存赋值 0CCCCCCCCh,重复 39h 次,每次赋值双字(四字节的空间)。

  • call指令:将程序下一条指令的位置的IP压入堆栈中,并转移到调用的子程序。
    image-20210810171254154
    解释:将下一条指令的IP(00BF1A30)压入栈中,并移动到调用的子程序。

  • jmp指令:无条件跳转指令。
    image-20210810171429337
    解释:无条件跳转到IP为(0BF3BE0H)的位置。

  • add指令:用于将两个运算子相加,并将结果写入第一个运算子。
    image-20210810185459552
    解释:给 esp 加8,也就是 esp向高地址方向移动 8字节 ,相当于 pop操作后的指针变化。

  • ret指令:用于终止当前函数的执行,将运行权交还给上层函数。也就是,当前函数的帧将被回收。
    image-20210810171719031
    解释:执行这条命令之后,就自动返回刚才call指令的下一行。
    image-20210810171755047

3. 内存模型

image-20210812113541613

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

这次演示所使用的环境是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;
}

按下F10,在视图中打开调用堆栈窗口,我们发现main()函数被调用了。

但是main()函数被谁调用了呢?

image-20210810154724877

当我们接着调试到return 0;之后,再按F10,我们发现程序跳转到了调用main()函数的函数内

image-20210810165654723

原来main()函数是被__tmainCRTStartup函数调用的,而 __tmainCRTStartup又是被mainCRTStartup调用的。

接下来分步骤演示函数栈帧的创建和销毁的过程。

1. 为main()函数开辟栈帧

image-20210811155928381
m1

2. 在main()函数中创建变量

image-20210811070952006

main3

3. 调用Add()函数前的准备

image-20210811074733127

main5

4. 为Add()函数开辟栈帧

image-20210810181337849在这里插入图片描述

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

image-20210811200819392

Add2

6. Add()栈帧的销毁

image-20210811080322450

m3

7. 返回main()函数栈帧

image-20210811081128187

可以看到这里返回到了第3步(3. 调用Add()函数前的准备),最后指令call的下一条指令。

之后的过程还很复杂,这里就不详细展示了。

有兴趣的铁铁们可以自己研究研究。

总结

看到这里,想必以前学习中的许多困惑已经有了答案吧。

比如:

  1. 局部变量是怎么创建的?
  2. 为什么局部变量的值是随机值?
  3. 函数是怎么传参的?传参的顺序是怎样的?
  4. 形参和实参是什么关系?
  5. 函数调用是怎么做的?
  6. 函数调用是结束后怎么返回的?

希望可以对大家有所帮助,如果有什么不对的地方,还烦请指教,谢谢!

版权声明:本文为CSDN博主「三•九」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_45691717/article/details/119641497

生成海报
点赞 0

三•九

我还没有学会写个人说明!

暂无评论

相关推荐

基于51单片机的洗衣机控制系统

设计要求: (1)设计一个电子定时器,控制洗衣机作如下运转:定时启动→正转20s→暂停10s→反转20s→暂停10s→定时未到回到"正转20s-→暂停10s→反转20s→暂…“定时到则停止转动; (2)若定时到&#x