在崩溃转储中查找所有可能的上下文记录

如果您调试了一段时间的崩溃转储,那么您可能遇到了这样的情况:调试器提供的初始转储上下文对应于在处理初始异常时发生的第二个异常,该异常可能更接近您正在调查的问题中的原始基础问题。
这可能很烦人,因为“.ecxr”命令将指向次要故障异常的位置,而不是原始异常上下文本身。然而,在大多数情况下,原始的、主要的异常上下文仍然在堆栈上;人们只需要知道如何找到它。

有两种方法可以解决这个问题:

  1. 对于硬件生成的异常(access violations),可以查找堆栈上的ntdll!KiUserExceptionDispatcher,它以PCONTEXT和PEXCEPTION_RECORD作为参数。
  2. 对于软件生成的异常(如C++异常),情况会变得更糟。你可以找在堆栈上调用ntdll!RtlDispatchException,然后从那里获取PCONTEXT参数。

如果堆栈展开失败,或者您正在处理其中一个转储,其中多个线程同时出现异常,这可能会有点乏味,这通常是由于崩溃转储写入失控。如果调试器能稍微自动化一下这个过程就好了。
幸运的是,用一点蛮力的方法来做到这一点其实并不难。具体地说,只是一个普通的“哑”内存扫描,以查找大多数上下文记录所共有的内容。这并不完全是一种巧妙的方法,但通常比手动在堆栈中低头要快得多,尤其是在涉及多个线程或多个嵌套异常的情况下。虽然可能会有误报,但通常很明显的一点是,涉及到一个活动异常有什么意义。然而,有时,快速而肮脏的暴力类型解决方案最终真的做到了这一点。

但是,为了基于内存搜索查找上下文记录,我们需要一些公共数据点,这些数据点通常对于所有上下文结构都是相同的,并且最好是连续的(为了便于使用“s”命令,调试器的内存搜索支持)。幸运的是,它以上下文结构的段寄存器的形式存在:

0:000> dt ntdll!_CONTEXT
+0x000 ContextFlags : Uint4B
[…]

+0x08c SegGs : Uint4B
+0x090 SegFs : Uint4B
+0x094 SegEs : Uint4B
+0x098 SegDs : Uint4B

[…]

现在,事实证明,对于给定进程中的所有线程,几乎总是具有相同的段选择器值,不包括异常的和非常不寻常的情况,如VDM进程。(x64上的段选择器值也是如此。)四个非零的32位值(实际上,零填充到32位的16位值)足以在不被误报的情况下合理地完成搜索。下面介绍如何使用臭名昭著的WinDbg调试器脚本(也适用于其他启用DbgEng的程序,如kd):

.foreach ( CxrPtr { s -[w1]d 0 l?ffffffff @gs @fs @es @ds } ) { .cxr CxrPtr – 8c }

这是一个有点冗长的命令,所以让我们把它分解成各个组件。首先,我们有一个“.foreach”构造,根据调试器文档,它遵循以下约定:

 .foreach [Options] ( Variable { InCommands } ) { OutCommands }

foreach命令(实际上是多个版本调试器脚本命令之一,一旦人们习惯使用它)基本上接受由输入命令(in commands)生成的一系列输入字符串,并调用一个命令来处理该输出(OutCommands),输入命令的结果作为变量参数。它很难看,并且基于文本解析进行操作(除其他外,还支持跳过每个X输入;请参阅调试器文档),但它完成了任务。

此操作的下一部分是s命令,它指示调试器在目标内存中搜索模式。这里提供的参数指示调试器只搜索可写内存(w),只输出每个匹配项(1)的地址,在地址空间(0L?)的较低4GB中扫描DWORD(4字节)大小的量化项(d)?在本例中,我们假设目标是一个32位进程(它可能托管在Wow64上,因此使用的是4GB而不是3GB)。命令的其余部分指定要查找的搜索模式;当前线程的段寄存器值。“s”命令有很多其他选项(不幸的是,有一个相当笨拙和复杂的语法);调试器文档运行在其他功能的范围内。

这个命令字符串的最后一部分是output命令,它简单地指示调试器将当前上下文设置为输入命令输出替换宏的值,偏移量为0x8c。(如果调用,0x8c是从结构_CONTEXT的开始到SegGs成员的偏移量,这是我们搜索的第一个值;因此“s”命令返回的地址将是SegGs成员的地址。)请记住,我们将“s”命令的输出限制为仅作为地址本身,这样我们就可以轻松地将该地址传递给不同的命令(这可能会让人认为“s”和“.foreach”命令是一起工作的)。

将命令字符串放在一起,它指示调试器在连续内存中搜索由四个32位值(当前线程的gs、fs、es和ds段选择器值)组成的序列,并显示每个匹配项的包含CONTEXT结构。

在执行这个comand时,除了异常相关的上下文记录之外,您还可以找到其他一些上下文记录(特别是,初始线程上下文是常见的),但是与错误相关的上下文记录通常是非常明显和不言而喻的。当然,这个方法并不是万无一失的,但是它让调试器为您做了一些艰苦的工作(这比在多个线程中手动地在损坏的堆栈中卑躬屈膝只为了提取一些上下文记录要好得多)。

当然,“.foreach”和“s”命令还有许多其他用途;不要害怕尝试使用它们。还有其他的助手可以自动执行某些任务(!for_each_frame,!for_each_local, !for_each_module, !for_each_process,!for_each_thread)。调试器脚本支持可能不是看起来最漂亮的,但是它可以非常方便地加速常见的、重复的任务。

一个带有“.foreach”的分隔提示(实际上是两个分隔提示):变量替换宏只有在用空格将其与其他符号分隔时才起作用。但是,在某些情况下,这可能是一个问题(在这种情况下,您需要对生成的展开宏执行某些运算,例如在这种情况下减去0x8c),因为宏符号展开时仍保留空格。有些命令,比如“dt”,不遵循标准的表达式解析规则(这让我非常恼火),如果它们用空格给出了参数,就会窒息。

但是,这些命令不会丢失所有内容;解决此问题的一种方法是将宏替换存储到伪寄存器中(例如,“r@$t0=ReplacementVariableMacro–0x8c”),并在实际输出命令中使用该伪寄存器,因为您可以在“输出命令”部分发出多个分号分隔的命令。

posted on 2019-12-07 13:44  活着的虫子  阅读(298)  评论(0编辑  收藏  举报

导航