原文:
http://www.cnblogs.com/yangchaobj/archive/2013/02/23/2923416.html
http://www.cnblogs.com/yangchaobj/archive/2013/02/23.html
最近半年单位的项目集中上线,出了一些 crash 的问题。
程序crash 后,会自动生成一个 dump 文件,这是通过调用系统API MiniDumpWriteDump 实现的。
通过事后对 dump 文件的分析,工程师可以找到出问题的点。
不方便的是,每次打开dump文件后,堆栈总是停在类似 ntdll!KiFastSystemCallRet 的地方,通过这个 callstack 完全找不到任何程序的蛛丝马迹。而是用 .ecxr 则报错,说没有 ContextRecord. 工程师需要执行 windbg 的 s 命令,在堆栈中寻找 ExceptionContext 去恢复现场,对于做过的人稍有些麻烦,对于生手就比较困难了。
*具体搜索思路是:根据ContextRecord 的定义,第一个成员是异常号,比如 0xc0000005,那就在堆栈上搜这个异常号:s -d esp L1000 0xc0000005
如果是 x64平台,用 s -d rsp L1000 0xc0000005
一般会找到2-3个匹配的堆栈地址,再逐个去试,就很容易找到。
还有一个问题,有时候 dump 文件可以生成,有时候程序直接退出,没有任何 dump。
基于这些现象,我有了一些问题:
1. 能否不用每次手动寻找 ExceptionContext ,而让 windbg 直接重现现场。
2. 能否保证所有的异常都被捕获并生成 dump (当然是在外围条件都满足的情况下)
由这个 Dump 文件引发了我一系列的研究和学习。现在将成果总结如下。
在windbg 定位的时候,需要用到 .cxr 指令,通过查windbg帮助手册,了解到这个指令是 Display Context Record 的缩写。在异常发生时,windows 会将异常发生时的各个寄存器数值、堆栈位置等记录下来,存在一个 ContextRecord 结构里,并把这个结构压入堆栈。.cxr 命令还带一个地址参数,用来指定 ContextRecord 的位置。这个结构对于能否恢复堆栈起着决定性的作用。
通过读《Windows核心编程(第5版)》,了解到windows会在异常发生时,向堆栈上压入3个结构体:ExceptionRecord, ContextRecord, EXCEPTION_POINTERS。
只要生成 dump 文件时这三个结构体还没有出栈,则通过搜索堆栈是一定可以找到ContextRecord,就一定可以恢复堆栈。
但为什么每次要手动搜索 ContextRecord 呢?经过研究MiniDumpWriteDump,有了答案:
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
);
第5个参数 ExceptionParam,是一个结构地址,这个结构的定义如下:
typedef struct _MINIDUMP_EXCEPTION_INFORMATION {
DWORD ThreadId;
PEXCEPTION_POINTERS ExceptionPointers;
BOOL ClientPointers;
} MINIDUMP_EXCEPTION_INFORMATION, *PMINIDUMP_EXCEPTION_INFORMATION;
结构体的第二个参数就是 EXCEPTION_POINTERS,而这个指针的第二个成员就是 ContextRecord。
之前生成 dump 的时候,并没有将这个结构填好传给 MiniDumpWriteDump,所以才造成直接用 .ecxr 命令找不到 ContextRecord 的错误。
通过修改代码,在异常发生时将 EXCEPTION_POINTER 保存下来(如果能确保生成 dump 时,堆栈上的 ContextRecord 依然有效,则仅保留EXCEPTION_POINTER的地址即可)。
调用 MiniDumpWriteDump 时将这个 EXCEPTION_POINTER 给传进去,就可以做到 .ecxr 指令自动恢复堆栈定位错误行了。
问题:有些异常可以捕获,生成 dump,有些则不能。
要解决这个问题,必须对 windows 和 C++ 的异常机制有一个了解。
平时我们写程序都这样捕获异常:
1 try 2 { 3 fire_access_violation(); 4 } 5 catch (exception& ex) 6 { 7 // ...... 8 } 9 catch (...) 10 { 11 // ...... 12 }
第3行的函数会触发 ACCESS_VIOLATION (0xC0000005) 异常,第5行用来catch 普通的 C++异常,第9行用来捕获其他异常,ACCESS_VIOLATION 就属于这种异常。
但运行后发现,第9行的 catch (...) 什么也抓不到。要让它起作用,还需要调整默认的编译参数,找到 C++ -> Code Generation -> Enable C++ Exceptions 选项,设为 Yes with SEH Exceptions (/EHa),即通过SEH机制,让C++的 catch (...) 能够捕获所有的异常。
这一步做下来,我们已经可以捕获try 块上的所有异常了。但虽然捕获了异常,我们还无法知道异常类型。也没有前文所说的 ContextRecord 信息。要更精确的得到这些信息,还需要多做一步:把 SEH 映射成C++异常。
C 提供了一个函数 _set_se_translator
_se_translator_function _set_se_translator( _se_translator_function seTransFunction ); typedef void (*_se_translator_function)(unsigned int, struct _EXCEPTION_POINTERS* );
这个函数要尽量早的注册到系统,所以在线程一开始的时候,就要调用这个函数。
它给本线程设置一个异常转换函数,当有 SEH 异常发生时,会调到用户注册的 _se_translator_function,传入 exception code 和 EXCEPTION_POINTERS,这时我们需要自己生成一个结构体,把这些非常有用的信息存在结构体里。还是那句话:如果能确保异常处理还在本堆栈上,并且异常信息还没有出栈,则存一个指针即可,否则要把 EXCEPTION_POINTERS 的整个结构复制过来,在遇到嵌套异常时,他的成员 ExceptionRecord 有可能是一个链表,如何保存这个链表还是件比较麻烦的事儿,所以能在原地处理异常时最好!
转换函数看起来可以是这样:
1 struct StructuredException : public exception 2 { 3 PEXCEPTION_POINTERS exp_ptr; 4 5 StructuredException() : exception() {} 6 StructuredException(const char* text) : exception(text) {} 7 }; 8 9 void trans_func(unsigned int code, EXCEPTION_POINTERS* p) 10 { 11 cout << "Exception Record: " << p->ExceptionRecord << endl; 12 StructuredException se; 13 se.exp_ptr = p; 14 throw se; 15 }
StructuredException 从 C++ 异常 exception 继承。只要安装了 tranlator 的线程内发生了结构化异常,windows 就会在该线程上调用次函数。
最后一句即第14行,抛出StructuredException,即抛出一个C++异常。通过 catch (exception& ex) 就可以捕获它。当然为了区别处理结构化异常,也可以这也写 catch (StructuredException& se) {...},毕竟结构化异常的出现时超出预期的。
到了这一步,可以保证每个线程都在我们的保护之下。但还有一些线程不是我们自己写的,可能是一些库自己创建的,这些线程有可能没有try块的保护,如果这些线程发生了异常,还是会造成程序悄悄退出,没有 dump。windows 也提供了方法来抓住这种漏网之鱼。
Windows 提供了 UnhandledExceptionFilter 来处理那些没被 catch 住的异常。
1 LPTOP_LEVEL_EXCEPTION_FILTER WINAPI SetUnhandledExceptionFilter( 2 __in LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter 3 ); 4 5 LONG WINAPI UnhandledExceptionFilter( 6 __in struct _EXCEPTION_POINTERS* ExceptionInfo 7 ); 8 9 LONG WINAPI unhandled_exception_filter(__in struct _EXCEPTION_POINTERS *ExceptionInfo) 10 { 11 // do something 12 return EXCEPTION_CONTINUE_SEARCH; // EXCEPTION_EXECUTE_HANDLER 13 }
整个进程设置一个 filter 就可以拦住进程内任何线程抛出的漏网的异常。如果你用 debugger 调试 filter 代码,会发现它从来不会被调到,因为异常已经被 debugger 拦下来了,于是就不是“漏网之鱼”,所以这个 filter 不会被调到。
可以看到 filter 的参数里也有 EXCEPTION_POINTERS,所以在这个 filter 函数里我们可以调用 MiniDumpWriteDump 生成我们需要的 dump 文件。filter被调到的线程就是发生异常的线程。
做到这一步,进程内几乎所有的异常都会被我们捕获到并通过 mini dump 文件把现场保存下来以供事后分析。
为什么说是“几乎所有”呢,还是有一些特殊情况下的异常无法被捕获到:
当遇到 Heap Corrupt 异常时(两次 delete 同一块内存地址就会出现这种异常),windows有两种处理策略:杀掉进程或者不杀。在64位的 windows server 下,当堆发生错误时,系统会直接杀死进程,不会有全局展开,异常捕获函数什么都不活不到,这种异常代码是:0xc0000374,这时需要外挂 windbg 或 cdb 来帮助我们 dump。
如果 mini dump 的参数设的过于“求全”,既要保存堆栈信息又要保持堆信息,系统很可能没有足够的硬盘空间来保持 dump 文件,这样 mini dump 也会失败。
但无论如何,只要进程非正常退出,总会在系统事件中记上一笔,我们在 Event Viewer 里就可以看到。
总结:
通过 /Eha 编译参数,我们可以既捕获C++异常,又捕获结构化异常。
通过 _set_se_tranlator 可以把结构化异常转换成C++异常,并保存异常发生的现场(需要 /Eha 参数)。
通过 SetUnhandledExceptionFilter,可以捕获没有 try-catch/__try __except 保护的线程上抛出的异常。
通过 MiniDumpWriteDump 加上我们保存的异常现场信息,我们可以在生成 mini dump 时把 进程id、线程id、发生异常的时间、ContextRecord 都写进 mini dump(前三项通过文件名的形式),极大的方便后期调试。
通过以上努力,可以保证绝大多数异常都会被捕获并保存下现场信息。
相关推荐
总结一下,Win32结构化异常处理是一个强大的错误处理机制,它由操作系统提供服务,但通过编译器的扩展关键字进行封装。理解SEH的基本概念和工作原理对于编写健壮的Windows应用程序至关重要。尽管C++的异常处理和SEH...
总结来说,Win32异常处理是Windows平台上确保程序可靠性和容错能力的关键技术。通过理解和掌握这一机制,开发者可以编写出更加健壮的软件,有效地应对运行时可能出现的各种异常情况。在实际项目中,结合使用C++异常...
总结,Windows下入侵检测系统的研究与设计——检测模块设计,涉及到网络监控、数据处理、异常检测算法、系统集成等多个技术领域。开发者需要具备扎实的网络基础知识,理解Windows系统内部机制,并掌握编程技能,才能...
SEH(Structured Exception Handling,结构化异常处理)是Windows操作系统提供的一种强大的错误处理机制,它允许开发者在程序出现异常时进行适当的处理,从而避免程序意外崩溃。在高级语言如Visual C++中,通过`_try...
本节介绍了一些高级工具和技术,帮助开发者和研究人员更深入地了解Windows的内部机制。 ###### 1.3.1 可靠性和性能监视器 可靠性与性能监视器是一个强大的工具,用于监控和分析系统的运行状况和性能。 ###### ...
接着,转向C++程序员面试宝典,这部分内容通常涵盖C++语言的基础、标准库的使用、面向对象编程原则、模板、异常处理、内存管理以及STL(标准模板库)等。面试者需要了解C++的关键特性,例如构造函数与析构函数、继承...
深入研究Windows内部原理系列之二:Windows体系结构 在探讨Windows体系结构时,我们进入了一个复杂而精细的领域,这是操作系统设计与实现的核心部分。Windows作为全球最广泛使用的桌面操作系统之一,其体系结构的...
通过分析源代码,初学者可以了解到如何利用C#实现基本的数学运算,如加减乘除,以及可能涉及的异常处理,如除数为零的情况。同时,还能学习到如何创建和管理窗口、按钮、文本框等控件,以及响应用户的输入事件。 ...
2. 数据处理:脚本会将这些原始数据进行预处理,包括数据格式转换、异常值处理等,确保数据的准确性和可用性。 3. 坐标转换:核心部分是将eta坐标系下的数据插值到P坐标系。η坐标是一种垂直坐标系统,常用于描述...
- 错误处理和调试:如何调试驱动程序,处理USB传输错误和设备异常。 通过这份教程,开发者可以掌握USB驱动的基本概念,理解USB驱动的工作原理,并逐步学会编写一个简单的USB鼠标驱动。对于那些想要深入研究Windows ...
3. **异常处理**:查看源码中对可能出现的错误如何进行捕获和处理,理解异常安全编程的重要性。 4. **内存管理**:学习如何有效地管理资源,特别是在涉及到系统级操作时,如打开和关闭注册表键。 5. **编码规范**:...
书中不仅详细介绍了诸如Visual Studio、WinDbg等常见调试工具的使用方法,而且还深入探讨了调试器的基本概念、内存管理、线程同步、异常处理以及调试策略等核心主题,为读者提供了一套完整的调试理论体系和实践操作...
- **异常处理与错误报告**:分析了Windows系统中异常处理机制的工作原理,并介绍了如何编写有效的错误报告以帮助快速定位问题。 #### 四、特色与亮点 - **实用性**:本书结合作者多年实践经验编写而成,内容丰富...
9. **异常处理**:良好的错误处理和异常处理机制能够提高程序的健壮性。学生需要学习如何在Windows环境中使用try-catch块来捕获和处理异常。 10. **设计模式**:为了写出可维护和可扩展的代码,理解并应用设计模式...
这部分内容介绍了几种常用的数据预处理技术,如缺失值处理、异常值检测等,以及如何使用工具或算法来提高数据质量。 #### 结论 通过对以上知识点的学习,考生可以全面了解物联网工程中智能数据处理的相关理论和...
此外,教程还会涉及异常处理、系统调用、系统服务以及内核模式下的调试技术,这些都是高级内核调试者必备的技能。通过实际案例分析,读者可以学习如何追踪和定位复杂的问题,如死锁、资源泄露、系统不稳定等。 文件...
此外,文章还介绍了防止快速失败的守护页、任意释放异常处理以及Exp战术位图翻转2.0等技术。 在用户空间的安全性方面,作者详细解析了Heap_USERDATA_HEADER的攻击,以及Windows 8如何通过增加堆相关安全特性来防止...
3. 异常处理和通知:在程序中正确处理异常事件,理解通知(notification)机制对于排错至关重要。文章深入探讨了异常的捕获和处理,以及系统通知的生成和利用。 4. 内存、堆栈管理:文章还着重讨论了内存管理问题,...