13.3 栈帧布局
栈帧(stack frame)保存一次调用中无法完全放进寄存器的数据。依据目标 ABI 与函数特征,它可能包含返回地址、saved frame pointer、callee-saved 寄存器、局部变量、spill slot、outgoing 参数、临时调用区、对齐 padding、stack canary,以及垃圾回收或 unwind 使用的元数据。
精确布局是生成指令、calling convention、调试器、unwinder 与运行时之间的契约。偏移错误可能读到另一个局部;对齐错误可能让向量指令 fault,或在 call 边界违反 ABI。
分配槽位与偏移
寄存器分配完成后,后端才知道哪些值 spill、函数用了哪些 callee-saved 寄存器,此时开始构造 frame。每个 stack object 都有 size、alignment、lifetime 与用途。布局算法在保持对齐的同时放置对象,并记录相对 frame pointer 或 prologue 后 stack pointer 的偏移。
高地址
incoming stack arguments
return address
saved frame pointer
saved registers
local and spill slots
outgoing argument area
alignment padding
低地址 ← prologue 后的 stack pointer生命周期不重叠的对象可以共享栈槽,就像不干涉的值可以共享寄存器。安全或调试模式可能禁止部分复用。高对齐对象会制造 padding,因此放置顺序会影响总 frame size。
固定与动态 frame
固定大小 frame 可让每个槽都用相对 sp 或 fp 的常量位移寻址。变长数组与动态 alloca 会在运行时移动 stack pointer;此时 frame pointer 提供稳定基准,否则编译器必须精确追踪不断变化的 sp。
有些 ABI 提供当前 sp 下方的 red zone,leaf function 可以无需调整 sp 就使用,但中断和平台规则决定它是否安全。Windows x64 则要求 caller 提供 shadow space。这些都是 ABI 特性,不是所有栈的通用规律。
大 frame 可能需要 stack probing,逐个触及 guard-page 间隔,避免一次跨过保护页。Stack canary 会把秘密值放在易受攻击数据附近,并在返回前检查。二者都会增加指令和固定布局义务。
Prologue 与 epilogue
Prologue 建立 frame:保存必需寄存器、调整 stack pointer、必要时建立 frame pointer、存 canary,并为 unwind 描述新的 call-frame address。每条正常 epilogue 都必须执行逆操作后才能返回。
多个 return 可以跳到共享 epilogue,但 tail call 与异常出口使用不同规则。Shrink wrapping 只在真正需要 saved register 的区域周围保存/恢复,而不是函数入口统一处理,可以减少冷路径工作,却会让 unwind 信息更复杂。
运行时元数据与验证
精确 GC 需要 stack map,描述 safepoint 上哪些 frame slot 与寄存器保存 live pointer。异常处理需要 call-frame information(CFI)在 unwind 时恢复 saved registers;调试器则需要源码变量到不断变化的寄存器/栈位置映射。
Frame verifier 应检查最终大小满足对齐、每个偏移落在分配对象内、重叠槽生命周期不交叉、call site 恢复 ABI 所需栈对齐,以及每个 saved register 在所有 exit 上恢复。测试要覆盖空 leaf frame、高 spill 压力、动态分配、大对齐、stack argument、canary、异常与 tail call。