25.1 UnhandledExceptionFilter函数详解
25.1.1 BaseProcessStart伪代码(Kernel32内部)
void BaseProcessStart(PVOID lpfnEntryPoint) //参数为线程函数的入口地址 { DWORD retValue; DWORD currentESP; DWORD exceptionCode; currentESP = ESP; //lpfnEntryPoint被try/except封装着,这是系统安装的默认的异常处理程序,也是SEH链上最后一个异常处理程序 __try { NtSetInformationThread(GetCurrentThread(), ThreadQuerySetWin32StartAddress, &lpfnEntryPoint, sizeof(lpfnEntryPoint)); retValue = lpfnEntryPoint(); ExitThread(retValue); //如果异常,线程从这里退出! } __except ( //过滤器表达式代码 exceptionCode = GetExceptionInformation(), UnhandledExceptionFilter(GetExceptionInformation())) //出现异常会调用Unhandled...这个函数,该函数内部会调用
//用户通过SetUnhandledFilter设置的全局异常处理函数。 { //如果UnhandledExceptionFilter返回EXCEPTION_EXECUTE_HANDLER,则会控制流会执行到这里 ESP = currentESP;
if (!_BaseRunningInServerProcess) //普通进程,则退出进程 ExitProcess(exceptionCode); else // 线程是作为服务来运行的,只退出线程并不终止整个服务 ExitThread(exceptionCode); } }
(1)如果异常过滤程序返回EXCEPTION_CONTINUE_SEARCH时,系统会继续向外层寻找异常过滤程序。但如果每个异常过滤程序都返回EXCEPTION_CONTINUE_SEARCH时,会未到遇处理异常。
(2)调用SetUnhandledExceptionFilter安装用户提供的全局(顶层)异常过滤回调函数(为所有线程共享)。如果顶层异常回调函数返回EXCEPTION_EXECUTE_HANDLER或EXCEPTION_CONTINUE_SEARCH则直接传递给UnhandledExceptionFilter函数,UnhandledExceptionFilter根据这个返回值判断是终止进程还是重新执行异常代码。如果顶层异常回调函数返回EXCEPTION_CONTINUE_SEARCH,则接下来的要发生的事情就比较复杂(可参考后面的《UnhandledExceptionFilter内部工作流程》)
(3)SetUnhandledExceptionFilter返回值为上次安装的异常过滤程序的地址。如果使用C/C++运行库,则会默认安装一个__CxxUnhandledExceptionFilter过滤程序。该函数首先检查异常是不是C++异常,如果是则在结束时执行abort函数(该函数内部调用了UnhandledExceptionFilter函数,注意这可能会造成循环调用,因为UnhandledExceptionFilter内部调用了我们安装的全局异常过滤函数_CxxUnhandledExceptionFilter,而这个函数的内部又调用UnhandledExceptionFilter,为了防止无限递归调用,_CxxUnhandledExceptionFilter在调用UnhandledExceptionFilter之前会调用SetUnhandledExceptionFilter(NULL))。如果不是C++异常则返回EXCEPTION_CONTINUE_SEARCH。所以当我们调用SetUnhandled*函数,返回的地址为_CxxUnhandledExceptionFilter的地址。
(4)注意,在我们的顶层异常过滤函数里,在返回EXCEPTION_CONTINUE_SEARCH前,不应调用之前的全局异常过滤函数(即我们通过SetUnhandledExceptionFilter的返回值取得的那个函数)。因为如果这个函数是在某个动态库里,那它随时都可能被卸载了。
(5)如果SetUnhandledExceptionFilter(NULL),则取消我们设置的全局异常过滤函数。
【UnhandledExceptionFilter程序】演示设置顶层异常过滤函数
#include <tchar.h> #include <windows.h> #include <locale.h> LONG WINAPI MyUnhandledExceptionFilter( struct _EXCEPTION_POINTERS *lpTopLevelExceptionFilter) { _tprintf(_T("发生未处理异常\n")); _tsystem(_T("PAUSE")); return EXCEPTION_EXECUTE_HANDLER; //这样返回,进程将被终止。 } int _tmain() { SetUnhandledExceptionFilter(MyUnhandledExceptionFilter); //安装用户自定义的未处理异常 _tsetlocale(LC_ALL, _T("chs")); __try{ //SetErrorMode(SEM_NOGPFAULTERRORBOX); *(int*)0 = 5;//引发异常 } __except (EXCEPTION_CONTINUE_SEARCH){ //这里返回EXCEPTION_CONTINUE_SEARCH,异常就会到达MyUnhandled* } _tsystem(_T("PAUSE"));//这行不会被执行! return 0; }
25.1.2 UnhandledExceptionFilter内部工作流程
①判断是否因为对资源进行写入操作引发的异常。如果是,将资源的只读属性改为可写入,并返回EXCEPTION_CONTINUE_EXECUTION以允许失败的指令再次执行。
②确定进程是否被调试。如果被调试,就返回EXCEPTION_CONTINUE_SEARCH给调试器,通知调试器定位异常指令,并告知我们出了什么样的异常。
③调用我们设置的顶层异常过滤函数(如果存在的话)。如果顶层过滤函数返回EXCEPT_EXECUTE_HANDLER或EXCEPTION_CONTINUE_EXECUTION,将直接传递给UnhandledExceptionFilter,由它将返回值给系统。如果返回EXCEPT_CONTINUE_SEARCH,则跳到第④步。
④再次将未处理异常报告给调试器
⑤终止进程:如果线程调用SetErrorMode并设置SEM_NOGPFAULTERRORBOX标志,那么UnhandledExceptFilter会返回EXCEPTION_EXECUTE_HANDLER,在未处理异常的情况下进行全局展开并执行未执行的finally块,然后进程终止。
如果没有调用SetErrorMode函数,UnhandledExceptionFilter会返回EXCEPTION_CONTINUE_SEARCH。于是系统内核得到程序控制,它将通过ALPC(高级本地过程调用)机制将异常通知给WerSvc(Windows错误报告专用服务),然后ALPC先阻塞自己的线程,直到WerSvc执行完毕。
⑥UnhandledExceptionFilter与WER的交互
当WerSvc接到通知时,会先调用CreateProcess来启动WerFault.exe,然后 WerSvc会等待这个新进程的结束。而WerFault.exe会向我们创建上面的两个对话框以报告错误的发生。当第1个对话框出现时,可以选择“取消”来终止我们的应用程序,否则过一会儿,会弹出第2个对话框,如果我们选择“关闭程序”,则WerFault.exe会调用TerminateProcess来结束我们的应用程序。如果选择“调试”,WerFault.exe会创建一个子进程(调试器),让他附着在出错的程序上进行“即时调试”
25.2 即时调试
(1)默认调试器:HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug子项下有一个名为Debugger的值,系统通过个值找到调试器。
(2)WerFault.exe会给这个调试器传入两个参数:要调试的进程ID和继承过来的事件句柄(这个句柄由WerSvc服务创建用于通知被调试进程调试也结束)
(3)通过将调试器附着到被调试进程,可以查看全局、局部和静态变量的值,也可以设置断点,检查函数调用树等调试工作。
【Spreadsheet程序】通过SEH向预订的地址空间稀疏调拨存储器
25.3 向量化异常和继续处理程序
25.3.1向量化异常(vectored exception handing,VEH)——在SEH前被调用
①对于多层嵌套的SEH来说,外层的__try_except语句块可能没有机会处理被内层嵌套拦截的异常。对于一般软件来说,这不是太大的问题,但是当内层嵌套的软件是第三方的库函数,并且内部以不友好的方式处理了异常,比如:异常退出进程了事,这对整个程序将造成很不利的影响。
②此时可以利用向量化异常处理,在正常的SEH之前以合适的方式拦截和处理异常。
③当异常发生时,系统在执行SEH过滤程序之前,会先依次调用VEH列表中的每个VEH异常处理函数。
(2)注册VEH异常处理程序:AddVectoredExceptionHandler(bFirstInTheList,pfnHandler)
①参数bFirstInTheList为0表示pfnHandler被添加到列表尾端,非0在列表头部。
②pfnHandler:异常处理函数,如果返回EXCEPTION_CONTINUE_SEARCH,则重新执行导致异常的指令,如果返回EXCEPTION_CONTINUE_SEARCH表示让VEH链表中的其他函数去处理异常,如果所有函数都返回EXCEPTION_CONTINUE_SEARCH,SEH过滤函数就会被执行。
(3)删除VEH异常处理函数:RemoveVectoredExceptionHandler(pHandler),其中pHandler这个句柄为AddVectoredExceptionHandler的返回值。
25.3.2 继续处理程序:——用于实现程序的诊断和跟踪
(1)安装:PVOID AddVectoredContinueHandler(bFirstInTheList,pfnHandler);
①参数bFirstInTheList为0,表示安装在继续处理程序列头的尾部,非0在头部。
②通过该函数安装的异常处理程序是在SetUnhandledExceptionFilter安装的异常处理程序返回EXCEPTION_CONTINUE_SEARCH之后才被调用。
③如果pfnHandler函数返回EXCEPTION_CONTINUE_EXECUTION重新执行导致异常的指令,EXCEPTION_CONTINUE_SEARCH让系统执行它后面的异常处理程序。
(2)删除:RemoveVectoredContinueHandler(pHandler);
【VectoredExceptionFilter】演示向量化异常过滤函数的调用
#include <windows.h> #include <tchar.h> #include <locale.h> int g_iVal = 0; //VEH1异常过滤函数 LONG CALLBACK VEH1(struct _EXCEPTION_POINTERS* pEP){ _tprintf(_T("VEH1\n")); return EXCEPTION_CONTINUE_SEARCH; } //VEH2异常过滤函数 LONG CALLBACK VEH2(struct _EXCEPTION_POINTERS* pEP){ _tprintf(_T("VEH2\n")); //以下的注释,可以取消以观察不同的输出结果 //if ((EXCEPTION_INT_DIVIDE_BY_ZERO == pEP->ExceptionRecord->ExceptionCode)){ // g_iVal = 25; // return EXCEPTION_CONTINUE_EXECUTION; //} return EXCEPTION_CONTINUE_SEARCH; } //VEH3异常过滤函数 LONG CALLBACK VEH3(struct _EXCEPTION_POINTERS* pEP){ _tprintf(_T("VEH3\n")); return EXCEPTION_CONTINUE_SEARCH; } //SEH异常过滤函数 LONG SEHFilter(PEXCEPTION_POINTERS pEP){ _tprintf(_T("SEH\n")); if ((EXCEPTION_INT_DIVIDE_BY_ZERO == pEP->ExceptionRecord->ExceptionCode)){ g_iVal = 34; return EXCEPTION_CONTINUE_EXECUTION; } return EXCEPTION_CONTINUE_SEARCH; } void Fun1(int iVal){ __try{ _tprintf(_T("Fun1 g_iVal = %d iVal = %d\n"), g_iVal, iVal); iVal /= g_iVal; //这里发生异常,VEH异常会先被调用! _tprintf(_T("Fun1 g_iVal = %d iVal = %d\n"), g_iVal, iVal); } __except (EXCEPTION_EXECUTE_HANDLER){ _tprintf(_T("Func1 _except块执行,程序将退出!\n")); _tsystem(_T("PAUSE")); ExitProcess(1); } } int _tmain(){ _tsetlocale(LC_ALL, _T("chs")); PVOID pVEH1 = AddVectoredExceptionHandler(0, VEH1);//安装到VEH链表尾部 PVOID pVEH2 = AddVectoredExceptionHandler(0, VEH2);//安装到VEH链表尾部 PVOID pVEH3 = AddVectoredExceptionHandler(0, VEH3);//安装到VEH链表尾部 __try{ Fun1(g_iVal); } __except (SEHFilter(GetExceptionInformation())){ _tprintf(_T("main _except excuted!\n")); } RemoveVectoredExceptionHandler(pVEH1); RemoveVectoredExceptionHandler(pVEH2); RemoveVectoredExceptionHandler(pVEH3); _tsetlocale(LC_ALL, _T("chs")); _tsystem(_T("PAUSE")); return 0; }
25.4 C++异常与结构化异常的比较
(1)框架的差别
//C++异常 void ChunkyFunky(){ try{ //try块 ... throw 5;
} catch (int x){ //catch块 ... } ... } |
//SEH异常 void ChunkyFunky(){ __try{ //try块 //... RaiseException(Code = 0xE06D7363,//ASCII的“msc” Flag = EXECEPTION_NONCONTINUABLE, Args = 5); } __except ((ArgType == Integer)? EXCEPTION_EXECUTE_HANDLER: EXCEPTION_CONTINUE_SEARCH){ //Catch块 ... } ... } |
(2)SEH和C++异常的比较
①SEH是操作系统提供的,它在任何语言中都可以使用,而C++异常只有在编写C++代码时才可以使用。
②如果开发C++应用程序,应该使用C++异常,而不是SEH异常,因为C++异常是语言的一部分,编译器会自动生成代码来调用C++对象的析构函数,保证对象的释放。
③C++异常也是利用操作系统的SEH来实现的,所以在创建一个C++try块时,编译器也会生成一个SEH的__try块。C++的catch语句对应SEH异常过滤程序,catch块中的代码对应SEH __except块中的代码。C++的throw语句也是对RaiseException函数的调用。
④C++调用throw抛出异常时,都会自动带EXCEPT_NONCONTINUEABLE标志。这意味着C++不能再次执行错误代码。
⑤__except通过比较throw变量的数据类型与C++ catch语句中所用到的变量的数据类型,如果一致,返回EXCEPTION_EXECUTE_HANDLER,让__except块执行。如果不同,返回EXCEPTION_CONTINUE_SEARCH继续向搜索外层的__try/__except。
25.5 异常与调试器
(1)首次机会通知和最后机会通知:
当某个线程抛出异常里,操作系统会马上通知调试器(如果调试器己经附着),这个通知被称为“首次机会通知(First-Chance Notification)”调试器将响应这个通知,促使线程寻找异常过滤程序。如果所有的异常过滤程序都返回EXCEPTION_CONTINUE_SEARCH,操作系统会给调试器一个“最后机会通知(Last-Chance Notification)”
(2)每个解决方案(.sln),可以从主菜单“调试”→“异常(x)…”找出“异常”对话框。每个异常代码为32位,如果勾选“引发”表示每当被调试线程引发异常时,调试器都会收到首次机会通知,此时异常刚刚发生,线程还没有得到机会执行异常过滤程序。调试器会弹出相应的对话框让我们选择操作,我们可以选择调试,然后在代码里设置断点,查看变量的值或线程的函数调用栈,如果不勾选“引发”这项,则不会弹出对话框,但调试器收到通知时,会在输出窗口中显示一行文字,以表示它收到了这个异常通知,然后允许线程寻找异常过滤程序。 “用户未处理的”表示异常过滤函数返回EXCEPTION_CONTINUE_SEARCH,调试器收到最后机会通知,也会弹出相应的对话框让用户来选择操作。
(3)可以自定义软件异常:只需单击“添加”按钮,然后输入异常名称和异常代码(不能与己有的重复)