shadow space
这是一个关于 Windows x64 调用约定中一个非常重要且特定的概念。
一、核心定义
Shadow Space(中文常译为“影子空间”或“阴影空间”)是 Windows x64 调用约定(包括 x64 fastcall)强制要求的一种栈空间。它在函数被调用时,由调用者在栈上为其预留的一块 32 字节(4个QWORD)的空间,紧邻着返回地址之后。
它的主要目的是为被调用函数提供一个固定的、专用的位置,用来临时保存寄存器参数。
二、为什么需要 Shadow Space?
在 x86 32位时代,参数通常通过栈来传递。但在 x64 下,为了提升性能,前 4 个参数(对于非浮点或混合类型)会使用寄存器传递:
- 第1个参数: RCX
- 第2个参数: RDX
- 第3个参数: R8
- 第4个参数: R9
这就带来一个问题:被调用的函数可能需要使用这些参数的值,但它可能也需要使用这些寄存器来做其他计算。如果它把寄存器覆盖了,原始的参数值就丢失了。
Shadow Space 就是为了解决这个问题而生的。 它提供了一个标准化的、在栈上的“安全港”。
三、Shadow Space 的工作原理
1.调用者的责任:
- 在 CALL 指令之前,调用者必须将栈指针(RSP)减少 至少 32 字节(即 sub rsp, 20h 或作为更大栈帧分配的一部分)。这32字节就是 Shadow Space。
- 虽然约定上说调用者“必须分配”,但并没有强制要求调用者必须向里面写入数据。
2.被调用函数的权利:
- 被调用函数可以自由地使用这 32 字节空间,而无需自己分配。
- 它最常见的用法是将前4个寄存器参数保存(spill)到栈上,以便在函数体内随时引用。
; 一个被调用函数的典型开头
MyFunction PROC
mov [rsp+8], rcx ; 将第1个参数保存到Shadow Space的第1个QWORD
mov [rsp+16], rdx ; 将第2个参数保存到Shadow Space的第2个QWORD
mov [rsp+24], r8 ; 将第3个参数保存到Shadow Space的第3个QWORD
mov [rsp+32], r9 ; 将第4个参数保存到Shadow Space的第4个QWORD
; ... 函数体 ...
ret
MyFunction ENDP
- 注意:[rsp] 的位置存放的是返回地址,所以 Shadow Space 是从 [rsp+8] 开始的。
3.调用者的清理:
- 在 CALL 指令之后,调用者负责将栈指针恢复,即 add rsp, 20h(或相应的清理操作)。
四、一个完整的示例
假设我们有一个 C 函数:
void MyFunc(int a, int b, int c, int d, int e) {
// ...
}
在汇编中,调用它看起来是这样的:
; 调用者代码
sub rsp, 20h ; 为MyFunc分配Shadow Space (32字节)
; 如果还有超过4个的参数,或者需要局部变量,这里会分配更多
mov ecx, 1 ; 第1个参数 a -> RCX
mov edx, 2 ; 第2个参数 b -> RDX
mov r8d, 3 ; 第3个参数 c -> R8
mov r9d, 4 ; 第4个参数 d -> R9
mov dword ptr [rsp+30h], 5 ; 第5个参数 e -> 栈上 (在Shadow Space之上)
call MyFunc ; 调用函数
add rsp, 20h ; 清理Shadow Space (以及其他栈分配)
ret
五、关键要点总结
1.谁分配,谁清理:调用者负责在调用前分配 Shadow Space,并在调用后清理。
2.固定大小:大小固定为 32 字节,无论被调用函数实际用了多少个寄存器参数。
3.用途:主要作为被调用函数的临时存储区,用于保存寄存器参数,使其在函数执行期间有一个稳定的内存地址可供引用。
4.调试意义:在调试器中,你可以查看 [rsp+8] 到 [rsp+32] 的内存区域,这通常就是被调用函数保存的寄存器参数,这对于逆向工程和调试非常有帮助。
5.必须遵守:即使是系统 API 调用,也必须遵守此约定。如果你在汇编中调用 MessageBox 或 CreateFile 等函数,忘记分配 Shadow Space 几乎肯定会导致栈损坏和程序崩溃。

浙公网安备 33010602011771号