x64 FastCall调用栈管理疑问:Invoke宏为何用RBP而非Push?
x64汇编调用CreateFileW的参数传递疑问
问题背景
根据MSDN的x64调用约定:
- 最左侧4个整型参数依次通过
RCX、RDX、R8、R9寄存器传递; - 调用者需分配32字节的阴影存储,供被调用者保存寄存器;
- 剩余参数按从右到左的顺序入栈;
- 调用前栈指针
RSP必须保持16字节对齐。
我手动编写了调用CreateFileW的代码,可汇编但运行报错(错误码0x57,参数无效):
sub rsp, 20h ; 分配32字节阴影存储 mov rcx, offset filename ; lpFileName mov rdx, GENERIC_READ or GENERIC_WRITE ; dwDesiredAccess mov r8, FILE_SHARE_DELETE ; dwShareMode xor r9, r9 ; LpSecurityAttributes ; 剩余参数按从右到左入栈 push 0 ; hTemplateFile push FILE_ATTRIBUTE_NORMAL ; dwFlagsAndAttributes push CREATE_ALWAYS ; dwCreationDisposition call CreateFileW
但使用俄罗斯MASM64 SDK的Invoke宏生成的代码却能正常运行,代码如下:
mov rcx,src.403000 ; name mov edx,C0000000 ; GENERIC_READ or GENERIC_WRITE mov r8d,4 ; FILE_SHARE_DELETE xor r9d,r9d ; 0 mov qword ptr ss:[rbp-20],2 ; CREATE_ALWAYS mov qword ptr ss:[rbp-18],80 ; FILE_ATTRIBUTE_NORMAL mov qword ptr ss:[rbp-10],0 ; hTemplateFile call qword ptr ds:[<&CreateFileW>]
我的疑问是:为什么宏生成的代码用RBP而非push指令存储剩余参数,且看不到显式的32字节阴影存储分配?
解答
1. 为何用RBP而非PUSH存储剩余参数?
宏选择基于RBP的栈帧方式存储参数,而非直接push,原因有三点:
- 调试友好:以
RBP为基址的固定偏移能让调试器更方便地定位参数,避免push导致栈指针频繁波动带来的参数定位困难; - 栈对齐稳定:x64调用约定要求调用前
RSP必须是16字节对齐,push指令每次会让RSP减8,若剩余参数数量为奇数,会破坏16字节对齐;而通过RBP偏移写入参数,能精准控制栈布局,确保对齐要求; - 代码健壮性:当参数数量较多或需要修改参数时,栈帧方式比多次
push更易维护,不易出错。
2. 为何看不到显式的32字节阴影存储分配?
宏生成的代码依赖栈帧结构,阴影存储的分配是在函数的序言(Prologue)阶段完成的,而非显式的sub rsp, 20h:
- 通常函数序言会包含
push rbp; mov rbp, rsp,之后会通过sub rsp, xx分配足够的栈空间,其中已经包含了32字节的阴影存储,以及剩余参数所需的空间; - 你看到的
[rbp-20]、[rbp-18]等偏移,已经包含了阴影存储的32字节(0x20),剩余参数实际存储在阴影存储之后的栈区域中。
补充:你的手动代码报错原因
你的代码报错本质是栈对齐不符合要求:
- 调用
call指令前,RSP必须是16字节对齐(因为call会压入8字节的返回地址,压入后RSP仍需保持16字节对齐); - 你执行
sub rsp, 20h后,RSP是16字节对齐的,但连续push3个参数(每个占8字节)后,RSP的偏移为20h + 3*8h = 38h,此时RSP % 16 = 8,破坏了16字节对齐,导致CreateFileW因参数布局错误返回无效参数。
内容的提问来源于stack exchange,提问作者mantissa
相关产品推荐
相关产品推荐

