函数栈帧的幕后世界
在计算机科学中,函数调用是程序执行的基础,但幕后真正支撑这一过程的是一种精妙而严谨的机制——函数栈帧。每一次函数的调用与返回,都伴随着栈帧的创建失眠的创建销毁,它们默默地管理着局部变量、参数传递、返回地址以及运行上下文。
理解栈帧的运作,不仅能够深入应用的行程序的执行流程,还能帮助开发者写出更高效、更安全的底层代码。对于这些难题,比如缓冲区溢出、内存泄漏等安全隐患,往往与栈帧的管理息息相关。 So,让我们一起揭开函数栈帧的神秘面纱,探索它在程序运行初始的每一个细节。
寄存器
在了解函数栈帧之前,我们首先应该明白一个概念——寄存器。那么什么是寄存器呢?
寄存器是CPU内部的一种高速存储单元,用于暂存指令、数据或地址,其访问速度远高于内存。它们直接参与运算和控制操作,如累加器存储算术结果,指令寄存器保存当前执行的指令。寄存器的位数通常与CPU架构相关(如32位或64位),数量有限但能显著提升处理效率。
下面笔者将要提到的 ebp 、esp 等就是众多寄存器之一。这两个寄存器存放的是地址,是用来维护函数栈帧的。
函数栈帧
第一,大家要了解什么是函数栈帧。每一个函数调用时,都要在栈区开辟一个空间,该空间就叫做函数栈帧,它是由 ebp 和 esp 两个寄存器共同维护的,且当前正在调用哪个函数,这两个寄存器就维护哪个函数的函数栈帧。假设我们现在由这样两个函数:
int add(int a, int b)
{
return a + b;
}
int main()
{
int a = 10;
int b = 20;
int c = add(a, b);
return 0;
}
因为不同的编译器在函数栈帧的过程中会存在一些不同,这里我们以VS2013为例。在VS2013中,main
函数也是被其他函数所调用的。而main
函数栈帧的栈帧如下图所示:(这里 ebp 存储的是高地址,esp 存储的是低地址)
在栈空间初始化时,ebp 和 esp 两个寄存器会首先划定一段内存区域:ebp 保存这段空间的高地址,esp 保存低地址。接着,edi、esi 和 ebx 三个寄存器依次执行压栈操作(压栈指在栈顶添加元素,出栈则是移除栈顶元素)。在此过程中,esp 指针会不断向低地址移动。这两个寄存器始终负责维护整个main
函数的栈帧结构。
随后,系统会在函数栈帧中填充随机值,通常是CCCCCCCC
。这就是为什么未初始化的局部变量会输出"烫烫烫烫烫烫"的原因。
这里我们再来看一下局部变量在函数栈帧中是如何创建的:在为函数创建的函数栈帧中,从底部开辟一部分空间,并在其中放入局部变量的值,这样,我们就完毕了局部变量的创建。
在上文提到的代码片段中,我们可以看到还有一个函数add
,那么在函数调用的时候,参数和被调用函数在栈中又是怎么运作的呢?
- 在调用函数之前,先将参数依次放入寄存器中,随后进行压栈操作,同时寄存器 esp 也要向低地址处移动。
- 在调用
add
函数时,编译器会先将寄存器 ebp 指向寄存器 esp 所指向的位置,随后 esp 向低地址移动一段距离,为add
函数开辟出属于它的函数栈帧。 - 在被调用的函数中,先为形参开辟一个空间,然后将参数从 ebp 高地址处的寄存器中取出,放入为形参开辟的空间中。
- 在函数运行结束之后,先将返回值放入 eax 寄存器中,接着根据原函数局部变量的创建位置和 ebp 寄存器的位置,将两个寄存器恢复到调用函数栈帧的位置。同时将被调用函数的函数栈帧销毁。最后,将 eax 寄存器中的值取出,放入调用函数的变量中。
经历了上述描述的过程,大家就完整的走完了函数栈帧的创建销毁的过程。这样一趟短暂的旅途下来,不知道是否为你对函数的调用实现等功能有了一个更深入的理解呢。因为设备困难,这里笔者不便向大家展示分析和操作时的全过程,如果有疑问,欢迎在评论区进行讨论。
要是这篇文章对你有支援,请为笔者点一个小小的赞,谢谢。