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

获取函数调用者CPU上下文的最优方案及边缘场景处理

问题描述

在管理标准C++异常时,我希望为异常添加CPU上下文信息,因此编写了一套辅助抛出std::exception派生异常的函数,其中包含一个获取调用者上下文的汇编函数(AMD64架构、MASM、MSVC环境)。该函数通过调用RtlCaptureContext获取上下文并修正rdi、rcx、rax、rsp、rip等寄存器值以匹配调用者状态,但目前存在两个问题:

  1. 无法准确获取调用者call指令前的地址,当前只能将rip设为返回地址;
  2. 无法处理编译器生成的通过jmp指令调用该函数的场景(无返回地址栈可读取修正)。

对应的汇编代码:

EXTERN RtlCaptureContext: PROC

_TEXT SEGMENT

    GrabEHContext PROC

        push rdi ; save modified registers
        push rcx
        push rax

        sub rsp, 1264 ; sizeof shadow space for call & CONTEXT aligned with space for return address to 16 bytes
        mov ecx, 154 ; sizeof(CONTEXT) in QWORDS
        lea rdi, [rsp + 32] ; point to allocated CONTEXT
        xor eax, eax ; store a 0 (memset)
        rep stosq ; do the clear

        lea rcx, [rsp+32] ; set parameter to point to CONTEXT
        call RtlCaptureContext ; do the call

        mov rax, [rsp + 1264] ; get stored RAX
        mov [rsp + 1264 - 120], rax ; update RAX

        mov rax, [rsp + 1264 + 8] ; get stored rcx
        mov [rsp + 1264 - 128], rax ; update rcx

        mov rax, [rsp + 1264 + 8 + 8] ; get stored rdi
        mov [rsp + 1264 - 176], rax ; update rdi

        lea rax, [rsp + 1264 + 24 + 8] ; get previous rsp
        mov [rsp + 1264 - 152], rax ; update rsp

        sub rax, 8 ; point to return address
        mov rax, [rax] ; get the return address
        mov [rsp + 1264 - 248], rax ; update rip to point to return instruction
        
        ; at this point RCX has pointer to caller's CONTEXT
        ; call other function that throws...

    GrabEHContext ENDP


_TEXT ENDS
END

需要处理的边缘场景示例代码:

sub rsp, 40
jmp GrabEHContext ;no return address / stack to read and fixup

解决方案

1. 准确获取调用者call指令前的地址

依赖Windows提供的栈回溯API可以避免手动计算寄存器的误差,核心思路是用RtlVirtualUnwind自动回溯栈帧并修正调用者上下文:

  • 调用RtlCaptureContext获取当前上下文后,通过RtlLookupFunctionEntry获取当前函数的元数据;
  • 调用RtlVirtualUnwind回溯到调用者的栈帧,此时上下文的rip会被设置为call指令的下一条指令;
  • 若需要call指令本身的地址,需动态解析该返回地址的前序指令长度(x64下call指令长度为2-14字节),用返回地址减去指令长度即可得到call指令的起始地址。

2. 处理jmp调用的场景

要区分函数是被call还是jmp调用,可通过以下方法实现:

  • 栈地址合法性检查:call调用时栈顶会有合法的返回地址(属于可执行内存区域),用VirtualQuery检查该地址的内存属性,若无效则判定为jmp调用;
  • 利用RtlVirtualUnwind返回值:若RtlLookupFunctionEntry无法找到当前函数的元数据,或RtlVirtualUnwind回溯失败,直接判定为jmp调用场景;
  • 修正jmp场景的上下文:jmp调用时栈未被修改,直接用当前rip减去jmp指令长度得到调用者地址,同时修正rsp为函数调用前的栈指针。

优化后的汇编示例

EXTERN RtlCaptureContext: PROC
EXTERN RtlLookupFunctionEntry: PROC
EXTERN RtlVirtualUnwind: PROC
EXTERN VirtualQuery: PROC

_TEXT SEGMENT

GrabEHContext PROC
    ; 保存非易失性寄存器(AMD64调用约定要求)
    push rbx
    push rsi
    push rdi
    push rbp
    push r12
    push r13
    push r14
    push r15

    ; 预留shadow space和CONTEXT存储区(1280字节对齐)
    sub rsp, 1280
    lea rcx, [rsp + 64] ; CONTEXT存储位置
    call RtlCaptureContext

    ; 获取当前函数的函数表项
    mov rcx, [rsp + 64 + 248] ; 读取CONTEXT中的RIP(当前函数入口)
    call RtlLookupFunctionEntry
    test rax, rax
    jz HandleJmpCase ; 未找到函数表项,判定为jmp调用

    ; 回溯到调用者上下文
    mov rcx, 0          ; 回溯层数
    mov rdx, rax        ; 函数表项
    mov r8, [rsp + 64 + 248] ; 当前RIP
    lea r9, [rsp + 64]  ; CONTEXT指针
    sub rsp, 32         ; 预留shadow space
    call RtlVirtualUnwind
    add rsp, 32

    ; 解析call指令长度并修正RIP为call指令起始地址
    mov rax, [rsp + 64 + 248] ; 调用者返回地址(call下一条指令)
    ; 此处需动态解析指令长度,示例简化为减去5字节(常见相对call长度)
    sub rax, 5
    mov [rsp + 64 + 248], rax
    jmp ContextReady

HandleJmpCase:
    ; 处理jmp调用场景
    mov rax, [rsp + 64 + 248] ; 当前函数入口地址
    ; 解析jmp指令长度,示例简化为减去5字节
    sub rax, 5
    mov [rsp + 64 + 248], rax ; 设置RIP为jmp指令起始地址
    ; 修正RSP为调用前的栈指针
    lea rax, [rsp + 1280 + 64]
    mov [rsp + 64 + 152], rax

ContextReady:
    ; 此时RCX指向修正后的调用者上下文,调用异常抛出函数
    lea rcx, [rsp + 64]
    ; call ThrowExceptionWithContext(...)

    ; 恢复栈和寄存器
    add rsp, 1280
    pop r15
    pop r14
    pop r13
    pop r12
    pop rbp
    pop rdi
    pop rsi
    pop rbx
    ret
GrabEHContext ENDP

_TEXT ENDS
END

关键注意事项

  • 指令长度解析:x64指令长度不固定,建议实现简单的指令解码器或使用Windows内部的反汇编函数,避免硬编码长度导致错误;
  • 内存合法性检查:读取栈上地址前必须用VirtualQuery验证,防止非法内存访问;
  • 寄存器保存:严格遵循AMD64调用约定,保存所有非易失性寄存器,避免破坏调用者上下文。

内容的提问来源于stack exchange,提问作者Arush Agarampur

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 14:45:02