VC8 CRT的默认无效参数行为不会进入调试器

在这里工作的人时常感到困惑的一个问题是,如果你碰巧遇到了一个条件,这个条件触发了VC8的“无效参数”处理程序,并且你在这个进程上附加了一个失败的调试器,那么这个进程神秘地退出,而没有给调试器一个检查程序状态的机会问题。


对于那些不熟悉这个概念的人,“无效参数”处理程序是微软CRT的一个新添加,如果遇到各种无效状态,它会终止进程。例如,如果幸运的话,取消引用发布版本中的伪迭代器可能会触发无效的参数处理程序(如果不幸运的话,您可能会看到随机内存损坏)。


这里没有调试器交互的原因是默认的CRT无效参数处理程序(如果您手头有CRT源代码,则出现在invagg.c中)调用UnhandledExceptionFilter,试图(可能)让调试器看到异常。不幸的是,实际上,如果进程附加了调试器,UnhandledExceptionFilter将立即返回,假设这将导致标准SEH dispatcher逻辑将事件传递给调试器。因为默认的无效参数处理程序并没有真正通过SEH分派器,而实际上只是直接调用UnhandledExceptionFilter,所以这不会向调试器发出任何通知。


当您试图调试一个问题时,这种违反直觉的行为可能会让您有点困惑,因为从调试器中,您可能会看到,在一个错误的迭代器取消引用这样的情况下:

0:000:x86> g
ntdll!NtTerminateProcess+0xa:
00000000`7759053a c3              ret

如果我们提取一个堆栈跟踪,那么事情就会变得更有信息性:

0:000:x86> k
RetAddr           
ntdll32!ZwTerminateProcess+0x12
kernel32!TerminateProcess+0x20
MSVCR80!_invoke_watson+0xe6
MSVCR80!_invalid_parameter_noinfo+0xc
TestApp!wmain+0x10
TestApp!__tmainCRTStartup+0x10f
kernel32!BaseThreadInitThunk+0xe
ntdll32!_RtlUserThreadStart+0x23

然而,虽然我们可以通过一个简单的单线程程序获得触发无效参数事件的线程的堆栈跟踪,但是添加多个线程将破坏此场景的可调试性。例如,使用以下简单的测试程序,在继续初始进程断点后,在调试器下运行进程时,我们可能会看到以下情况(此示例在Vista x64下作为32位程序运行,但其他地方也应适用相同的原则):

0:000:x86> g
ntdll!RtlUserThreadStart:
sub     rsp,48h
0:000> k
Call Site
ntdll!RtlUserThreadStart

怎么了?好吧,这里进程中的最后一个线程碰巧是新创建的线程,而不是名为TerminateProcess的线程。更糟糕的是,另一个线程(导致实际问题的线程)已经不见了,被TerminateProcess杀死,它的堆栈也被吹走了。这意味着我们不能通过请求进程中所有线程的堆栈跟踪来查明发生了什么:

0:000> ~*k

.  0  Id: 1888.1314 Suspend: -1 Unfrozen
Call Site
ntdll!RtlUserThreadStart

不幸的是,这种情况在实践中相当常见,因为大多数非平凡程序出于某种原因使用多个线程。除此之外,许多操作系统提供的api在内部创建或使用工作线程。
在这样的场景中,有一种方法可以获得有用的信息,但不幸的是,事后并不容易做到,这意味着您将需要附加一个调试器,并在发生故障之前可以随意使用。在这里最简单的抓捕罪犯的方法就是在ntdll上设置断点!NtTerminateProcess。(如果进程经常调用TerminateProcess,则可以使用条件断点检查第一个参数中的NtCurrentProcess((HANDLE)-1),但情况通常不是这样,通常只需在例程上设置一个盲断点就足够了。)
例如,对于所提供的测试程序,我们可以通过设置断点获得更有用的结果:

0:000:x86> bp ntdll32!NtTerminateProcess
0:000:x86> g
Breakpoint 0 hit
ntdll32!ZwTerminateProcess:
mov     eax,29h
0:000:x86> k
RetAddr           
ntdll32!ZwTerminateProcess
kernel32!TerminateProcess+0x20
MSVCR80!_invoke_watson+0xe6
MSVCR80!_invalid_parameter_noinfo+0xc
TestApp!wmain+0x2e
TestApp!__tmainCRTStartup+0x10f
kernel32!BaseThreadInitThunk+0xe
ntdll32!_RtlUserThreadStart+0x23

对于完全错误的线程,这比堆栈跟踪更容易诊断。
注意,从错误报告的角度来看,可以通过注册无效的参数处理程序(通过设置无效的参数处理程序)来捕获这些错误,这与为纯虚拟函数调用失败注册自定义处理程序的机制非常相似。

 

posted on 2020-06-01 08:40  活着的虫子  阅读(303)  评论(0编辑  收藏  举报

导航