首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【C语言】函数栈帧的创建和销毁

【C语言】函数栈帧的创建和销毁

作者头像
小年糕是糕手
发布2026-01-14 17:19:46
发布2026-01-14 17:19:46
1140
举报
文章被收录于专栏:C++学习C++学习
一、核心概念:什么是栈和栈帧?
  1. 栈 (Stack)
    • 它是内存中的一块连续区域,专门用于管理函数调用。
    • 它遵循 “后进先出” 的原则,就像一摞盘子,你总是取最上面的那个。
    • 有一个 栈指针 (SP - Stack Pointer) 寄存器,始终指向栈的顶部。
    • 在x86架构中,栈的生长方向是 从高地址向低地址 扩展的。
  2. 栈帧 (Stack Frame)
    • 也叫活动记录,是栈中为单个函数调用所分配的一块内存区域。
    • 每一个函数调用都会在栈上创建一个属于自己的栈帧。
    • 当函数调用结束时,其对应的栈帧会被销毁。
    • 它包含了该函数执行所需的所有信息,如局部变量、参数、返回地址等。
二、主角寄存器

在深入过程之前,先认识两个关键寄存器(以x86-32位为例):

  • esp (Extended Stack Pointer)栈顶指针,始终指向系统栈的顶部。
  • ebp (Extended Base Pointer)栈底指针,又称帧指针,指向当前函数栈帧的底部。函数内部通过ebp的固定偏移来访问参数和局部变量。

可以把ebpesp想象成一个画框的上下边框,它们框出了当前函数栈帧的范围

三、调用过程
第一阶段:函数栈帧的创建(函数调用开始时)

假设我们有如下代码,main函数调用add函数。

代码语言:javascript
复制
int add(int a, int b) {
    int result = a + b;
    return result;
}

int main() {
    int a = 10;
    int b = 20;
    int ret = add(a, b);
    return 0;
}

main函数执行到int ret = add(a, b);这一行时,栈帧的创建过程如下:

步骤 1:参数压栈 (从右向左)

  • 编译器通常以从右向左的顺序将函数参数压入栈中。
    1. 将参数b的值(20)压入栈。
    2. 将参数a的值(10)压入栈。
  • 此时,esp栈顶指针会向上(低地址方向)移动,指向最后压入的参数a

注意:这一步有时也通过push指令完成,它会隐式地减少esp并存入数据。 步骤 2:调用函数,压入返回地址

  • 执行 call add 指令。这条指令做了两件事:
    1. 下一条指令的地址(即call add后面那条指令的地址,这里是int ret = ...的地址)压入栈中。这个地址就是返回地址,函数执行完后要回到这里。
    2. 跳转到add函数的代码开始处执行。

步骤 3:进入新函数,保存旧栈帧

  • 现在,CPU开始执行add函数体的代码。
  • add函数首先要建立自己的“地盘”(栈帧)。
    1. push ebp: 将调用者(main函数)的ebp 值压栈保存。这是为了在add函数返回时,能恢复main函数的栈帧。
    2. mov ebp, esp: 让ebp指向新的栈顶。此时,ebpesp指向同一个位置,这就是add函数栈帧的底部

步骤 4:为新栈帧分配空间

  • sub esp, XXh: 将栈顶指针esp向上(低地址)移动一段距离(XX是16进制数)。这段新开辟的空间就是用于存放add函数的局部变量等数据。比如这里的int result

至此,add函数的栈帧已经完全创建好了。此时栈的布局如下图所示:

高地址 ... ---------------------------- |        参数 b (20)       | <-- main函数的栈帧 ---------------------------- |        参数 a (10)       | ---------------------------- |     main的返回地址       | ---------------------------- |    保存的main的ebp      | <-- [ebp]指向这里 (add栈帧的底部) ---------------------------- |                          | |   add函数的局部变量      | |     (例如: result)       | <-- [esp]指向这里 (add栈帧的顶部) |                          | ---------------------------- ... 低地址

第二阶段:函数体内的操作

在创建好的栈帧内,函数可以自由访问它的数据:

  • 访问参数: 通过 [ebp + 8] 访问第一个参数a,通过 [ebp + 12] 访问第二个参数b
  • 访问局部变量: 通过 [ebp - 4] 等方式访问局部变量result

函数体内部的运算(a + b)就在CPU的寄存器中进行,然后将结果存入局部变量result的位置。

第三阶段:函数栈帧的销毁(函数返回时)

add函数执行到return result;时,开始销毁自己的栈帧。

步骤 1:返回值处理

  • 通常,函数的返回值会存放在 eax 寄存器中。所以会执行 mov eax, [ebp - 4],将result的值放入eax

步骤 2:恢复栈指针和基址指针

  • mov esp, ebp: 将esp移回ebp的位置。这一步直接回收了为局部变量分配的所有栈空间esp现在指向保存的旧ebp
  • pop ebp: 将栈顶(esp指向的值)弹出到ebp寄存器中。这个值正是之前保存的main函数的ebp。现在,ebp就恢复指向了main函数的栈帧底部。同时,esp会自动下移(pop指令的效果),指向返回地址

步骤 3:返回调用者

  • 执行 ret 指令。这条指令会:
    1. 将栈顶的返回地址弹出到指令指针寄存器eip中。
    2. CPU接着从eip指向的地址(即main函数中call add的下一条指令)开始执行。

步骤 4:清理栈上的参数

  • 此时,esp指向参数amain函数需要清理为add函数调用压入的参数。
  • 通常通过 add esp, 8 指令实现(因为两个int参数共8字节)。这条指令让esp向下移动,完全回到了调用add之前的位置。

至此,add函数的栈帧被完全销毁,仿佛从未存在过。栈的状态和main函数调用add之前一模一样。main函数可以继续执行后续代码,并从eax寄存器中取得add函数的返回值,赋给局部变量ret

四、总结与要点

阶段关键操作作用创建参数压栈(从右向左)传递参数call 指令压入返回地址确保函数能正确返回push ebp / mov ebp, esp保存旧栈帧,建立新栈帧基线sub esp, XX为局部变量分配空间销毁mov eax, [ebp-4]将返回值存入eaxmov esp, ebp回收局部变量空间pop ebp恢复调用者的栈帧基线ret跳回调用处,继续执行add esp, XX (在调用者中)清理参数空间

五、理解栈帧的意义
  1. 实现函数调用/返回机制:通过返回地址和保存的ebp,保证了函数能层层调用并正确返回。
  2. 隔离作用域:每个函数的局部变量都在自己的栈帧中,实现了变量的隔离,避免了命名冲突。
  3. 支持递归:递归的每一层调用都有自己的栈帧,互不干扰。
  4. 调试利器:调试器就是通过分析栈帧链(每个保存的ebp都指向上一个栈帧的底部)来生成调用堆栈(Call Stack)信息的。
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2026-01-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、核心概念:什么是栈和栈帧?
  • 二、主角寄存器
  • 三、调用过程
    • 第一阶段:函数栈帧的创建(函数调用开始时)
    • 第二阶段:函数体内的操作
    • 第三阶段:函数栈帧的销毁(函数返回时)
  • 四、总结与要点
  • 五、理解栈帧的意义
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档