函数栈帧的创建和销毁(编程底层原理)

2023-12-15 20:50:12

? ? ? ? 本篇的内容格外的难写,里面包含了许多的专业术语名和汇编指令等晦涩难懂的东西,既不利于讲解,也不利于读者的理解。但我会尽力去讲述出里面的底层逻辑,帮助大家去理解里面的过程,理解编程的底层原理可以为我们后续更为复杂的知识学习打下基础。

一、什么是函数栈帧?

? ? ? ? 函数栈帧(stack frame)就是函数调用过程中在程序的调用栈(call stack)所开辟的空间,这些空间是用来存放:1、函数参数和函数的返回值。2、临时变量(包括函数非静态的局部变量以及编译器自动产生的其他临时变量)。3、保存上下文信息(包括在函数调用前后需要保持不变的寄存器)。

二、函数栈帧的创建和销毁

? ? ? ? 1、什么是栈?

? ? ? ? ? ? ? ? 栈(stack)是现代计算机程序里最为重要的概念之一,几乎每一个程序都要使用栈,函数因栈而生,栈是函数存在的根基。在经典的计算机科学中,栈被定义为一种特殊的容器,用户可以将数据压入栈中(入栈,汇编指令为push),也可以将已经压入栈中的数据弹出(出栈,汇编指令为pop),这里存在一个顺序问题:先入栈的数据后出栈(First In Last Out)。就像堆书一样,先堆上去的书在最后才能取下来,因为它被堆在了最下面。

? ? ? ? 实际上,栈在计算机系统中是一个动态内存区域,程序可以将数据压入栈中,也可以将数据从栈顶弹出,压栈操作是栈增大,而弹出操作时栈减小,栈总是向下增长(由高地址向低地址)的。在我们常见的i386或者x86-64下,栈顶由成为 esp 的寄存器进行定位的。

? ? ? ? 2、相关的寄存器及汇编指令

相关寄存器

eax:通用寄存器,保留临时数据,常用于返回值。

ebx:通用寄存器,保留临时数据。

ebp:栈底寄存器。

esp:栈顶寄存器。

eip:指令寄存器,保存当前指令的下一条指令的地址。

相关汇编指令

mov:数据转移指令,实际上就是赋值/存放。

push:数据入栈。就是压栈操作,同时esp栈顶的寄存器的位置改变。

pop:数据弹出至指定位置。就是出栈操作,同时esp栈顶寄存器的位置改变。

sub:减法命令。

add:加法命令。

call:函数调用。1、压入返回地址。2、转入目标函数。

jump:通过修改eip,转入目标函数,进行调用。

ret:回复返回地址,压入eip,类似pop eip命令。

????????3、解析过程

????????在每次调用函数时,都要为本次函数的调用开辟空间,即函数栈帧创建的空间。这块空间的维护是由esp和ebp两个寄存器进行的,ebp记录的是栈底的地址,esp记录的是栈顶的地址。

? ? ? ? 因讲解目的是理解代码编译的底层逻辑,本次使用的代码使比较简单的Add加法函数,如下:

#include <stdio.h>
int Add(int x, int y)
{
    int z = 0;
    z = x + y;
    return z;
}
int main()
{
    int a = 3;
    int b = 5;
    int ret = 0;
    ret = Add(a, b);
    printf("%d\n", ret);
    return 0;
}
????? ???调用堆栈是反馈函数调用逻辑的,那我们可以清晰的观察到,main 函数调用之前,是由 invoke_main 函数来调用 main函数。 那我们就可以确定, invoke_main 函数应该会有自己的栈帧, main 函数和 Add 函数也会维护自己的栈 帧,每个函数栈帧都有自己的 ebp esp 来维护栈帧空间。接下来我们从 main 函数的栈帧创建开始讲解:
? ? ? ? 开始之前我们要先进行一个操作, 为了让我们研究函数栈帧的过程足够清晰,不要太多干扰,我们可以关闭下面的选项,让汇编代码中排 除一些编译器附加的代码:

然后转到反汇编:?

????????调试到main 函数开始执行的第一行,右击鼠标转到反汇编。
注: VS编译器每次调试都会为程序重新分配内存,截图中的反汇编代码是一次调试代码过程中数据,每次调试略有差异。
函数栈帧的创建
????????接下来我们就一行行拆解汇编代码:

?这里插入一个小知识:

????????之所以上面的程序输出“ 这么一个奇怪的字,是因为 main函数调用时,在栈区开辟的空间的其中每一个字节都被初始化为 0xCC ,而 arr 数组是一个未初始化的数组,恰好在这块空间上创建的, 0xCCCC(两个连续排列的 0xCC )的汉字编码就是 ,所以 0xCCCC 被当作文本就是

????????我们继续,?接下来我们再分析main函数中的核心代码:

Add函数的传参

函数调用过程

????????call 指令是要执行函数调用逻辑的,在执行 call 指令之前先会把 call指令的下一条指令的地址进行压栈操作,这个操作是为了解决当函数调用结束后要回到 call 指令的下一条指令的地方,继续往后执行。

接下来跳转到Add函数,开始观察Add函数的反汇编代码

????????代码执行到Add 函数的时候,就要开始创建 Add函数的栈帧空间了。在 Add 函数中创建栈帧的方法和在 main 函数中是相似的,只在栈帧空间的大小上略有差异。
1. main 函数的 ebp 压栈。
2. 计算新的 ebp 和 esp。
3. ebx esi edi 寄存器的值保存。
4. 计算求和,在计算求和的时候,我们是通过 ebp 中的地址进行偏移访问到了函数调用前压栈进去的参数,这就是形参访问。
5. 将求出的和放在 eax 寄存器尊准备带回

函数栈帧的销毁

?????????当函数调用要结束返回的时候,前面创建的函数栈帧也开始销毁。让我们来看一下反汇编代码,去体会函数栈帧的销毁过程。

但调用完 Add 函数,回到 main 函数的时候,继续往下执行,可以看到:

文章来源:https://blog.csdn.net/zy1215058242/article/details/135020990
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。