x64 DLL栈访问越界疑问:VS2017编译代码反汇编异常分析
你看到的这个现象其实是Windows x64调用约定的正常行为,核心在于**影子空间(Shadow Space)**的机制。让我们一步步拆解:
1. Windows x64调用约定的基础规则
在Windows x64平台上,调用者在调用函数前必须为被调用者预留32字节的影子空间(也称为“寄存器参数区”)。这片空间的作用包括:
- 被调用者可用来保存RCX、RDX、R8、R9这四个参数寄存器的值(如果后续操作会覆盖这些寄存器的话)
- 被调用者也能自由使用它来保存非易失性寄存器(比如RBX、RDI、RSI等)或临时数据
调用者预留影子空间后执行call指令,此时会把返回地址(RIP)压入栈中(占8字节),进入被调用函数时的栈布局大致为:
[调用者栈帧] ... [影子空间起始地址] ← 调用者rsp - 32 [影子空间:32字节] [返回地址] ← 进入函数时的rsp
2. 你的函数栈帧计算
看你的反汇编代码:
000007FEFAD01000 | 40 57 | push rdi ; 保存非易失性寄存器RDI,rsp减少8字节 000007FEFAD01002 | 48 83 EC 20 | sub rsp,20 ; 分配32字节栈空间(20为十六进制=32),rsp再减少32字节
执行这两步后,当前rsp的位置是:进入函数时的rsp -8(push rdi) -32(sub rsp,20) = 调用者rsp -32(影子空间) -8(返回地址) -8 -32 = 调用者rsp -80
再看写入[rsp+30]的指令:
000007FEFAD0102B | 48 89 5C 24 30 | mov qword ptr ss:[rsp+30],rbx
这里的rsp+30(30为十六进制=48字节)对应的绝对地址是:调用者rsp -80 +48 = 调用者rsp -32
这个地址正好是调用者预留的影子空间的起始位置!
3. 为什么编译器要这么做?
RBX是Windows x64调用约定中的非易失性寄存器——函数必须保证调用前后RBX的值不变,所以你的函数需要先保存RBX的值,退出时再恢复。
编译器选择把RBX保存在影子空间里,而非自己分配的栈空间,原因很简单:影子空间已经被调用者分配好,属于可用内存区域,不需要额外分配栈空间,能稍微优化栈空间的使用。
后面的指令也验证了这一点:
000007FEFAD01043 | 48 8B 5C 24 30 | mov rbx,qword ptr ss:[rsp+30] ; 恢复RBX的值
总结
你看到的[rsp+30]并不是父函数的私有栈空间,而是调用者为当前函数预留的影子空间,当前函数完全有权限使用这片区域。这是VS2017编译器针对Windows x64平台的正常优化行为,不属于异常现象。
内容的提问来源于stack exchange,提问作者Chin

