获取函数调用者CPU上下文的最优方案及边缘场景处理
问题描述
在管理标准C++异常时,我希望为异常添加CPU上下文信息,因此编写了一套辅助抛出std::exception派生异常的函数,其中包含一个获取调用者上下文的汇编函数(AMD64架构、MASM、MSVC环境)。该函数通过调用RtlCaptureContext获取上下文并修正rdi、rcx、rax、rsp、rip等寄存器值以匹配调用者状态,但目前存在两个问题:
- 无法准确获取调用者
call指令前的地址,当前只能将rip设为返回地址; - 无法处理编译器生成的通过
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
相关产品推荐
相关产品推荐

