Windows用户态程序高效排错 -- 异常(Exception)和通知(Debug Event)

理解操作系统对程序的反馈:异常(Exception)和通知(Debug Event)
本小结首先介绍异常的原理和相关资料,再举例说明异常跟崩溃和调试是如何紧密联系在一起的。最后说明如何利用工具来监视异常,获取准确的信息。

2.3.1  异常(Exception)的方方面面和一篇字字珠玑的文章

异常是CPU,操作系统和应用程序控制代码流程的一种机制。正常情况下,代码是顺序执行的,比如下面两行:

*p=11;

printf(“%d”,*p);

这里应该会打印出11。 但若p指向的地址是无效地址呢?那么这里对*p赋值的时候,也就是CPU向对应地址做写操作的时候,CPU就会触发无效地址访问的异常,接下来的printf很可能就不会执行了。

从这个简单的例子可以看到,当程序行为跟预期相左的时候,很可能就是异常的发生改变了程序的执行逻辑。在很多案例中,抓准异常的原因,其实就解决了问题。

异常发生的时候,由于操作系统在内核挂接了对应的CPU异常处理函数,CPU就会跳转去执行操作系统提供的处理函数,所以printf就不一定会被执行了。在操作系统的处理函数里面,如果检测到发生在用户态的程序的异常,操作系统会再把异常信息发送给用户态进程对应的处理函数,让用户态程序有处理异常的机会。

用户态程序处理完了异常,代码会继续执行,不过执行的次序可以是紧接着的下一个指令,比如printf,也可以跳到另外的地址开始执行,比如catch block,或者重新执行一次出错的指令。这些都是用户态的异常处理函数可以控制的。

如果用户态程序没有处理这个异常,那操作系统的默认行为就是中止程序的执行,然后用户可以看到给Microsoft发送错误报告的界面,或者干脆就是一个红色的框框,说某某地址上的指令在访问某某地址的时候遭遇了访问违例的错误。

除了上面的非预期异常,也可以手动触发异常来控制执行顺序,C++/C# 中的throw关键字就可以触发异常。手动触发异常需要依赖于编译器和操作系统API来实现。

异常的类型,是通过异常代码来标识的。比如访问无效地址的号码是0xc0000005,而C++异常的号码是0xe06d7363。其他很多看似跟异常无关的东西,其实都是跟异常联系在一起的,比如调试的时候设置断点,或者单步执行,都有通过break point exception来实现的。越权指令,堆栈溢出的处理也依靠异常。在Windbg帮助文件的Controlling Exceptions and Events主题里面,有一张常用异常代码表。

程序的行为跟预期的不一样,直接原因是代码执行次序跟预期的不一样。异常改变了代码执行次序,比如代码中从来都没有什么函数跳一个红框框出来,说某某地址上的指令在访问某某地址的时候遭遇了访问违例。弄清楚异常发生的时间、地址、导致异常的指令和异常导致的结果对排错是至关重要的。

异常如此重要,所以操作系统提供了对应的调试功能,可以使用调试器来检视异常。异常发生后,操作系统在调用用户态程序的异常处理函数前,会检查当前用户态程序是否有调试器加载。如果有,那么操作系统会首先把异常信息发送给调试器,让调试器有观察异常的第一次机会,所以也叫做first chance exception,调试器处理完毕后,操作系统才让用户态程序来处理。

如果用户态程序处理了这个异常,就没调试器什么事了。否则,程序在unhandled exception崩溃前,操作系统会给调试器第二次观察异常的机会,所以也叫做second chance exception。

请注意,这里的1st chance, 2nd chance是针对调试器来说的。虽然C++异常处理的时候也会有first phrase find exception handler, second phrase unwind stack这样的概念,但是两者是不一样的。

操作系统提供的异常处理功能叫做 Structrued Exception Handle(SEH),C++和其他高级语言的异常处理机制都是建立在SEH上的。如果要直接使用SEH,可以在C/C++中使用__try,__except关键字。

关于异常处理的详细信息,所有的来龙去脉,操作系统做了些什么事情,C++编译器做了些什么事情,SEH和C++异常处理的关系,以及调试器是如何参与的,下面几篇文章有非常详细的介绍。

A Crash Course on the Depths of Win32™ Structured Exception Handling

http://www.microsoft.com/msj/0197/Exception/Exception.aspx

这篇文章出来后,没见人写第二篇了。深入浅出,字字珠玑。

RaiseException

http://msdn.microsoft.com/library/default.asp?url=/library/en-us/debug/base/raiseexception.asp

注意,上面链接中,remark section详细介绍了异常处理函数是如何被分发的。

案例分析:如何让C++像C#一样打印出函数调用栈(callstack)

如果用C#或者Java,在异常发生后,可以获取异常发生时刻的call stack。但是对于C++,除非使用调试器,否则是看不到的。现在用户想尽可能少地修改代码,让C++程序在异常崩溃后,能够打印出call stack,有什么方法呢?

我的解法是直接使用SEH,加上局部变量析构函数在异常发生时候会被执行的特点来完成。这个例子当时使用VC6在Windows 2003上调试通过。当重新整理这个例子的时候,发现这段代码在VC2005+Windows 2003 SP1上有奇怪的现象发生。如果用debug模式编译,运行正常。如果用release模式编译,程序会在没有任何异常报告的情况下悄然退出。关于整个源代码和对应的分析,请参考:

SEH,DEP, Compiler,FS:[0] and PE format

      http://eparg.spaces.msn.com/blog/cns!59BFC22C0E7E1A76!712.entry

2.3.2  Adplus,抓取dump的方便工具

前面提到了dump文件能保存进程状态,方便分析。由于dump文件记录的是进程某一时刻的具体信息,所以保存dump的时机非常重要。比如程序崩溃,dump应该选在引发崩溃的指令执行时(也就是1st chance exception发生的时候)获取,这样分析dump的时候就能够看到问题的直接原因。

Adplus是跟Windbg在同一个目录的VBS脚本。Adplus主要是用来抓取dump文件。 详细的信息,可以参考Windbg帮助文件中关于adplus的帮助。有下面一些常见用法:

假设我们的目标程序是test.exe:

假设test.exe运行一段时间崩溃,在test.exe启动后崩溃前的这个时间段,运行下面的命令监视:

Adplus –crash –pn test.exe –o C:/dumps

当test.exe发生2nd chance exception崩溃的时候,adplus在C:/dumps生成full dump文件。当发生1st chance AV exception, 或者1st chance breakpoint exception的时候,adplus在C:/dumps生成mini dump文件。

也可以用:

Adplus –crash –pn test.exe –fullonfirst –o C:/dumps

差别在于,加上-fullonfirst参数后,无论是1st chance exception还是2nd chance exception,都会生成full dump文件。

假如test.exe发生deadlock,或者memory leak,并不是crash,需要获取任意时刻的一个dump,可以用下面的命令:

Adplus –hang –pn test.exe –o C:/dumps

该命令立刻把test.exe的full dump 抓到C:/dumps下。

Adplus更灵活的方法就是用-c参数带配置文件。在配置文件里面,可以选择exception发生的时间,生成的dump是mini dump还是full dump,还可以设定断点等等。对于adplus各项参数的选用原则,在最后一章还会作进一步介绍。

案例分析:华生医生(Dr. Watson)在什么情况下不能记录Dump文件

问题描述

客户声称用VC开发的程序偶尔会崩溃。为了获取详细信息,客户激活了Dr. Watson,以便程序崩溃的时候可以自动获取dump文件。但是问题再次发生后,Dr. Watson并没有记录dump文件。

背景知识

dump文件包含的是内存镜像信息。在Windows系统上,dump文件分为内核dump和用户态dump两种。前者一般用来分析内核相关的问题,比如驱动程序;后者一般用来分析用户态程序的问题。如果不作说明,本书后面所指的dump都表示用户态dump。用户态的dump又分成mini dump和full dump。前者尺寸小,只记录一些常用信息;后者则是把目标进程用户态的所有内容都记录下来。Windows提供了MiniDumpWriteDump API可供程序调用来生成mini dump。通过调试器和相关工具,可以抓取目标程序的full dump。拿到dump后,可以通过调试器检查dump中的内容,比如call stack,memory,exception等等。关于dump和调试器的更详细信息,后面会有更多介绍。跟Dr. Watson相关的文档是:

Description of the Dr. Watson for Windows (Drwtsn32.exe) Tool

http://support.microsoft.com/?id=308538

Specifying the Debugger for Unhandled User Mode Exceptions

http://support.microsoft.com/?id=121434

INFO: Choosing the Debugger That the System Will Spawn

http://support.microsoft.com/?id=103861

也就是说,通过设定注册表中的AeDebug项,可以在程序崩溃后,选择调试器进行调试。选择Dr. Watson就可以直接生成dump文件。

问题分析

回到这个问题,客户并没有获取到dump文件,可能性有两个:

1.         Dr. Watson工作不正常。

2.         客户的程序根本没有崩溃,不过是正常退出而已。

为了测试第1点,提供了如下的代码给客户测试:

int *p=0;

*p=0;

测试上面的代码,Dr. Watson成功地获取了dump文件。也就是说,Dr. Watson工作是正常的。那看来客户声称的崩溃可能并不是unhandled exception导致的。说不定在非预料情况下调用了ExitProcess,被客户误认为是崩溃。所以,抓取信息不应该局限于unhandled exception,而应该检查进程退出的原因。

当程序在Windbg调试器中退出的时候,系统会触发调试器的进程退出消息,可以在这个时候抓取dump来分析进程退出的原因。

如果让客户每次都先启动Windbg,然后用Windbg启动程序,操作起来很复杂。最好有一个自动的方法。Windows提供了让指定程序随调试器启动的选项。设定注册表后,当设定的进程启动的时候,系统先启动指定的调试器,然后把目标进程的地址和命令行作为参数传递给调试器,调试器再启动目标进程调试。这个选项在无法手动从调试器中启动程序的时候特别有用,比如调试先于用户登录而启动Windows Service程序,就必须使用这个方法:

How to debug Windows services

http://support.microsoft.com/?kbid=824344

有趣的是,好多恶意程序也通过这个方法来达到加载进程的目的。很多人把这个方法叫做IFEO 劫持(Image File Execution Option Hacking)。

在Windbg目录下,有一个叫做adplus.vbs的脚本可以方便地调用Windbg来获取dump文件。所以这里可以借用这个脚本:

How to use ADPlus to troubleshoot "hangs" and "crashes"

http://support.microsoft.com/kb/286350/EN-US/

脚本的详细说明可以参考adplus /?的帮助。

新的做法

结合上面的信息,具体做法是:

1.         在客户机器的Image File Execution Options注册表下面创建跟问题程序同名的键。

2.         在这个键的下面创建Debugger字符串类型子键。

3.         设定Debugger= C:/Debuggers/autodump.bat。

4.         编辑C:/Debuggers/autodump.bat文件的内容为如下:

cscript.exe C:/Debuggers/adplus.vbs -crash -o C:/dumps -quiet -sc %1

通过上面的设置,当程序启动的时候,系统自动运行cscript.exe来执行adplus.vbs脚本。Adplus.vbs脚本的-sc参数指定需要启动的目标进程路径(路径作为参数又系统传入,bat文件中的%1代表这个参数),-crash参数表示监视进程退出,-o参数指定dump文件路径,-quiet参数取消额外的提示。可以用notepad.exe作为小白鼠做一个实验,看看关闭notepad.exe的时候,是否有dump产生。

根据上面的设定,问题再次发生后,C:/dumps目录生成了两个dump文件。文件名分别是:

PID-0__Spawned0__1st_chance_Process_Shut_Down__full_178C_DateTime_0928.dmp

PID-0__Spawned0__2nd_chance_CPlusPlusEH__full_178C_2006-06-21_DateTime_0928.dmp

注意看第二个的名字,这个名字表示发生2nd chance的C++ exception!打开这个dump后找到了对应的call stack,发现的确是客户忘记了catch潜在的C++异常。修改代码添加对应的catch后,问题解决。

问题解决了,可是为什么华生医生(Dr. Watson)抓不到dump呢

当然疑问并没有随着问题的解决而结束。既然是unhandled exception导致的crash,为什么Dr. Watson抓不到呢?首先创建两个不同的程序来测试Dr. Watson的行为:

int _tmain(int argc, _TCHAR* argv[])

{

  throw 1; 

  return 0;

}

int _tmain(int argc, _TCHAR* argv[])

{

  int *p=0;

  *p=0;

  return 0;

}

果然,对于第一个程序,Dr. Watson并没有保存dump文件。对于第二个,Dr. Watson工作正常。看来的确跟异常类型相关。

仔细回忆一下。当AeDebug下的Auto设定为0的时候,系统会弹出前面提到的红色框框。对于上面这两个程序,框框的内容是不一样的。

在我这里,看到的对话框分别是(对话框出现的时候用Ctrl+C保存的信息):

---------------------------

Microsoft Visual C++ Debug Library

---------------------------

Debug Error!

Program: d:/xiongli/today/exceptioninject/debug/exceptioninject.exe

This application has requested the Runtime to terminate it in an unusual way.

Please contact the application's support team for more information.

(Press Retry to debug the application)

---------------------------

Abort   Retry   Ignore  

---------------------------

---------------------------

exceptioninject.exe - Application Error

---------------------------

The instruction at "0x00411908" referenced memory at "0x00000000". The memory could not be "written".

Click on OK to terminate the program

Click on CANCEL to debug the program

---------------------------

OK   Cancel  

---------------------------

两者行为完全不一样!如果做更多的测试,会发现对话框的细节还跟编译模式release/debug 相关。

程序可以通过SetUnhandledExceptionFilter函数来修改unhanded exception的默认处理函数。这里,C++运行库在初始化CRT(C Runtime)的时候,传入了CRT的处理函数 (msvcrt!CxxUnhandledExceptionFilter)。如果发生unhandled exception,该函数会判断异常的号码,如果是C++异常,就会弹出第一个对话框,否则就交给系统默认的处理函数(kernel32!UnhandledExceptionFilter)处理。第一种情况的call stack 如下:

USER32!MessageBoxA

MSVCR80D!__crtMessageBoxA

MSVCR80D!__crtMessageWindowA

MSVCR80D!_VCrtDbgReportA

MSVCR80D!_CrtDbgReportV

MSVCR80D!_CrtDbgReport

MSVCR80D!_NMSG_WRITE

MSVCR80D!abort

MSVCR80D!terminate

MSVCR80D!__CxxUnhandledExceptionFilter

kernel32!UnhandledExceptionFilter

MSVCR80D!_XcptFilter

第二种情况CRT交给系统处理。Callstack如下:

ntdll!KiFastSystemCallRet

ntdll!ZwRaiseHardError+0xc

kernel32!UnhandledExceptionFilter+0x4b4

release_crash!_XcptFilter+0x2e

release_crash!mainCRTStartup+0x1aa

release_crash!_except_handler3+0x61

ntdll!ExecuteHandler2+0x26

ntdll!ExecuteHandler+0x24

ntdll!KiUserExceptionDispatcher+0xe

release_crash!main+0x28

release_crash!mainCRTStartup+0x170

kernel32!BaseProcessStart+0x23

详细的信息可以参考:

SetUnhandledExceptionFilter

http://msdn.microsoft.com/library/default.asp?url=/library/en-us/debug/base/setunhandledexceptionfilter.asp

UnhandledExceptionFilter

http://msdn.microsoft.com/library/default.asp?url=/library/en-us/debug/base/unhandledexceptionfilter.asp

上面观察到的信息能解释Dr. Watson的行为吗?看起来似乎有关系。为了进一步确认这个问题,可以通过下面的测试,使用Windbg代替Dr. Watson,看看是否可以获取dump。如果仅仅换一个调试器就可以获取dump,那说明问题是跟调试器相关,跟程序抛出的异常无关。具体做法是:

1.         运行drwtsn32.exe –i注册Dr. Watson。

2.         打开AeDebug注册表,找到Debugger项,里面应该是drwtsn32 -p %ld -e %ld -g。

3.         修改Debugger为: C:/debuggers/windbg.exe -p %ld -e %ld -c ".dump /mfh C:/myfile.dmp ;q"。

当unhanded exception发生后,系统会启动windbg.exe作为调试器加载到目标进程。但是windbg.exe不会自动获取dump,所以需要用-c参数来指定初始命令。命令之间可以用分开分割。这里的.dump /mfh C:/myfile.dmp命令就是用来生成dump文件的。接下来的q命令是让windbg.exe在dump生成完毕后自动退出。用这个方法,对于unhandled C++ exception,windbg.exe是可以获取dump文件的。所以我认为Dr. Watson这个工具在获取dump的时候是有缺陷的。研究的发现在:

    http://eparg.spaces.msn.com/blog/cns!59BFC22C0E7E1A76!1213.entry

2.3.3  通知(Debug Event)是操作系统跟调试器交流的一种方法

通知,也叫做调试信息(Debug Events),是操作系统在某些事件发生的时候,通知调试器的一个手段。跟异常处理相似,操作系统在某些事件发生的时候,会检查当前进程是否有调试器加载。如果有,就会给调试器发送对应的消息,以便使用调试器进行观察。跟异常不一样的地方就是,只有调试器才会得到通知,应用程序本身是得不到的。同时调试器得到通知后不需要做什么处理,没有1st /2nd chance的差别。在Windbg帮助文件的Controlling Exceptions and Events主题里面,可以看到关于通知的所有代号。常见的通知有:DLL的加载、卸载,线程的创建、退出等。

案例分析:VB6的版本问题

客户用VB6开发的程序,在VB6 IDE调试的时候无法访问Access 2003创建的数据库,访问Access 97的数据库却是好的。如果换一台开发机,测试就一切正常。

这个问题的思路非常简单,既然只有一台机器有问题,说明是环境的原因。既然访问Access 97没问题,或许跟Access客户端文件,也就是DAO的版本有关。通过工具Windbg目录下的tlist工具检查进程中加载的DLL,发现有问题的机器加载的是dao350.dll,没有问题的机器加载的是dao360.dll。下一步就需要知道为什么加载的是dao350.dll?

DAO是一个COM对象,很有可能是通过COM对象加载的方法完成的。那么,可以采取1.2节中ShellExecute同时打开两个文件的处理方法,从创建COM的API: CoCreateInstanceEx开始,用wt命令跟踪整个函数的执行,保存下来后比较两种不同情况的异同。通过这个方法肯定是可以找出原因的,不过要想用wt命令一直跟踪到LoadLibrary函数加载这个DLL,可能需要执行一整天。所以,应该找一个可操作性更强一点的方法来检查。既然最后要追踪到LoadLibrary为止,那何不在这个函数上设置断点,观察检查DAO350.DLL加载起来的情况?

在LoadLibrary上设定断点并不是一个很好的方法。因为:

1.         加载DLL不一定要调用LoadLibrary的。可以直接调用Native API,比如ntdll!LdrLoadDll。

2.         假设有几十个DLL要加载,如果每次LoadLibrary都断下来,操作起来也是很麻烦的事情。虽然可以通过条件断点判断LoadLibrary的参数来决定是否断下来,但是设定条件断点也是很麻烦的。

最好的方法,就是使用通知,在moudle load的时候,系统给调试器发送通知。由于Windbg在收到moudle load通知的时候,可以使用通配符来判断 DLL的名字,操作起来就简单多了。首先,在Windbg中用sxe ld:dao*.dll设置截获Moudle Load的通知,当文件名是dao*.dll的时候,Windbg就会停下来。(关于Windbg的详细信息,以及这里使用到的命令,后面都有章节详细介绍)。看到的结果就是:

0:008> sxe ld:dao*.dll

ModLoad: 1b740000 1b7c8000   C:/Program Files/Common Files/Microsoft Shared/DAO/DAO360.DLL

eax=00000001 ebx=00000000 ecx=0013e301 edx=00000000 esi=7ffdf000 edi=20000000

eip=7c82ed54 esp=0013e300 ebp=0013e344 iopl=0         nv up ei pl zr na po nc

cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246

ntdll!KiFastSystemCallRet:

7c82ed54 c3               ret

ntdll!KiFastSystemCallRet                            

ntdll!NtMapViewOfSection                             

ntdll!LdrpMapViewOfDllSection                        

ntdll!LdrpMapDll                                     

ntdll!LdrpLoadDll                                    

ntdll!LdrLoadDll                                     

0013e9c4 776ab4d0 0013ea40 00000000 00000008 kernel32!LoadLibraryExW

ole32!CClassCache::CDllPathEntry::LoadDll            

ole32!CClassCache::CDllPathEntry::Create_rl          

ole32!CClassCache::CClassEntry::CreateDllClassEntry_rl

ole32!CClassCache::GetClassObjectActivator           

ole32!CClassCache::GetClassObject                    

ole32!CServerContextActivator::GetClassObject        

ole32!ActivationPropertiesIn::DelegateGetClassObject 

ole32!CApartmentActivator::GetClassObject            

ole32!CProcessActivator::GCOCallback                 

ole32!CProcessActivator::AttemptActivation           

ole32!CProcessActivator::ActivateByContext           

ole32!CProcessActivator::GetClassObject              

ole32!ActivationPropertiesIn::DelegateGetClassObject 

ole32!CClientContextActivator::GetClassObject        

ole32!ActivationPropertiesIn::DelegateGetClassObject 

ole32!ICoGetClassObject                              

ole32!CComActivator::DoGetClassObject                

ole32!CoGetClassObject                               

VB6!VBCoGetClassObject                               

VB6!_DBErrCreateDao36DBEngine                        

通过检查LoadLibraryExW的参数,可以看到:

0:000> du 0013ea40

0013ea40  "C:/Program Files/Common Files/Mi"

0013ea80  "crosoft Shared/DAO/DAO360.DLL"

从上面的信息可以看到:

1.         DAO360不是通过CoCreateInstanceEx加载进来的,而是另外一个COM API: CoGetClassObject。所以如果对CoCreateInstanceEx做想当然的跟踪,就浪费时间了。

2.         COM调用的发起者是VB6!_DBErrCreateDao36DBEngine这个函数。应该仔细检查这个函数。

有了前面DLL HELL 案例的教训,在检查这个函数前,首先检查VB6.EXE的版本。发现正常情况下的版本是6.00.9782,有问题的机器上的版本是6.00.8176。 在有问题的机器上安装Visual Studio 6,SP6升级VB6版本后,问题解决。

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"

http://support.microsoft.com/kb/286350/

未处理异常发生后的主动退出

在某些特殊情况下,程序员为了需要,会在发生未处理异常后主动退出,而不是等到崩溃被动发生。使用这种技术的有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是否调用取决于系统内核的判断。用户态的调试器要想改变这个行为,要破费一番脑筋了。

 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值