前言:
在C语言程序中,编写函数是我们必不可少的技能,但是我们通常会忽视了函数在内存中的开辟过程,一般地我们通过将一些功能封装到函数中,通过将实参传入,在主函数main.c中调用函数,但是事实上,函数调用的过程十分复杂,若是要追根究底的话,需要有一定的汇编知识,这篇文章我们略过汇编代码,通过用图文的知识去理解函数栈帧。

一、函数栈帧是什么?
在程序运行过程中,函数栈帧(Function Stack Frame) 是栈内存中为单个函数调用分配的一块独立内存区域。
主要作用如下:
①用于管理函数的参数、局部变量、返回地址、寄存器状态等信息
②它是函数调用和返回的基础,确保了函数之间的独立执行和数据隔离。
关于内存的分布如下图所示:

二、前言知识的准备
栈是一种 “简单却强大” 的数据结构。
简单来说,栈相当于一支乒乓球桶,我们放入乒乓球通过桶口放入,我们拿出乒乓球通过桶口拿出,底下的乒乓球先被放进去,顶上的乒乓球后被放进去,顶上的乒乓球先被拿出,底下的乒乓球后被拿出,所以说栈是一个先进后出的数据结构。
其中放入乒乓球被称为压栈(push)
其中拿出乒乓球被称为出栈(pop)
1.压栈

2.出栈

3.维护栈的指针(两个寄存器)
①EBP(栈底指针):固定指向当前栈帧的 “基地址”,用于定位栈帧内的参数、变量(偏移量固定)。
②ESP(栈顶指针):始终指向栈的 “顶部”,随压栈(
push)/ 弹栈(pop)动态移动。
4.CPU 的EAX寄存器
主要作用就是传递函数返回值
三、案例代码详解函数栈帧
代码示例:主函数中调用add函数,实现x+y
#include <stdio.h>
// 被调用函数:计算x+y
int add(int x, int y)
{
int z; // 局部变量
z = x + y; // 计算逻辑
return z; // 返回结果
}
// 调用者函数
int main()
{
int a = 2, b = 3; // main的局部变量
int c = add(a, b); // 调用add,接收返回值
printf("c=%d", c);
return 0;
}
阶段 1:初始状态
如下图所示:
main 函数执行中,未调用 add 前, 变量a先压入栈中,变量b后压入栈中,此时esp栈顶指针指向下一个即将压入栈中元素的位置,ebp维护的是main函数栈帧中的基底位置。

阶段2:调用 add 前
main 准备参数,压栈,C 语言函数参数默认从右向左压栈(先压最右边的参数
y,再压左边的x),然后压入 “返回地址”(add 执行完后,main 要回到的代码位置)。

步骤 2.1:压入 add 的第二个参数
如下图所示:
- 执行
push 3(将b的值压栈)- ESP 向下移动 4 字节(int 占 4 字节),指向新栈顶

步骤 2.2:压入 add 的第一个参数
如下图所示:
- 执行
push 2(将a的值压栈)- ESP 再向下移动 4 字节

步骤 2.3:压入 “返回地址”
如下图所示:
- 当 add 执行完后,需要回到 main 的
int c = add(a,b);下一条指令(即printf("c=%d", c);的地址,记为0x00401234);- 执行
push 0x00401234(压入返回地址);- ESP 再向下移动 4 字节。

阶段3: 进入 add 函数
创建 add 的栈帧,此时 CPU 跳转到 add 函数的代码,开始初始化 add 的栈帧 —— 核心是 “保存 main 的 EBP” 和 “设置 add 的 EBP”。
步骤 3.1:保存 main 的 EBP
如图所示:
- 为了 add 执行完后能恢复 main 的栈帧,需要先把 main 的 EBP压栈;
- 执行
push ebp;- ESP 向下移动 4 字节。

步骤 3.2:设置 add 的 EBP
如图所示:
- 将当前 ESP 的值赋给 EBP,此时 EBP 成为 add 栈帧的 “基地址”;
- 此时 EBP 固定指向 add 栈帧的 “基地址”,后续 add 访问参数 / 变量都通过 EBP 的偏移量(如
EBP+8是 x,EBP+12是 y)

步骤 3.3:分配 add 的局部变量空间
- 此时esp向低地址处一定的字节大小,同时add开辟一定的空间
- add 有局部变量
int z,需要在栈中至少预留 4 字节空间;

阶段4:执行 add 函数逻辑
如图所示:计算 z = x + y
核心逻辑:
- 取
x:访问EBP+8,值为 2;- 取
y:访问EBP+12,值为 3;- 计算
2+3=5,将结果存入z的地址

阶段 5:add 函数返回
如图所示:销毁 add 的栈帧,回到 main
步骤 5.1:传递返回值
z=5 存入 EAX,通过将函数中的返回值存入到Cpu中的EAX寄存器中,然后将其带回主函数。
步骤 5.2:释放 add 的局部变量空间
将 ESP 拉回 EBP 的位置,相当于 “回收” 局部变量
z的空间。
简单来说,add函数所开辟的空间被操作系统回收,用户不在拥有访问权限,后续会被其他在栈上开辟的空间所覆盖。

步骤 5.3:恢复 main 的 EBP
如图所示:
弹出旧 的EBP,相当于让EBP重新回到了main函数基底的位置。

步骤 5.4:弹出返回地址,回到 main 执行
如图所示:
- 执行
ret:将栈顶(0x1008)的 “返回地址 = 0x00401234” 弹出,CPU 跳转到这个地址;- ESP 向上移动 4 字节,指向 (栈顶现在是参数 x=2)

步骤 5.5:清理 add 的参数(main 回收参数空间)
如图所示:
- main 调用完 add 后,需要回收之前压入的参数(x=2、y=3);
- 执行
add esp, 8(ESP 向上移动 8 字节,因为两个 int 参数共 8 字节);

阶段 6:main 接收返回值,继续执行
如图所示:
- main 从
EAX寄存器中取出返回值 5,赋值给局部变量c(int c = 5);- 后续执行
printf("c=%d", c),输出c=5,程序结束。

四、总结:
函数栈帧的核心要点:
- 创建栈帧:调用者压参数→压返回地址→被调用者压旧 EBP→设新 EBP→分配局部变量。
- 使用栈帧:通过 EBP 的固定偏移访问参数(
EBP+偏移)和局部变量(EBP-偏移)。- 销毁栈帧:释放局部变量→恢复旧 EBP→弹出返回地址→清理参数→回到调用者。
既然看到这里了,不妨点赞+收藏,感谢大家,若有问题请指正。

2041

被折叠的 条评论
为什么被折叠?



