C语言 1000问 · 函数与递归Q1:一次函数调用在底层发生了什么?栈帧(stack frame)里都放了什么?

来源:Flomo 原始笔记|查看原笔记|迁移日期:2026-10-31

C语言 1000问 · 函数与递归Q1:一次函数调用在底层发生了什么?栈帧(stack frame)里都放了什么?

▎ 五大学习法:西蒙学习法——把「函数调用」钻到底层

调用过程(以典型 x86-64 / ARM 为例)

  1. 调用方准备参数:前几个参数放寄存器(x86-64 的 rdi / rsi / rdx…、ARM 的 r0-r3),多出的压栈。
  2. call 指令:把「返回地址」(call 下一条指令的地址)压栈,再跳到函数入口。
  3. 被调函数建立栈帧(prologue):保存调用方的帧指针 / 寄存器(push rbp; mov rbp, rsp),再为局部变量分配栈空间(sub rsp, N)。
  4. 执行函数体:访问参数(寄存器或栈上参数区)、操作局部变量。
  5. 返回(epilogue):恢复寄存器、释放局部空间、把返回值放寄存器(eax / rax、r0),ret 弹出返回地址跳回。
  6. 调用方继续:从返回地址往下执行。

栈帧里都放了什么

  • 返回地址(call 自动压入)。
  • 保存的调用方帧指针 / 被调用者保存寄存器(callee-saved)。
  • 函数的局部变量。
  • 溢出到栈的参数(超过寄存器参数个数的部分)。
  • 对齐填充(ABI 要求栈 16 字节对齐等)。
  • 有时还有临时空间、变长数组(VLA)、alloca 空间。

关键理解

  • 栈帧 = 一次函数调用占用的那块栈;函数进进出出,栈就是压栈 / 弹栈。
  • 帧指针(rbp / fp)像一根锚,让编译器用固定偏移访问局部变量和参数。
  • 编译器优化(-O2)常省略帧指针,用 rsp 直接算,省寄存器但调试难。
  • 调用约定(calling convention):x86-64 的 System V ABI(Linux)与 Microsoft x64 ABI 寄存器用法不同;ARM 有 AAPCS。跨语言 / 跨模块调用必须一致。

几个衍生点

  • 递归为什么危险:每层调用都建一个栈帧,太深就栈溢出。
  • 栈溢出攻击:超长输入覆盖返回地址、劫持控制流——所以有栈保护(canary)、NX、ASLR。
  • 尾调用优化:编译器可把递归尾调用变成跳转、不新建栈帧(C 不保证)。
  • 内联:直接把函数体展开,省掉调用和栈帧(但可能代码膨胀)。
  • 调试时看到的「调用栈」(backtrace)就是这一串栈帧。

优化带来的观察差异

  • 局部变量可能被优化进寄存器、不在栈上——调试时看不到,用 -O0 或 volatile 才看得到。

一句话:函数调用就是「传参 → call 压返回地址 → 建栈帧存局部与保存寄存器 → 执行 → 恢复并 ret」;栈帧里装着返回地址、保存的寄存器、局部变量和溢出参数——递归深、栈帧就多,这也是栈溢出和攻击的由来。

📎 问题库(专栏⑭导航)→ https://v.flomoapp.com/mine/?memo_id=MjQ1NzM0NTk5

📎 可变参数Q3(参数转发)→ https://v.flomoapp.com/mine/?memo_id=MjU2NDUwNzE4

C语言