前言
经常做调试的朋友可能会遇到在 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
。
总结
64
位程序中KiUserExceptionDispatch()
对应栈帧的Child-SP
保存了_CONTEXT
参数,rsp+4f0
处保存了_EXCEPTION_RECORD
参数,可以根据这个规律直接找到异常发生时的线程上下文信息及异常信息。当直接通过
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
位程序呢?敬请期待~