2.3.4 题外话和相关讨论
错过第一现场后还从dump中分析出线索吗
前面介绍了用Windbg截取1st chance exception进行分析的方法。
但是好多情况下,程序并没有运行在调试器下。崩溃发生后留在桌面上的是红色的框框,这时候已经错过了第一现场,但还是有机会找到对应exception的信息。
前面介绍过,红色的框框是通过 UnhandledExceptionFilter函数显示出来的,而UnhandledExceptionFilter的参数就包含了异常信息。这个时候检查UnhandledExceptionFilter的参数,就可以找到异常信息和异常上下文的地址,然后通过.exr和.cxr就可以在 Windbg中把对应信息打印出来。
(注意:在Vista和Windows 2008中,系统改良了Error Reporting功能。程序崩溃后,系统会在Error Reporting的时候从内核直接挂起出错的进程。这个时候如果用调试器检查,会看到出错进程就停在发生问题的指令上,不再需要在调试器中手动恢复 exception context。
详细信息可以参考:
Inside the Windows Vista Kernel: Part 3
http://www.microsoft.com/technet/technetmag/issues/2007/04/vistakernel/default.aspx?loc=en)
拿案例2中的第2个例子做一个实验。直接运行,崩溃后看到弹出的框框。这个时候不要点击确定,而是启动Windbg,attach到这个进程,然后用kb命令打印出call stack,找到UnhandledExceptionFilter的参数:
0:000> kb
ChildEBP RetAddr Args to Child
0012f74c 7c821b74 77e999ea d0000144 00000004 ntdll!KiFastSystemCallRet
0012f750 77e999ea d0000144 00000004 00000000 ntdll!ZwRaiseHardError+0xc
0012f9bc 004339be 0012fa08 7ffdd000 0044c4d8 kernel32!UnhandledExceptionFilter+0x4b4
第一个参数0012fa08保存的就是异常信息和异常上下文的地址:
0:000> dd 0x0012fa08
0012fa08 0012faf4 0012fb10 0012fa34 7c82eeb2
接下来用.exr加上异常信息地址打印出异常的信息:
0:000> .exr 0012faf4
ExceptionAddress: 0041a5a8 (release_crash!main+0x00000028)
ExceptionCode: c0000005 (Access violation)
ExceptionFlags: 00000000
NumberParameters: 2
Parameter[0]: 00000001
Parameter[1]: 00000000
Attempt to write to address 00000000
然后可以用.cxr加上异常上下文地址来切换上下文:
0:000> .cxr 0012fb10
eax=00000000 ebx=7ffde000 ecx=00000000 edx=00000001 esi=00000000 edi=0012fedc
eip=0041a5a8 esp=0012fddc ebp=0012fedc iopl=0 nv up ei pl nz na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010206
release_crash!main+0x28:
0041a5a8 c60000 mov byte ptr [eax],0x0 ds:0023:00000000=??
上下文切换完成后,可以用kb命令重新打印出该上下文上的call stack,就可以看到异常发生时候的状态:
0:000> kb
*** Stack trace for last set context - .thread/.cxr resets it
ChildEBP RetAddr Args to Child
0012fedc 00427c90 00000001 00361748 003617d0 release_crash!main+0x28 [c:/documents and settings/lixiong/desktop/amobrowser/release_crash.cpp @ 51]
0012ffc0 77e523cd 00000000 00000000 7ffde000 release_crash!mainCRTStartup+0x170
0012fff0 00000000 00418b18 00000000 78746341 kernel32!BaseProcessStart+0x23
这里可以直接看到问题发生在release_crash.cpp文件的第51行。
Adplus,天天都用的工具
如果要捕获崩溃时候的详细信息,通常可以在调试器下运行程序,或者使用更方便的adplus来自动获取异常产生时候的dump文件。可以参考:
How to use ADPlus to troubleshoot "hangs" and "crashes"
未处理异常发生后的主动退出
在某些特殊情况下,程序员为了需要,会在发生未处理异常后主动退出,而不是等到崩溃被动发生。使用这种技术的有COM+,ASP.NET,还有淘宝旺旺客户端。
这样做的好处是:
1. 可以自定义接口。
2. 可以把发生异常时候的详细信息保存下来以便后继分析。
3. 可以防止调试器带来的不必要干扰,保证发生崩溃的程序能立刻被系统回收,同时可以进行必要的挽救工作,比如重新启动发生错误的进程继续服务。
实现方法非常简单。一种方法是在程序的main函数,或者关键函数中,使用SEH的__try和__except语句捕获所有的异常。在__except语句中做相应的操作后(比如显示UI,保存信息)直接退出程序。
另外一种方法是使用SetUnhandledExceptionFilter。有很多程序有崩溃后发送异常报告的功能。淘宝旺旺客户端就是这样的一个例子,可以参考:
http://eparg.spaces.msn.com/blog/cns!59BFC22C0E7E1A76!817.entry
根据我的分析,淘宝旺旺客户端这里用了SetUnhandledExceptionFilter这个函数来定义自己的异常处理函数,在异常处理函数中通过MiniDumpWriteDump API实现dump的捕获。
使用这个技术的缺点就是调试器无法接收到2nd chance exception了,给调试增加了难度。比如要获取COM+程序上crash的信息,颇费一番周折,还需要使用上面提到的.exr/.cxr命令:
How To Obtain a Userdump When COM+ Failfasts
http://support.microsoft.com/?id=287643
How to find the faulting stack in a process dump file that COM+ obtains
http://support.microsoft.com/?id=317317
如何调试UnhandledExceptionFilter
根据MSDN的描述,UnhandledExceptionFilter在没有debugger attach的时候才会被调用。所以,SetUnhandledExceptionFilter函数还有一个妙用,就是让某些敏感代码避开 debugger的追踪。比如你想把一些代码保护起来,避免调试器的追踪,可以采用的方法:
1. 在代码执行前调用IsDebuggerPresent来检查当前是否有调试器加载上来。如果有,就退出。
2. 把代码放到SetUnhandledExceptionFilter设定的函数里面。通过人为触发一个unhandled exception来执行。由于设定的UnhandledExceptionFilter函数只有在调试器没有加载的时候才会被系统调用,这里巧妙地使用了系统的这个功能来保护代码。
第一钟方法很容易被绕过。看看IsDebuggerPresent的实现:
0:000> uf kernel32!IsDebuggerPresent
kernel32!IsDebuggerPresent:
281 77e64860 64a118000000 mov eax,fs:[00000018]
282 77e64866 8b4030 mov eax,[eax+0x30]
282 77e64869 0fb64002 movzx eax,byte ptr [eax+0x2]
283 77e6486d c3 ret
IsDebuggerPresent是通过返回FS寄存器上记录的地址的一些偏移量来实现的。([FS: [18]]:30保存的其实是当前进程的PEB地址)。在debugger中可以任意操作当前进程内存地址上的值,所以只需要用调试器把[[FS: [18]]:30]:2的值修改成0,IsDebuggerPresent就会返回false,导致方法1失效。
对于第二种方法,使用[[FS:[18]]:30]:2的欺骗方法就没用了。因为UnhandledExceptionFilter是否调用取决于系统内核的判断。用户态的调试器要想改变这个行为,要破费一番脑筋了。
Kwan Hyun Kim提供了一种欺骗系统的方法:
How to debug UnhandleExceptionHandler