前言

今天是第十三天,老师讲了函数栈帧的创建和销毁。第一遍听的时候,完全懵了——ESP、EBP、压栈、弹栈,感觉在听天书。第二遍硬着头皮听,做了点笔记,又追问了好几次,才终于搞懂“原来函数调用时底层是这么回事”。这篇博客不抄课件,只记录我最后搞懂的那张图和那个比喻。

摘要: 本文用“草稿纸”的比喻,通俗讲解 C 语言函数栈帧的创建与销毁。先回答“为什么局部变量只在函数内有效”,再介绍栈的 LIFO 特性与 ebpesp 两个关键指针,随后以 Add(3, 5) 为例,逐步拆解函数调用时压参、压返回地址、保存旧 ebp、设置新 ebp、为局部变量腾空间的完整过程,并说明返回时栈帧如何销毁。最后用表格总结局部变量生命周期、递归栈溢出、返回局部变量地址等常见疑问,附上作者第二遍听课的核心笔记。

〇、先回答一个核心问题:为什么局部变量只在函数内有效?

这个问题,从学函数第一天就有人问。答案就在栈帧里:

每次调用函数,系统会在栈区给它划一块“临时草稿纸”。函数返回,这张草稿纸就被撕掉。草稿纸都没了,上面的字(局部变量)当然也没了。

这是C语言底层的规定,不是你写代码能改变的。所以,局部变量的生命周期 = 函数的生命周期

一、什么是“栈”?

计算机内存中有一块专门用来临时存放函数调用信息的区域,叫

它是一块后进先出(LIFO) 的存储空间——就像一叠盘子,最后放上去的,最先被拿走。

每次调用一个函数,系统就在栈上叠一张新草稿纸(栈帧)。函数执行完,就把最上面那张纸撕掉(销毁栈帧)

栈帧里存放的是:

  • 函数的参数
  • 局部变量
  • 返回地址(函数结束后回到哪里)

二、两个关键指针:ebpesp

老师在讲栈帧时,绕不开两个寄存器:ebpesp。第一遍听的时候,这两个东西让我最头疼。后来用比喻理解了:

  • ebp(栈底指针):一根固定的钉子,钉在当前草稿纸的底部。通过它,可以准确找到参数和局部变量。
  • esp(栈顶指针):一支移动的笔,随着数据不断压入和弹出而上下移动,永远指向当前草稿纸的“写到的位置”。

简而言之: ebp 是基准线,esp 是活动端。它们共同划定了一块栈帧的范围。

三、函数调用时,栈帧的变化过程(完整拆解)

以这段代码为例:

int Add(int x, int y)
{
    int z = x + y;
    return z;
}

int main()
{
    int result = Add(3, 5);
    return 0;
}

第 1 步:调用前(main 正在执行)

  • ebpesp 指向 main 函数的栈帧
  • 栈上已经有 main 的局部变量

第 2 步:压入参数(从右向左)

C 语言规定,函数参数从右向左压栈:

  1. 先压入 5y 的值)—— esp 下移(栈向低地址生长)
  2. 再压入 3x 的值)—— esp 继续下移

第 3 步:压入返回地址

call 指令会做两件事:

  1. 下一条指令的地址(返回地址)压入栈
  2. 跳转到 Add 函数的起始位置

第 4 步:保存旧的 ebp

进入 Add 函数后,第一件事是保存调用者(main)的 ebp

  • push ebp:把 mainebp 压入栈

这样等函数返回时,可以恢复 main 的栈帧位置。

第 5 步:设置新的 ebp

  • mov ebp, esp:让 ebp 指向当前 esp 的位置

现在 ebp 就是 Add 函数栈帧的底部基准

第 6 步:为局部变量腾空间

Add 函数里有一个局部变量 z

  • sub esp, 4:把 esp 向下移动 4 个字节(int 占 4 字节),为 z 腾出空间

现在 Add 的栈帧已经完整。我画了一张示意图:

高地址
┌─────────────┐
│   main 的   │
│   栈帧      │
├─────────────┤
│   参数 y=5   │  ← ebp + 8
├─────────────┤
│   参数 x=3   │  ← ebp + 4
├─────────────┤
│   返回地址   │
├─────────────┤
│   旧的 ebp   │  ← ebp(当前栈底)
├─────────────┤
│   局部 z     │  ← ebp - 4
│             │  ← esp(当前栈顶)
└─────────────┘
低地址

注意: 栈向低地址生长,所以地址从上往下减小。

四、函数返回时,栈帧如何销毁?

  1. mov esp, ebp:让 esp 回到 ebp 的位置(释放局部变量占用的空间)
  2. pop ebp:恢复 main 函数的 ebp(从栈上弹出旧的 ebp 值)
  3. ret:弹出返回地址,跳回 main 继续执行

至此,Add 函数的栈帧被完全销毁,局部变量 z 不复存在。

五、理解了栈帧,你能回答这几个问题

问题答案
为什么局部变量只在函数内有效?函数返回时,栈帧被销毁,局部变量一起消失
为什么递归太深会栈溢出?每层递归都占一块栈帧,叠太多就把栈空间撑爆了
为什么不能返回局部变量的地址?返回的是草稿纸上的门牌号,但草稿纸已经被撕掉了

六、我的听课笔记(第二遍才记下来的)

以下是第二遍听课时记的核心内容:

  • ebpesp 存放的是地址,用来维护函数栈帧
  • 当有新的栈帧时,esp 会去到最上面,ebp 在新的函数栈底
  • 每一个函数调用,都要在栈区建立一个空间
  • 压栈(push):给栈顶放一个元素
  • 弹栈(pop):从栈顶删除一个元素
  • 有新的元素都是往新的栈帧上堆

七、写在最后

函数栈帧这一讲,第一遍听的时候我完全是懵的。听不懂不是因为笨,是因为这是第一次接触“函数调用在底层发生了什么”这个问题。第二遍听,结合画图和比喻,才终于搞懂。

如果你也卡在这里,建议你拿张纸画一画:画一个栈,标出 ebpesp 的位置,然后按步骤把参数、返回地址、局部变量填进去。画一遍,比看十遍都管用。

苔花如米小,也学牡丹开。下次见。 (●’◡’●)


(全文完)

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐