X86-64汇编中栈操作正确性问题技术求助
X86-64汇编与C交互的栈规范及栈损坏问题修复
你这明显是在Windows x64环境下写汇编和C交互的代码时踩了调用约定的坑,我来帮你拆解问题并给出靠谱的解决办法:
一、逐个解答你的疑问
1. sub rsp, 28h的作用及规则
在Windows x64调用约定里,有两个硬要求必须遵守:
- 影子空间(Shadow Space):调用C函数前,必须给被调用函数分配至少32字节(0x20)的栈空间——哪怕函数不需要这么多参数也得分配,这是给C函数用来保存RCX/RDX/R8/R9这4个寄存器参数用的。
- 16字节栈对齐:执行
call指令之前,栈指针RSP必须是16字节对齐的(因为call会压入8字节的返回地址,执行后RSP会自动落到16字节边界上)。
你用的sub rsp, 28h(40字节),应该是32字节影子空间加上8字节的额外空间——要么是为了对齐,要么是用来临时存数据。要是移除这行,调用C函数时就破坏了栈对齐要求,程序直接崩溃,这就是为啥移除后没法运行。
2. RBX恢复时值不对的原因
你确实用push rbx保存了非易失性寄存器RBX(Windows x64里RBX属于非易失性寄存器,要么被调用函数保留它的值,要么调用者自己存起来),但后续的栈操作完全乱了:
你push rbx之后,又做了push ax、call MemRead8_s(这个call会自动把8字节的返回地址压进栈),这些操作都改了栈指针。等你执行pop rbx的时候,栈的位置早就不对了,弹出的根本不是之前存的RBX值,自然就错了。
3. Push的AX值被覆盖、RSP看似未变的问题
首先,AX是易失性寄存器,C函数MemRead8_s想怎么改就怎么改,这是正常的——但你保存AX的方式完全错了:
- 你
push ax之后,执行call MemRead8_s,这个call会把8字节的返回地址压进栈。此时栈里的顺序是:原来的RBX → 你push的AX → 返回地址。 - 等你
pop dx的时候,弹出的是返回地址的低2字节,根本不是你之前存的AX值!这就是为啥拿到错误值。 - 至于你说调用前后RSP没变,那是因为
MemRead8_s内部遵守了调用约定:它自己分配的栈空间在返回前已经恢复了,所以回到你的代码时RSP和call前一致,但call本身已经压入了返回地址,所以RSP其实比push ax时少了8字节。
二、Windows x64汇编与C交互的核心栈规则
我把必须遵守的规则整理成清晰的条目,记牢这些就不会踩坑:
- 非易失性寄存器必须自己存/恢复:RBX、RBP、RDI、RSI、R12-R15这些是非易失性寄存器,如果你在汇编函数里改了它们,必须用
push保存,函数返回前用pop恢复(顺序必须相反,先进后出)。 - 调用C函数前必须分配影子空间:调用前执行
sub rsp, 0x20(或者更大的、满足16字节对齐的值),调用完成后用add rsp, 0x20恢复。 - 调用C函数前必须保证栈对齐:
call指令执行前,RSP必须是16字节对齐的(因为call会压入8字节返回地址,执行后RSP会自动对齐到16字节边界)。 - 易失性寄存器不用特意保存:RAX、RCX、RDX、R8-R11是易失性寄存器,C函数可以随便改,要是你需要保留它们的值,得自己存到栈里或者非易失性寄存器中。
- 栈操作必须严格匹配:每一个
push对应一个pop,每一次sub rsp对应一次add rsp,不然栈平衡被打破,程序必崩。
三、修复你的代码
根据上面的规则,我修改了你的代码,每一步都加了注释说明:
PUBLIC Lbra_R_A ; Op 16 - Long Branch Always Lbra_R_A PROC ; 分配影子空间+对齐:0x20影子空间 + 0x8(给push rbx留的位置) ; 执行后RSP是16字节对齐,满足call前的要求 sub rsp, 28h ; 保存非易失性寄存器RBX(后面要修改它,必须先存起来) push rbx ; --- 计算分支目标地址 --- ; 获取当前PC值 movzx rcx, word ptr [pc_s] mov rbx, rcx ; 把PC存到RBX(非易失性寄存器,C函数不会动它) inc bx ; PC+1,指向偏移的高字节 ; 调用C函数读取高字节:Windows x64约定第一个参数放RCX call MemRead8_s ; 返回的字节在RAX的低8位(AL) mov bh, al ; 把高字节存到BH(RBX的高8位,安全不会被破坏) inc bx ; PC+2,指向偏移的低字节 mov rcx, rbx ; 设置MemRead8_s的参数(要读取的地址) call MemRead8_s ; 返回的低字节在AL mov bl, al ; 把低字节存到BL ; 把BH和BL拼成16位偏移(补码),计算目标地址:PC+2 + 偏移 mov ax, bx ; AX现在是完整的16位偏移 movzx rcx, word ptr [pc_s] add cx, 2 ; PC+2 add cx, ax ; PC+2加上偏移得到目标地址 mov word ptr [pc_s], cx ; 更新PC值 ; --- 恢复寄存器和栈 --- pop rbx ; 恢复之前保存的RBX(和push顺序相反) add rsp, 28h ; 释放之前分配的栈空间 ret Lbra_R_A ENDP
如果你还是想用栈来保存高字节,那得注意call会压入返回地址,所以应该把值存到影子空间里,而不是直接push:
PUBLIC Lbra_R_A ; Op 16 - Long Branch Always Lbra_R_A PROC sub rsp, 28h ; 0x20影子空间 + 0x8(push rbx用) push rbx movzx rcx, word ptr [pc_s] mov rbx, rcx inc bx call MemRead8_s ; 把高字节存到影子空间的某个位置(影子空间是rsp到rsp+0x20,这里选rsp+20h,避开C函数可能用到的区域) mov byte ptr [rsp+20h], al inc bx mov rcx, rbx call MemRead8_s mov ah, byte ptr [rsp+20h] ; 从影子空间取出高字节 ; 现在AX是高字节+低字节组成的16位偏移 movzx rcx, word ptr [pc_s] add cx, 2 add cx, ax mov word ptr [pc_s], cx pop rbx add rsp, 28h ret Lbra_R_A ENDP
四、额外提醒
- Windows x64中,函数返回值如果是8/16/32位,会存在RAX的低对应位里;64位返回值直接存在RAX里。你的
MemRead8_s返回字节,所以返回值在AL。 - 栈操作一定要严格对应,任何时候栈不平衡都会导致程序崩溃或者寄存器值乱掉——这是你这次遇到的核心问题。
- 调试时可以多观察RSP的值,对比每一步操作后的变化,能快速定位栈问题。
内容的提问来源于stack exchange,提问作者Walter
相关产品推荐
相关产品推荐

