函数被调用时,开头和结尾各有一套固定动作:先把调用者的栈底存起来、再给自己定一个新栈底——这套动作叫【建立栈帧】。
10.7 讲的是怎么跳过去、怎么回来,这一节讲跳过去之后,函数自己要做的那几件事。
一、入口和出口的固定动作
入口两件事:pushl %ebp(保存调用者的栈底)、movl %esp, %ebp(把自己的栈底定在当前位置)。
出口两件事:popl %ebp(恢复调用者的栈底)、ret(按栈上记的地址返回)。
四条指令,每次进出函数必然出现——哪怕函数体只有一行。
这也是为什么前面看到,一行 C 落到汇编会有六条指令(10.2 数过)。
二、栈帧是什么
这两条入口指令合起来有个名字:建立栈帧。 栈帧就是【这个函数自己的一块专用地盘】——它的局部变量全住在这里。 一做完,函数里的局部变量就有了固定的落脚点——全都用【ebp 减多少】来表示。 关键点在这儿:不管栈怎么长怎么缩,这个偏移都不变。
三、实测看 ebp 的变化
跑一遍:调用前 ebp 是 0x1000 这个值。
pushl %ebp 把老栈底压进栈,ebp 自己还没动。
movl %esp, %ebp 之后,ebp 换成了新的位置——栈帧立起来了。
函数返回时 popl %ebp,ebp 又变回 0x1000——像什么都没发生过。
这就是【函数进出栈】在地址上的样子。
四、为什么要有这把【固定标尺】
因为函数执行期间,栈还可能继续长——再调别的函数、再压参数,esp 一直在动。 如果局部变量按 esp 定位,那它每动一次、变量的位置就变一次,根本没法用。 换成 ebp 就稳了:无论栈怎么动,【ebp 减 4】永远是同一个格子。 所以 ebp 从头到尾就是那把固定标尺——这才是它存在的理由。
五、动手 Demo:被调用函数的工作原理
每个函数被调用时,开头和结尾都有一套固定动作。 跑一遍,看栈帧是怎么建立、又怎么拆掉的。
prog = [
("movl", "$100", "%ebx"),
("call", "sub1"),
("movl", "$7", "%ecx"),
("label", "sub1"),
("pushl", "%ebp"),
("movl", "%esp", "%ebp"),
("movl", "$8", "%eax"),
("popl", "%ebp"),
("ret",),
]
vm = VM(prog)
while vm.pc < len(prog):
shown, before, after = vm.step()
print(shown, "ebp=", hex(after["ebp"]), "栈深=", len(vm.stack))
实跑输出:
--------------------------------------------------------------------
被调用函数的工作原理
--------------------------------------------------------------------
每个函数被调用时,**开头和结尾都有一套固定动作**:
阶段 动作 为什么
------------------------------------------------------
入口 pushl %ebp 保存调用者的栈底
入口 movl %esp, %ebp 把自己的栈底定在当前位置
出口 popl %ebp 恢复调用者的栈底
出口 ret 按栈上记的地址返回
这两条入口指令合起来有个名字:**建立栈帧。**
**它一做完,函数里的局部变量就有了固定的落脚点**——
全都用【ebp 减多少】来表示,**不管栈怎么长,偏移都不变。**
指令 ebp 栈里有几个值 eax
----------------------------------------------------
movl $100, %ebx 0x1000 0 0
call sub1 0x1000 1 0
sub1: 0x1000 1 0
pushl %ebp 0x1000 2 0
movl %esp, %ebp 0x1ff8 2 0
movl $8, %eax 0x1ff8 2 8
popl %ebp 0x1000 1 8
ret 0x1000 0 8
sub1: 0x1000 0 8
**注意 ebp 的变化:从 0x1000 挪到了新位置,函数返回时又变回去。**
这正是【函数进出栈】在地址上的样子。
**ebp 从头到尾就是那把【固定标尺】——局部变量靠它定位。**
实测:ebp 从 0x1000 挪到 0x1ff8,函数返回时又变回 0x1000。 ebp 就是那把【固定标尺】——局部变量靠它定位,栈怎么动都不受影响。
30 秒实验
- 在纸上画出 pushl %ebp 和 movl %esp, %ebp 之后,栈里多了什么。
- 想一想:如果局部变量按 esp 定位会出现什么麻烦。
常见误区
- 以为函数体只有一行就不用建栈帧。进出套路一条都不会少。
- 以为局部变量是按 esp 定位的。是按 ebp 的相对偏移定位。
自测三问(点开对答案)
1. 函数入口和出口各有哪两条指令? —— 入口 pushl %ebp 与 movl %esp, %ebp;出口 popl %ebp 与 ret。 2. 栈帧是什么? —— 函数自己的一块专用地盘,局部变量都住在里面,用 ebp 的相对偏移定位。 3. 为什么局部变量不用 esp 定位? —— 因为函数执行期间栈还会继续变化,esp 一直在动;ebp 在函数内是固定的,偏移才稳定。- ☐ 下一节(10.9 全局变量和局部变量的工作原理)
相关笔记
- 函数调用的工作原理 — 怎么进来的
- 加载时生成的栈和堆 — 栈是什么时候建起来的
- 全局变量和局部变量的工作原理 — 这两类变量活法完全不同
- CPU是寄存器的集合体 — ebp、esp 这些格子
- ⚙️ 程序是怎样跑起来的 · 索引