注意:这个答案是 32 位特定的,因为我还没有做太多的 64 位调试。我不知道 64 位适用多少。
假设以下代码:
class foo_exception : public std::exception {};
void throw_foo()
{
throw foo_exception();
}
让我们假设您已开启打破 C++ 异常的首次机会异常:sxe eh
现在,当调试器中断时,您的异常记录将位于堆栈顶部。所以如果你只是想看看是什么类型,可以显示异常记录信息:
0:000> .exr @esp
ExceptionAddress: 751dc42d (KERNELBASE!RaiseException+0x00000058)
ExceptionCode: e06d7363 (C++ EH exception)
ExceptionFlags: 00000001
NumberParameters: 3
Parameter[0]: 19930520
Parameter[1]: 0027f770
Parameter[2]: 0122ada0
pExceptionObject: 0027f770
_s_ThrowInfo : 0122ada0
Type : class foo_exception
Type : class std::exception
看看当前的堆栈,你可以看到这些东西在哪里:
0027f6c4 e06d7363
0027f6c8 00000001
0027f6cc 00000000
0027f6d0 751dc42d 内核基础!引发异常+0x58
0027f6d4 00000003
0027f6d8 19930520
0027f6dc 0027f770
0027f6e0 0122ada0 langD!_TI2?AVfoo_exception
...
因此,在此示例中,异常本身位于 0027f770,正如您从 pExceptionObject 旁边的 .exr 输出中看到的那样。你可以在0027f6dc看到堆栈上的值,或者从堆栈顶部偏移0x18,所以@esp+18。让我们看看调试器告诉我们关于该位置的信息。
0:000> dpp @esp+18 L1
0027f6dc 0027f770 01225ffc langD!foo_exception::`vftable'
此命令说:从@esp+18 开始,d添加一个 p 其他大小的值,然后将在那里找到的值取消引用为 pointer,并写下与第二个地址匹配的任何符号的名称。在这种情况下,它找到了 foo_exception 类的 vtable。这告诉我们地址0027f770 的对象是foo_exception。我们可以使用该信息为条件断点创建表达式。
我们需要一种方法来直接获取 vtable 的地址,看起来像这样:
@!"langD!foo_exception::`vftable'"
由于反引号和撇号,我们必须引用它。我们还需要拉取所需的堆栈值:
poi(poi(@esp+18))
poi 运算符获取地址并返回存储在那里的指针大小的值。第一次求值将堆栈地址转换为对象地址,第二次求值将对象地址转换为 vtable 地址,我们需要进行比较。整个情况是这样的:
@!"langD!foo_exception::`vftable'" == poi(poi(@esp+18))
现在我们可以判断它是否是 foo_exception,我们可以通过设置一个命令在调试器因 C++ 异常中断时自动运行来跳过对它们的中断:
sxe -c".if ( @!\"langD!foo_exception::`vftable'\" == poi(poi(@esp+18)) ) {gc}" eh
翻译:
- 在第一次出现 C++ 异常时中断并运行以下命令:
- 将
foo_exception vtable 地址与@esp+18 处对象的vtable 地址进行比较
- 如果它们相同,则发出
gc 命令,如果在到达此命令时调试器正在运行,该命令将继续运行
- (不要忘记转义内引号)
如果您想仅为foo_exception 打破,请将条件从== 更改为!=。
需要记住的是,有时异常会作为指针而不是按值引发,这意味着您需要在表达式的@esp 部分周围再添加一个poi()。你会知道,因为当你用.exr 转储异常记录时,Type 将是class foo_expression *。这完全取决于引发异常的代码,而不是异常类型本身,因此您可能需要针对这种情况调整 .if-condition。
最后,如果您想中断或跳过几种异常类型,这是可行的。我建议writing a script 使用链接的.if、.elsif 命令并将sxe 自动命令设置为$$><path\to\script。在一行上做大量的 if 条件链接可能很难阅读和正确,尤其是在额外的转义时。脚本不需要额外的转义。这是一个小例子:
.if ( @!"langD!foo_exception::`vftable'" == poi(poi(@esp+0x18)) )
{
$$ skip foo_exceptions
gc
}
.elsif ( @!"langD!bar_exception::`vftable'" == poi(poi(@esp+0x18)) )
{
$$ dump the exception to see the error message, then continue running
dt poi(@esp+18) langD!bar_exception
gc
}
.elsif ( @!"langD!baz_exception::`vftable'" == poi(poi(@esp+0x18)) )
{
$$ show the top 10 frames of the stack and then break (because we don't `gc`)
kc 10
}
(注意:Windbg 会在运行时抱怨脚本错误,因为它不喜欢 gc 命令后跟其他任何内容。但它仍然运行良好)