PAYATHON 2026

Windows x64 的栈帧、shadow space 和局部变量如何布局?

数据阿明

结论

你的示意图要成立,fun 必须先执行序言,保存 RBP、建立帧指针并分配局部空间。给出的代码只展示了 main 的序言;call fun 只会压入返回地址,不会自动保存 RBP 或为 fun 分配局部变量区。

按照标准 Windows x64 调用约定,调用者必须为被调用者预留 32 字节 shadow space。局部变量可以通过 RSP 或帧指针寻址,ABI 不要求使用 RBP。Microsoft x64 调用约定

刚进入 fun 时,栈是什么样?

设 S 表示刚进入 fun、尚未执行任何指令时的 RSP。以下偏移均为十六进制,每个槽位占 8 字节:

高地址
S + 0x28   main 保存的旧 RBP   <-- 此时 RBP 指向这里
S + 0x20   R9  home slot
S + 0x18   R8  home slot
S + 0x10   RDX home slot
S + 0x08   RCX home slot
S + 0x00   返回 main 的地址    <-- 此时 RSP = S
低地址

这里的 S + 0x28 位置由题目中 main 的具体序言决定,并非所有调用者都有这一布局。

执行 call 前,main 的 RSP 指向预留的 shadow space 的低地址端。call 将 RSP 减少 8 字节并保存返回地址,因此进入 fun 后,第一个 home slot 位于 [rsp+8]。

这些 home slot 不会自动填入 RCX、RDX、R8、R9 的值。需要时,fun 会主动保存相应参数。调用期间这块空间供 fun 使用,main 不能指望其中的数据跨调用保持不变。Microsoft 栈空间说明

叶函数是否需要保存 RBP?

不需要。例如,一个只计算整数结果的简单叶函数可以完全不调整栈:

fun:
    mov rax, rcx
    add rax, 1
    ret

此时只有返回地址和调用者预留的 shadow space,没有保存的 RBP,也没有额外分配的局部变量区。

“叶函数”有两种语境:通常,不调用其他函数的函数称为叶函数;Windows 展开规则还区分 leaf function 和 frame function。即使不调用其他函数,只要分配了栈空间或保存了非易失寄存器,函数就属于需要相应展开信息的 frame function。Microsoft 函数类型说明

非叶函数如何安排栈空间?

如果 fun 要调用 helper,它就成为 helper 的调用者,需要为这次调用预留 shadow space,并确保执行 call 前 RSP 按 16 字节对齐。

下面的示例使用 MASM x64 语法,假设 helper 是一个已定义的无参函数。示例为它预留 32 字节 shadow space,并分配 16 字节局部变量空间:

fun PROC FRAME
    push rbp
    .pushreg rbp

    sub rsp, 30h
    .allocstack 30h

    lea rbp, [rsp+30h]
    .setframe rbp, 30h
    .endprolog

    mov qword ptr [rbp-8], 123
    call helper
    mov rax, qword ptr [rbp-8]

    add rsp, 30h
    pop rbp
    ret
fun ENDP

序言完成后、调用 helper 之前,栈布局如下:

高地址
RBP + 0x28   fun 的第 4 个 home slot
RBP + 0x20   fun 的第 3 个 home slot
RBP + 0x18   fun 的第 2 个 home slot
RBP + 0x10   fun 的第 1 个 home slot
RBP + 0x08   返回 main 的地址
RBP + 0x00   保存的 main 的 RBP   <-- RBP
RBP - 0x08   局部变量
RBP - 0x10   另一个局部变量槽位
RBP - 0x18   helper 的第 4 个 home slot
RBP - 0x20   helper 的第 3 个 home slot
RBP - 0x28   helper 的第 2 个 home slot
RBP - 0x30   helper 的第 1 个 home slot <-- RSP
低地址

两块 shadow space 用于不同的调用:上面一块由 main 为 fun 预留,下面一块由 fun 为 helper 预留。执行 call helper 时,新的返回地址会被压在当前 RSP 下方。

按正常 ABI 入口计算,fun 入口处满足 RSP % 16 == 8。执行 push rbp 后,RSP 恢复 16 字节对齐;再减去 0x30,对齐仍然成立。如果不保存任何寄存器,且只需要 32 字节 shadow space,常见的分配量是 0x28,其中额外的 8 字节用于对齐。实际分配量还要计入局部变量、其他保存的寄存器和通过栈传递的参数。

示例中的 PROC FRAME 和点号伪指令用于描述展开信息。编写正式的 Windows x64 汇编时,这些信息必须与实际序言一致,尾声也要遵守指令形式要求。Microsoft 序言与尾声规则

局部变量用 RSP 还是 RBP 寻址?

两者都可以。按上面的布局:

mov rax, [rbp-8]
mov rax, [rsp+28h]

当函数主体中的 RSP 保持不变时,这两条指令访问同一个局部变量。

  • 固定大小栈帧可以直接使用 [rsp+offset],无需专门占用帧指针寄存器。
  • 使用 alloca 等动态栈分配时,需要保存并建立一个非易失帧指针,以定位栈帧的固定部分;帧指针不一定是 RBP。
  • [rbp-offset] 只是常见布局中的写法。偏移也可以为正,取决于帧指针设置在哪里。

Windows x64 没有可供函数随意使用的 red zone。需要局部栈空间时,应先调整 RSP,不要直接写入当前 RSP 以下尚未分配的地址。Microsoft 栈使用规则

备注:内容仅供参考。