栈帧¶

当汇编函数用栈存储数据时,它通过创建栈帧来引用数据。它把 ESP 存入 EBP,然后从 ESP 减去 n 字节,实际上打开了一个夹在 EBP 和 ESP 寄存器之间的 n 字节间隙。为了更好地理解这一点,首先想象图 4-7 中的栈被传给一个需要 0x0C 字节局部存储空间的函数。
在这个示例中,地址 0x0000 是栈的绝对顶部。我们有从 0x0000 到 0xFF00 – 4 的未使用内存,在函数调用时,0xFF00 是栈顶。ESP 指向这个地址。0xFF00 之后的栈内存被调用链中前面的函数使用(从 0xFF04 到 0xFFFF)。函数被调用时,它做的第一件事是执行以下汇编代码,创建一个 0x0C(十进制 12)字节的栈帧:
PUSH EBP ; 保存下层栈帧的底部
MOV EBP, ESP ; 把当前栈帧的底部存入 EBP
; (也在下层栈帧上方 4 字节处)
SUB ESP, 0x0C ; 从 ESP 减去 0x0C 字节,沿栈向上移动,
; 标记栈帧的顶部
这段代码执行后,栈看起来更像图 4-8 所示。创建这个栈后,函数可以使用它在栈上分配的 0x0C 字节。

0x0000 仍然是栈的绝对顶部。我们有从 0x0000 到 0xFF00– 20 的未使用栈内存,地址 0xFF00 – 16 处的内存包含局部存储的最后 4 字节(由 [EBP-Ch] 引用)。这是当前栈帧的顶部,所以 ESP 指向这里。0xFF00 – 12 包含局部存储的中间 4 字节(由 [EBP-8h] 引用),0xFF00 – 8 包含局部存储的前 4 字节(由 [EBP-4h] 引用)。EBP 指向 0xFF00 – 4,这是当前栈帧的底部;这个地址保存 EBP 的原始值。0xFF00 是下层栈帧顶部,图 4-7 中的原始 ESP 指向这里。最后,你仍然可以看到调用链中前面的函数从 0xFF04 到 0xFFFF 的栈内存。
在这种栈状态下,函数可以随意使用它的局部数据。如果这个函数调用另一个函数,新函数会用同样的技术构建自己的栈帧(栈帧真的会堆叠起来)。不过,一旦函数用完栈帧,它必须把栈恢复到之前的状态。在我们的例子中,这意味着让栈看起来像图 4-7 那样。第二个函数结束时,我们的第一个函数用以下两条命令清理栈:

0xFF00-4)
但如果你想改变传给游戏中某个函数的参数,不要在函数的栈帧里找它们。函数的参数存储在调用它的函数的栈帧中,通过 [EBP+8h]、[EBP+Ch] 等引用。它们从 [EBP+8h] 开始,因为 [EBP+4h] 存储函数的返回地址。(第九十四页「函数调用」会进一步解释这个主题。)
注 代码可以编译为禁用栈帧。这种情况下,你会注意到函数不会以 PUSH EBP 开头,而是所有内容都相对于 ESP 引用。不过,大多数情况下,编译的游戏代码都启用了栈帧。