理解操作系统对程序的反馈:异常(Exception)和通知(Debug Event)【3】

本文介绍了当错过程序崩溃的第一现场时,如何利用Windbg分析未处理异常,并通过.exr和.cxr命令恢复异常信息及上下文。此外,还探讨了使用adplus自动获取dump文件的方法,以及如何通过SetUnhandledExceptionFilter实现自定义异常处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值