一
前言
经常做调试的朋友可能会遇到在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()
的原型,如下:
BOOLEAN RtlDispatchException(
IN PEXCEPTION_RECORD ExceptionRecord,
IN PCONTEXT ContextRecord
);
第一个参数是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
结果,伪代码整理如下:
void KiUserExceptionDispatcher(EXCEPTION_RECORD* exceptionRecord, CONTEXT* contextRecord)
{
if (Wow64PrepareForException)
{
Wow64PrepareForException(exceptionRecord, contextRecord);
}DWORD result;
if (RtlDispatchException(exceptionRecord, contextRecord) )
{
result = RtlGuardRestoreContext(contextRecord, 0);
}
else
{
result = ZwRaiseException(exceptionRecord, contextRecord, FALSE);
}RtlRaiseStatus(result);
}
疑问:
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
64
位程序发生异常时,可以使用这个规律查找到异常发生时的线程上下文信息,运行在32
位系统下的程序是否也满足这个规律?运行在64
位系统下的32
位程序呢?敬请期待~
看雪ID:编程难
https://bbs.kanxue.com/user-home-873494.htm
# 往期推荐
球分享
球点赞
球在看