⚙️ 第十章 通过汇编语言认识程序的真面目
📄 一句话概括

函数被调用时,开头和结尾各有一套固定动作:先把调用者的栈底存起来、再给自己定一个新栈底——这套动作叫【建立栈帧】。

📝 本节和前面几节的关系

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 秒实验

  1. 在纸上画出 pushl %ebp 和 movl %esp, %ebp 之后,栈里多了什么。
  2. 想一想:如果局部变量按 esp 定位会出现什么麻烦。

常见误区

  1. 以为函数体只有一行就不用建栈帧。进出套路一条都不会少。
  2. 以为局部变量是按 esp 定位的。是按 ebp 的相对偏移定位。
自测三问(点开对答案) 1. 函数入口和出口各有哪两条指令? —— 入口 pushl %ebp 与 movl %esp, %ebp;出口 popl %ebp 与 ret。 2. 栈帧是什么? —— 函数自己的一块专用地盘,局部变量都住在里面,用 ebp 的相对偏移定位。 3. 为什么局部变量不用 esp 定位? —— 因为函数执行期间栈还会继续变化,esp 一直在动;ebp 在函数内是固定的,偏移才稳定。

相关笔记