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 栈使用规则
备注:内容仅供参考。
