前言
经常做调试的朋友可能会遇到在 windbg 里通过 k 系列命令得到的调用栈没有太大参考意义。一般是由于线程上下文不对导致的。这时候可以通过 !analyze -v 让 windbg 自动帮我们分析出正确的调用栈及异常发生时的线程上下文。有了上下文信息,就可以执行 .cxr address_to_context 命令切换上下文,这时候再通过 k 命令查看调用栈,一般可以得到一个有意义的调用栈。
但有时候 !analyze -v 分析出来的上下文信息也是不对的。这时候就需要我们自己手动查找异常上下文了。
这不,最近我就遇到了一个需要手动查找异常上下文的情况。经过调查发现了一个非常重要的规律 —— 64 位程序中,KiUserExceptionDispatcher 函数对应栈帧的 Child-SP 的值保存了异常发生时的线程上下文。
本文完整记录了整个查找验证的过程。
吐槽:
64位程序的参数传递方式与32位程序大不相同,不能根据ebp定位参数了。而是需要结合反汇编代码来推断某个函数的参数是否保存到栈上。如果没保存到栈上,基本上很难找到相关参数了。
!analyze -v
一般拿到一个转储文件后,我做的第一件事是执行 !analyze -v。因为很有可能直接就破案了。但是这次没那么幸运,从 !analyze -v 给出的调用栈,看不出是什么导致的异常。

!analyze -v 在给出调用栈的同时,也会给出 Exception Record (下图中 .exr 部分)和 Context Record (下图中 .cxr 部分)的地址。

点击上图中两条命令对应的超链接即可执行对应的命令。但是经过确认,.exr 和 .cxr 对应的内容并没有什么实际意义。看来只能手动查找异常发生时的线程上下文了。该从哪里入手呢?
查看反汇编
KiUserExceptionDispatcher() 是分发异常的关键函数,可以从次函数入手进行分析。KiUserExceptionDispatcher() 函数的原型如下:
void KiUserExceptionDispatcher(__in PEXCEPTION_RECORD ExceptionRecord, __in PCONTEXT ContextRecord)
我的第一反应是查看传递给该函数的 rdx( 64 位程序中 rdx 指向第二个参数) 是否保存到栈上,但是没有找到有用线索。
说明: 折腾完才反应过来,此函数是从
0环返回到3环的入口函数,不能用普通的函数调用机制来理解。
尝试查看一下 KiUserExceptionDispatcher() 的反汇编代码?在 windbg 中输入 uf ntdll!KiUserExceptionDispatch 查看该函数的反汇编代码,结果如下:

好在该函数的反汇编代码比较短,让我这个弱鸡能有信心继续调查。
看到了该函数内部会调用几个函数,一个是 ntdll!Wow64PrepareForException(),一个是 ntdll!RtlDispatchException(),还有一个是 ntdll!NtRaiseException()。我在网上搜了一下 ntdll!RtlDispatchException() 的原型,如下:
1 | BOOLEAN RtlDispatchException( |
第一个参数是 ExceptionRecord 的地址,第二个参数是 ContextRecord 的地址。结合上图中的反汇编代码及 x64 调用约定(前两个参数会通过 rcx 和 rdx 传递)可以猜测,rsp 指向了 ContextRecord,rsp+0x4f0 指向了 ExceptionRecord。但是为什么是 0x4f0 呢?难道 _CONTEXT 结构体的大小是 0x4f0 ?
小心求证
在 windbg 中查看这两个结构体的大小,如下图:

发现 0X4f0 比 _CONTEXT 的大了 32 字节 (0x4f0 - 0x4d0 = 0x20 = 32)。如果 esp 指向了 _CONTEXT ,那么在 _CONTEXT 结构体后面偏移 32 字节的位置存放了 _EXCEPTION_RECORD。到底是不是这样呢?
验证一下 _EXCEPTION_RECORD 的内容(为什么验证这个结构体?因为 _EXCEPTION_RECORD 的中的 ExceptionCode 比较容易辨认),如下图所示:

注意:
0000004e 78dfa980是调用栈帧0e的Child-SP的值。加上偏移0x4f0就得到了_EXCEPTION_RECORD对象的首地址。
从上图可知,ExceptionCode 的值是 0xc0000005 (常见的访问违例),导致异常的指令地址是 0x00007ffd bbde9863。从 ExceptionInformation[0] 可知导致异常的访问类型是读取(0 表示读取,1 表示写入),从 ExceptionInformation[1] 可知,导致异常的访问地址是 0x000001bc 12e12052 。
说明: 对以上字段的解读可以参考《软件调试》第一版 第
12章320页。
执行 .cxr 0000004e 78dfa980 切换线程上下文,然后执行 k 命令查看调用栈。

从上图红框高亮部分可以看出,在读取地址 0x000001bc 12e12052 时出错了,导致异常的指令地址是 0x00007ffd bbde9863。与 _EXCEPTION_RECORD 中的信息完全吻合。
上面的猜测应该是正确的。此时,突然想起早些时候也查过一个类似的问题。当时在看雪群里问了一下,有一位大佬说 esp 指向了异常上下文,当时没往心里去。
我想我肯定忘不了这个至关重要的规律了!
尝试破案
执行 !address 0x1bc12e12052 查看地址属性,可以发现,该地址对应的页面确实不可访问。

本想继续调查一下具体原因,奈何没有对应的调试符号,只简单查看了一下反汇编代码,没有特别的发现。由于手头 bug 比较多,而且没有调试符号,查起来会比较耗时间,剩下的工作就留给平台同事继续调查吧。
KiUserExceptionDispatcher 伪代码
通过前面的反汇编代码,结合在 IDA 中对 KiUserExceptionDispatcher() 进行 F5 结果,伪代码整理如下:
1 | void KiUserExceptionDispatcher(EXCEPTION_RECORD* exceptionRecord, CONTEXT* contextRecord) |
疑问:
windbg调用栈中给出的名字是ntdll!KiUserExceptionDispatch,少了er(好像也可以用ntdll!KiUserExceptionDispatcher),在IDA中对应的函数名字是ntdll!KiUserExceptionDispatcher。
更新 UnhandledExceptionFilter
有时候调用栈中没有 KiUserExceptionDispatcher,但是有 KERNELBASE!UnhandledExceptionFilter,只能通过 KERNELBASE!UnhandledExceptionFilter 进行查看。KERNELBASE!UnhandledExceptionFilter 只有一个参数,类型是 _EXCEPTION_POINTERS *。64 位程序的第一个参数会存储到 RCX 中,需要查看 KERNELBASE!UnhandledExceptionFilter 用到的 RCX 寄存器的值。通过 u 命令查看 KERNELBASE!UnhandledExceptionFilter 的反汇编代码。很幸运,rcx 的值保存到栈上 rsp+0x60 偏移处。

总结
64位程序中KiUserExceptionDispatch()对应栈帧的Child-SP保存了_CONTEXT参数,rsp+4f0处保存了_EXCEPTION_RECORD参数,可以根据这个规律直接找到异常发生时的线程上下文信息及异常信息。64位程序中KERNELBASE!UnhandledExceptionFilter对应栈帧的Child-SP+0x60的地方保存了_EXCEPTION_POINTERS的指针。当直接通过
k命令得到的调用栈不符合预期时,可以通过.cxr context切换到context指定的线程上下文后再次尝试。可以通过
?? sizeof(struct_name)查看某个结构体的大小。可以通过
!address addr查看某个地址对应的页面属性。
参考资料
《软件调试》第一版 第
12章 未处理异常和JIT调试RtlDispatchException()http://www.codewarrior.cn/ntdoc/winnt/rtl/mips/RtlDispatchException.htm调试笔记之VTUNE崩溃 http://advdbg.org/blogs/advdbg_system/articles/7063.aspx
More
64 位程序发生异常时,可以使用这个规律查找到异常发生时的线程上下文信息,运行在 32 位系统下的程序是否也满足这个规律?运行在 64 位系统下的 32 位程序呢?敬请期待~