You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.29 09:06:20