定位Release程序崩溃原因 SetUnhandledExceptionFilter + StackWalker

(一) 发生异常时系统的处理顺序(by Jeremy Gordon, Hume): 

     1.系统首先判断异常是否应发送给目标程序的异常处理例程,如果决定应该发送,并且目标程序正在被调试,则系统 
     挂起程序并向调试器发送EXCEPTION_DEBUG_EVENT消息.

     2.如果你的程序没有被调试或者调试器未能处理异常,系统就会继续查找你是否安装了线程相关的异常处理例程,如果 
     你安装了线程相关的异常处理例程,系统就把异常发送给你的程序seh处理例程,交由其处理. 

     3.每个线程相关的异常处理例程可以处理或者不处理这个异常,如果他不处理并且安装了多个线程相关的异常处理例程, 
         可交由链起来的其他例程处理. 

     4.如果这些例程均选择不处理异常,如果程序处于被调试状态,操作系统仍会再次挂起程序通知debugger. 

     5.如果程序未处于被调试状态或者debugger没有能够处理,并且你调用SetUnhandledExceptionFilter安装了最后异 
     常处理例程的话,系统转向对它的调用. 

     6.如果你没有安装最后异常处理例程或者他没有处理这个异常,系统会调用默认的系统处理程序,通常显示一个对话框, 
     你可以选择关闭或者最后将其附加到调试器上的调试按钮.如果没有调试器能被附加于其上或者调试器也处理不了,系统 
     就调用ExitProcess终结程序. 

     7.不过在终结之前,系统仍然对发生异常的线程异常处理句柄来一次展开,这是线程异常处理例程最后清理的机会. 


(二)SetUnhandledExceptionFilter + StackWalker

    这个方案需要自己动手往工程里添加代码了。要实现上面的想法,需要做两件事情:1、需要在crash时有机会对程序堆栈进行处理;2、对堆栈信息进行收集。

    1、SetUnhandleExceptionFilter函数

    Windows平台下的C++程序异常通常可分为两种:结构化异常(Structured Exception,可以理解为与操作系统相关的异常)和C++异常。对于结构化异常处理(SEH),可以找到很多资料,在此不细说。对于crash错 误,一般由未被正常捕获的异常引起,Windows操作系统提供了一个API函数可以在程序crash之前有机会处理这些异常,就是 SetUnhandleExceptionFilter函数。(C++也有一个类似函数set_terminate可以处理未被捕获的C++异常。)

    SetUnhandleExceptionFilter函数声明如下:

    LPTOP_LEVEL_EXCEPTION_FILTER WINAPI SetUnhandledExceptionFilter(
      __in          LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter
    );

    其中 LPTOP_LEVEL_EXCEPTION_FILTER 定义如下:

    typedef LONG (WINAPI *PTOP_LEVEL_EXCEPTION_FILTER)(
        __in struct _EXCEPTION_POINTERS *ExceptionInfo
    );
    typedef PTOP_LEVEL_EXCEPTION_FILTER LPTOP_LEVEL_EXCEPTION_FILTER;

    简单来说,SetUnhandleExceptionFilter允许我们设置一个自己的函数作为全局SEH过滤函数,当程序crash前会调用我们的函 数进行处理。我们可以利用的是 _EXCEPTION_POINTERS 结构类型的变量ExceptionInfo,它包含了对异常的描述以及发生异常的线程状态,过滤函数可以通过返回不同的值来让系统继续运行或退出应用程 序。

    关于 SetUnhandleExceptionFilter 函数的具体用法和示例请参考MSDN。

    2、StackWalker
    现在我们已经有机会可以在crash之前对程序状态信息进行处理了,只需要生成并保存堆栈信息就大功告成了。Windows的dbghelp.dll库提 供了一个函数可以得到当前堆栈信息:StackWalk64(在Win2K以前版本中为StackWalk)。该函数声明如下:

    BOOL WINAPI StackWalk64(
      __in          DWORD MachineType,
      __in          HANDLE hProcess,
      __in          HANDLE hThread,
      __in_out      LPSTACKFRAME64 StackFrame,
      __in_out      PVOID ContextRecord,
      __in          PREAD_PROCESS_MEMORY_ROUTINE64 ReadMemoryRoutine,
      __in          PFUNCTION_TABLE_ACCESS_ROUTINE64 FunctionTableAccessRoutine,
      __in          PGET_MODULE_BASE_ROUTINE64 GetModuleBaseRoutine,
      __in          PTRANSLATE_ADDRESS_ROUTINE64 TranslateAddress
    );
    该函数的具体用法可以参考MSDN。在这里推荐一个牛人写好的StackWalker,可以直接拿来用,开源的。StackWalker提供了一个基类, 给出了几个简单的接口,可以方便地生成堆栈信息,并且支持一系列VC版本,非常好用。我们可以自己写一个子类,并重载虚函数OnOutput,就可以将堆 栈信息输出为特定格式了。StackWalker的地址为:http://www.codeproject.com/KB/threads/StackWalker.aspx。

    不过对于Release版本来说,StackWalk64函数获得的堆栈信息有可能不完整。如果异常是由MFC的模块抛出,那么获得的堆栈可能缺少前面调 用模块信息。另外,StackWalk64需要最新的dbghelp.dll文件支持才能工作;要正确输出crash的函数名和行号,需要要pdb文件支 持。以上不足有可能影响输出信息的完整性和效果,而对于发布在外的程序,要带上pdb文件几乎不可能,因此这个方案还是有缺憾的,比较适用于本地的 release版本调试。

当我们把自己的release版本程序发布出去以后,一般都是在用户的机器上运行。这种情况下,对于第四种方案,因为需要pdb文件才能够正确生成 堆栈调用的函数行号及代码行号,因此方案四只适用于本地release版的调试,否则只能生成不完整的堆栈信息。对于前三种方案,其实只需要用户告知崩溃 地址,然后在本地查找crash地址就可以了,但是定位crash的过程非常不方便,如果crash的情况比较多,前三种方案都不合适。而且,前三种方案 均不能生成堆栈调用信息,对于debug的作用有限。

    下面我们就来看一个更加完善的解决方案。

    方案五:SetUnhandledExceptionFilter + Minidump

    SetUnhandleExceptionFilter函数我们已经介绍过了,本方案的思路还是要利用我们自己的异常处理函数,来生成minidump文件。

    1、Minidump概念

    minidump(小存储器转储)可以理解为一个dump文件,里面记录了能够帮助调试crash的最小有用信息。实际上,如果你在 系统属性 -> 高级 -> 启动和故障恢复 -> 设置 -> 写入调试信息 中选择“小内存转储(64 KB)”的话,当系统意外停止时都会在C:\Windows\Minidump\路径下生成一个.dmp后缀的文件,这个文件就是minidump文件, 只不过这个是内核态的minidump。

   我们要生成的是用户态的minidump,文件中包含了程序运行的模块信息、线程信息、堆栈调用信息等。而且为了符合其mini的特性,dump文件是压缩过的。

    2、生成minidump文件

    生成minidump文件的API函数是MiniDumpWriteDump,该函数需要dbghelp.lib支持,其原型如下:

    BOOL WINAPI MiniDumpWriteDump(
      __in          HANDLE hProcess,
      __in          DWORD ProcessId,
      __in          HANDLE hFile,
      __in          MINIDUMP_TYPE DumpType,
      __in          PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam,
      __in          PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam,
      __in          PMINIDUMP_CALLBACK_INFORMATION CallbackParam
    );

    在我们的异常处理函数中加入以下代码:

    HANDLE hFile = ::CreateFile( _T("E:\\dumpfile.dmp"), GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
     if( hFile != INVALID_HANDLE_VALUE)
     {
         MINIDUMP_EXCEPTION_INFORMATION einfo;
         einfo.ThreadId = ::GetCurrentThreadId();
         einfo.ExceptionPointers = pExInfo;
         einfo.ClientPointers = FALSE;

        ::MiniDumpWriteDump(::GetCurrentProcess(), ::GetCurrentProcessId(), hFile, MiniDumpNormal, &einfo, NULL, NULL);
        ::CloseHandle(hFile);
     }

    其中,pExInfo变量为异常处理函数PEXCEPTION_POINTERS类型的参数。具体请参考MSDN。

    3、调试minidump

    调试dump文件首先需要pdb文件,因此我们build程序时需要设置 Debug Infomation Format 为 “Program Database(/Zi)”。其次,我们还要确保所用的dump文件与源代码、exe、pdb文件版本是一致的,这要求我们必须维护好程序版本信息。

    调试minidump最方便的环境就是VS了,我们只要将.dmp、.exe、.pdb文件放在一个路径下,保证源代码文件的路径与编译时的路径一致就可 以了,剩下的就是VS帮我们完成。双击.dmp文件或者在文件打开工程中选择“dump files”,加载dump文件,然后按F5运行就能直接恢复crash时的现场了,你可以定位crash的代码,可以查看调用堆栈,可以查看线程和模块 信息...一切都跟你设置断点调试一样,太强大了!看个截图吧。

定位Release程序崩溃原因 SetUnhandledExceptionFilter + StackWalker_第1张图片

    需要注意的是,对于release版的程序来说,很多代码是经过编译器优化过的,因此定位的时候可能会有所偏差,大家可以考虑设置选项去掉代码优化。

    其他可以调试minidump的工具还有WinDbg等,大家可以查阅相关资料。

    本文主要参考了这篇文章:http://vicchina.51.net/research/other/seh/minidumps/intro.htm。

上一篇我们已经给出了方案,能够非常方便的通过dump文件对crash错误进行调试和定位;从整个流程上看还差最后一步,即怎样拿到crash时 产生的dump文件。如果可以让用户把文件发送过来自然不错,但对于类似免费共享软件等在互联网上发布的程序呢?我们的用户是不确定的,而且用户量有可能 非常大,即使我们能想办法联系到用户,总不能挨个去收集crash信息吧。

    我们需要一种方案,能够提供crash信息汇报功能。

    我们可以架设一台服务器专门进行信息收集,只要客户端在crash时正确汇报即可,但是相应的维护成本和开发难度也不可忽视。有没有更简单的方法呢?还记得我的博文“为程序添加自动发送Email功能”吗?这就是简单有效的方法!

    方案六:minidump + email

    我们只需要在异常处理时,先生成minidump信息文件,再用email方式将文件发送到指定邮箱就行了。剩下的就是我们每天查看邮箱,提取dump文件进行调试了。

    1、Email功能

    首先我们来看一下email发送都需要哪些相关信息。

    a、发送端邮箱帐户;

    b、接收端邮箱帐户;

    c、email标题,一般应有软件名称及版本信息;

    d、email正文,一般应有简单的crash信息提示,以区别不同原因造成的crash;

    e、email附件,当然就是我们的dump文件了,还可以加上软件生成的log文件等。

    当然,对于标题应该尽量多加一些信息区别引起crash的原因,比如将crash的地址信息加到标题中;因为当每天有成百上千的crash汇报上来,重复 的crash占大多数,把时间都花在区分它们身上有点太浪费。由此看来,前面方案中提到的StackWalker还是有些用处的,我们可以用它来生成一些 crash的文字描述信息,写到标题或正文中去。

    dump文件的大小是否适合作为邮件的附件呢?实际上minidump产生的文件一般在几K到几十K之间,作为email的附件没有任何问题。

    关于发送email相关技术细节,已经在“为程序添加自动发送Email功能”文中介绍了,大家可以参考。其实,对接受邮箱中邮件的处理还是很费时费力的,大家可以考虑写一些脚本将处理流程自动化,提高效率。

    2、google breakpad

    google breakpad是一个开源的跨平台crash report系统,光从开源和跨平台这两个特点上来看,它就足以称的上是一个完善而有效的工具了。其实,breakpad在整个crash report层次上给出了一个系统级的解决方案,也就是说它几乎能适应各种软件、各种平台的应用要求。

    breakpad的整体思路跟上面介绍的方案是相似的,只不过最后提交dump文件的方式更加完善。大家有兴趣可以去它的官方网址查阅相关资料:http://code.google.com/p/google-breakpad/。

    ok,关于调试release发布程序的crash错误系列文章就写完了。这几篇文章给出的方案由简单到复杂,由简陋到完善,对crash调试有了一个比较全面的总结。当然,其中涉及到的概念和技术还很多,需要我们去不断学习和领悟,也希望大家能够互相交流。


原文地址:http://blog.sina.com.cn/s/blog_648d306d0100qmca.html

你可能感兴趣的:(定位Release程序崩溃原因 SetUnhandledExceptionFilter + StackWalker)