第二十五章  未处理异常,向量化异常处理与C++异常

本章内容

25.1 UnhandledExceptionFilter函数详解

25.2 即时调试

25.3 电子表格示例程序

25.4 向量化异常和继续处理程序

25.5 C++异常与结构化异常的比较

25.6 异常与调试器

前一章讨论的异常过滤函数返回EXCEPTION_CONTINUE_SEARCH会继续向外层搜索异常处理代码块。

如果每个异常过滤程序都返回EXCEPTION_CONTINUE_SEARCH,最后就会导致一个未处理异常

MS提供了一个函数SetUnhandledExceptionFilter,给我们处理异常的最后机会,否则Windows就认为该异常没被处理,从而交给错误报告处理程序来处理最后杀死进程。

WINBASEAPI
LPTOP_LEVEL_EXCEPTION_FILTER
WINAPI
SetUnhandledExceptionFilter(_In_opt_ LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter);

应该在进程初始化阶段调用该函数。进程中任意线程抛出未处理异常都将会导致我们设定的最上层异常过滤函数被执行。

该函数原型类似如下

 LONG WINAPI TopLevelUnhandledExceptionFilter(PEXCEPTION_POINTERS pExceptionInfo);

注意不要在过滤函数中使用堆,很可能已经被破坏

当我们设置了未处理异常过滤程序时,SetUnhandledExceptionFilter返回上次安装的异常过滤程序地址。

如果程序使用C/C++运行库,在进程入口点函数执行前,C/C++运行库默认会安装一个全局异常过滤程序__CxxUnhandledExceptionFilter, 该函数检查是否是C++异常。如果是则在结束时执行abort函数,后者将调用Kernel32.dll中的UnhandledExceptionFilter函数。

利用_set_abort_behavior可以配置abort函数执行的错误报告。 如果__CxxUnhandledExceptionFilter认为不是C++的异常,则会返回EXCEPTION_CONTINUE_SEARCH让windows来处理该异常。

如果过滤程序即将返回EXCEPTION_CONTINUE_SEARCH,读者可能想在此之前调用之前保存的全局异常过滤程序,但不是建议这么用。因为不知道程序使用了某个第三方组件是否安装了什么异常过滤程序,如果第三方组件是一个动态装载模块,此时它可能已经被卸载了。

线程函数是通过NTDLL.dll中的BaseThreadStart开始的

VOID BaseThreadStart(PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam) {__try{ExitThread((pfnStartAddr)(pvParam));}__except (UnhandledExceptionFilter(GetExceptionInformation())){ExitProcess(GetExceptionCode());}
}

如果线程函数内部抛出一个异常,并且所有try/except块的过滤函数都返回EXCEPTION_CONTINUE_SEARCH最后就会被BaseThreadStart中的UnhandledExceptionFilter捕获。和普通的过滤程序一样,改函数返回3个EXCEPTION_*异常标识符中的一个,表252-2分别描述各异常标识符返回时发生的情况。

在返回这些异常标识符之前,UnhandledExceptionFilter还会执行大量的代码。

25.1 UnhandledExceptionFilter 函数详解

UnhandledExceptionFilter会按顺序执行5个步骤。在执行完这些步骤以后,UnhandledExceptionFilter将执行控制交给Windows错误报告处理程序。

1. 允许对资源进行写入操作并继续执行

如果因为线程的写操作而引起非法访问异常,UnhandledExceptionFilter会查看这个线程是否在修改exe或DLL模块中的资源。这些资源默认是只读的,试图去修改就会触发访问违规。接着UnhandledExceptionFilter使用VirtualProtect将资源页的保护属性设置为PAGE_READWRITE并返回EXCEPTION_CONTINUE_EXECUTION以允许失败的指令再次执行。

2. 将未处理异常报告给调试器

UnhandledExceptionFilter首先检查当前应用程序是否是在调试器的控制下,如果是,它将返回EXCEPTION_CONTINUE_SEARCH。因为当前异常未处理,所以Windows通知附着在当前进程的调试器。调试器会收到当前异常的EXCEPTION_RECORD结构的ExceptionInformation成员,它通过这些信息来定位到代码里抛出异常的那条指令,并通知我们什么样的异常被抛出。可以在代码里调用IsDebuggerPresent来检测当前进程是否处于调试器的控制下。

3. 通知我们设置的全局异常过滤函数

如果应用程序已经通过SetUnhandledExceptionFilter来指定全局异常过滤程序,UnhandledExceptionFilter将调用这个函数。

如果异常过滤函数返回是EXCEPTION_EXECUTE_HANDLER或者EXCEPTION_CONTINUE_EXECUTION.UnhandledExceptionFilter将直接返回这个值给系统。

如果返回值是EXCEPTION_CONTINUE_SEARCH将转到步骤4.

注意:C++的全局未处理异常过滤函数__CxxUnhandledExceptionFilter会显式调用UnhandledExceptionFilter。因此这里有一个无穷递归调用。为了防止出现该问题。__CxxUnhandledExceptionFilter在调用UnhandledExceptionFilter之前会设置SetUnhandledExceptionFilter(NULL)

当程序使用CRT的时候,运行库会使用try/except结构保护线程入口点函数,对应的异常过滤程序会调用C/C++运行库的_XcpFilter函数。_XcpFilter函数内部也调用UnhandledExceptionFilter。而后者又会继续调用全局异常过滤函数(如果存在的话)。 如果我们注册的全局异常过滤函数返回EXCEPTION_CONTINUE_SEARCH,(真正的)未处理异常就会到达BaseThreadStart异常过滤程序,于是UnhandledExceptionFilter又会执行,结果,全局过滤程序也会再被执行一次。

4. 将未处理异常报告给调试器(两次)

在步骤3的时候,全局未处理异常过滤函数会调用调试器并让调试器附着到抛出异常的进程。如果这时候未处理异常过滤程序返回EXCEPTION_CONTINUE_SEARCH,调试器会再一次被调用(操作系统内核判断了目标进程以及附着了一个调试器,会再次将此异常发送给调试器)

5. 终止进程

如果进程中某个线程曾经调用SetErrorMode设置标志SEM_NOGPFAULTERRORBOX 那么UnhandledExceptionFilter会返回EXCEPTION_EXECUTE_HANDLER.。 在未处理异常的情形下,这个值将会引发全局展开,后者在进程悄然终止前让未执行的finally块执行。

如果进程处于作业中,并且作业的限制信息(limit information)设置了标志JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION, 那么UnhandledExceptionFilter也将返回EXCEPTION_EXECUTE_HANDLER

在这些步骤里,UnhandledExceptionFilter工作于后台,试图解决引发异常的问题,通知调试器(如果存在),或者在必要情况下简单终止程序运行。如果不能处理异常,就会返回EXCEPTION_CONTINUE_SEARCH,于是系统内核得到程序的控制,将程序运行错误通知用户。

UnhandledExceptionFilter返回并在Windows内核所做的工作之前,会先弹出一个用户界面,

6. UnhandledExceptionFilter与WER的交互

下图显示了Windows如何使用Windows错误报告机制(Windows Error Reporting infrastructure)来处理未处理异常。

从Vista开始UnhandledExceptionFilter函数不再直接发送错误报告到MS的服务器。取而代之,若其无法处理异常时,直接返回EXCEPTION_CONTINUE_SEARCH.

于是系统内核知道一个异常在用户态线程没有得到处理(步骤4),然后异常的通知被发送给WerSvc专用服务。

该通知通过一个叫做“高级本地过程调用”(Advanced Local Procedure Call 简称ALPC)的机制发出。ALPC会先阻塞线程,直到WerSvc完成它的处理(第五步)。

WerSvc通过调用CreateProcess来启动WerFault.exe(步骤6)。并等待新线程结束。报告的创建和发送(步骤7)由WerFault.exe来完成。在WerFault弹出的对话框中,用户可以选择关闭应用程序或者附着一个调试器(步骤8)。如果用户选择关闭程序,WerFault.exe会调用TerminateProcess来不加提示地完全结束应用程序(步骤9)

可以通过注册表配置这个过程中呈现的界面。

HKEY_CURRENT_USER\Software\Microsoft\Windows\Windows Error Reporting子项之下的DontShowUI设为1时,就不会有任何对话框弹出,报告会在后台生成并发送给MS。

如果在问题发生时用户选择发送或者不发送错误报告给MS,可以改变DefaultConsent的值。(类型是DWORD)

更推荐在控制面板中的Problem Reports And Solution(问题报告与解决方案)来打开WER控制台,并单击设置这些选项。

选择Ask Me To Check If A Problem Occurs WER将弹出一个新对话框。如下图

这个对话框提供3中选择,虽然不推荐用户在最终产品计算机上选择这个选项。(因为我们不想错过任何问题)但是当调试程序时,这可以节省很多时间,因为不需要等待报告的生成和发送。如果计算机没有联网,这就更节省了不少时间。(等待发送超时)

如果正在运行自动测试案例,可以不让WER对话框来破坏或终止自动测试。将Reporting注册表子项的ForceQueue设置为1,WER将在后台生成错误报告。一旦自动测试完毕,可以使用WER控制台来列出发生过的问题并得到它们的详细信息。参考26章。

当为处理异常发生时,WER提供的最后一个功能叫:即使调试。如果用户选择调试出错进程,WerFault.exe会创建一个事件,将手动重置设置为True,同时置为未触发状态,并设置bInheritHandles参数为TRUE。从而允许WerFault.exe的子进程(调试器),可以继承这个事件句柄,然后WER找到并启动默认调试器,让他们附着到出错的进程上。(即时调试)通过将调试器附着到进程,我们可以查看全局,局部和静态变量,设置断点,检查函数调用树,重启进程灯其他调试工作。

25.2 即时调试

即时调试真正的好处是可以在程序发生问题时马上处理它。在很多其他操作系统,必须通过调试器来重启程序并调试。然后还必须重现问题。这就使得解决问题缺陷更加困难。

可以动态附着调试器到进程是Windows最好的功能之一。

工作原理如下:

注册表有这个相关子项目:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug

这个子项有一个名为Debugger的值。在Visual Studio的安装过程被设置为如下

"C:\Windows\system32\vsjitdebugger.exe" -p %ld -e %ld

vsjitdebugger.exe本身并不是一个调试器。他只是运行我们通过下图所示对话框来选择调试器的应用程序。

也可以修改该选项让其直接指向一个调试器。WerFault.exe会给调试器传入两个参数。第一个是要调试的进程ID,第二个是继承过来的时间句柄,它指向由WerSvc服务创建的处于未触发的手动重置事件(步骤6)。注意问题进程也在等待WerSvc返回的ALPC通知。第三方厂商实现的调试器要能够识别-p 和-e两个开关,代表进程ID和事件句柄。

WerFault.exe先将进程ID和事件句柄合并到一个字符串里,然后通过CreateProcess启动调试器,并将bInheritHandles参数设置为TRUE,允许调试器的进程可以继承事件对象的句柄。

此时调试器进程启动并查看命令行参数。如果-p开关存在,调试器拿到进程ID并通过DebugActiveProcess将自己附着到对应进程上

WINBASEAPI
BOOL
APIENTRY
DebugActiveProcess(_In_ DWORD dwProcessId);

操作系统对调试器完成附着以后,会将被调试程序的状态报告给调试器。比如,操作系统会告诉调试器被调试程序有多少线程以及被调试程序载入多少DLL。调试器需要花时间处理这些数据来准备调试。在这些准备工作进行时,发生问题的进程只能等待。

检测到未处理异常的那段代码(步骤4)也在等待从WerSvc返回的ALPC通知(步骤5)

而ALPC本身因为以WerFault.exe进程句柄为参数调用WaitForSingleObjectEx被阻塞,直到WER完成它的工作。注意这里是WaitForSingleObjectEx,线程可以在可通知状态下等待。队列里的异步过程调用(asynchronous procedure call, APC)都能得到处理。

在Vista之前的操作系统上,问题进程里的其他线程并不会暂停,所以它们会在已经损坏的上下文中运行并可能抛出更多异常,因此系统很可能会突然终止进程,这使得问题更加复杂。就算没有其他线程崩溃,在产生dump文件时,程序运行状态已经改变。这使得查找异常的根本原因变得很困难。在Vista以后,如果应用程序不是一个服务,问题进程的所有线程都会被挂起,不会再占用cpu,直到WER让它们继续执行,以便将未处理异常通知调试器

调试器初始化以后,开始检查-e开关。如果存在 调试器得到事件句柄并调用SetEvent。设置事件(步骤11)将通知WerFault.exe调试器已经附着到了问题进程,可以接受并处理异常了。于是WerFault.exe进程将正常退出,而WerSvc服务也发现其子进程退出了,于是让ALPC返回(步骤12)。这就导致了被调试程序的线程被唤醒。它会通知内核一个调试器已经附着到当前线程,并准备好接受未处理异常的通知(第13步)这就和25.1的步骤3效果相同。接着调试器将收到关于未处理异常的通知,它将载入正确的源码文件,并定位到抛出异常的那条指令。

在未发生异常的时候,也可以使用vsjitdebugger.exe -p PID去附着任意进程,PID表示要被调试的进程ID。可以在任务管理器右键任意进程然后选择debug并选择一个调试器进行调试。

25.3 电子表格示例程序

演示如何通过SEH来处理预料中的异常,避免未处理异常所造成的的惨痛后果。

该例子通过SEH向已预订的地址空间区域稀疏地调拨存储器。

当程序试图去写入一个未调拨的内存地址时,会触发一个异常。通过SEH机制重新调拨物理内存并重新执行触发异常的代码。(EXCEPTION_CONTINUE_EXECUTION

当程序试图去读取一个未调拨的内存地址时,也会触发一个异常。通过SEH机制不会去申请存储空间,而是直接执行except块之后的代码(EXCEPTION_EXECUTION_HANDLER)

当试图读取已经调拨过的虚拟地址,不会触发任何异常。正常运行

试图写入一个在已经调拨过物理存储器的单元格相邻的地址时,由于调拨存储器是根据页面大小来的,因此相邻的地址也已经被调拨了物理存储器。同样不会触发任何异常,正常运行。

整个代码封装了一个CVMArray类来封装所有复杂的工作。

可以直接创建CVMArray的实例,并传递数组元素的上限给构造函数。这个类会自动为整个进程建立一个未处理异常过滤程序,任何访问虚拟内存数组中的内存地址触发的异常都会被其内部的SEH代码捕获并调用VirtualAlloc修复。这样简化了编码的工作。

但是若内存因为某些原因不能成功调拨,这就使得进程不能很好的恢复。

第二种方式是直接继承CVMArray。使用派生类,可以得到基类带来的好处,还可以添加自定义的功能。例如可以重载基类的某些处理函数使得内存不够的问题处理的更加合理。

Spreadsheet.cpp

/******************************************************************************
Module:  Spreadsheet.cpp
Notices: Copyright (c) 2008 Jeffrey Richter & Christophe Nasarre
******************************************************************************/#include "..\CommonFiles\CmnHdr.h"
#include <windowsx.h>
#include <tchar.h>
#include <strsafe.h>
#include "VMArray.h"
#include "resource.h"//HWND g_hWnd;       // Global window handle used for SEH reportingconst int g_nNumRows = 256;
const int g_nNumCols = 1024;// Declare the contents of a single cell in the spreadsheet
typedef struct {DWORD dwValue;BYTE bDummy[1020];
} CELL, *PCELL;// Declare the data type for an entire spreadsheet
typedef CELL SPREADSHEET[g_nNumRows][g_nNumCols];
typedef SPREADSHEET * PSPREADSHEET;//// A spreadsheet is a 2-dimensional array of CELLs
class CVMSpreadsheet : public CVMArray<CELL> {
public:CVMSpreadsheet() : CVMArray<CELL>(g_nNumRows * g_nNumCols) {}private:LONG OnAccessViolation(PVOID pvAddrTouched, BOOL bAttemptedRead,PEXCEPTION_POINTERS pep, BOOL bRetryUntilsSuccessful);
};//LONG CVMSpreadsheet::OnAccessViolation(PVOID pvAddrTouched, BOOL bAttemptedRead,PEXCEPTION_POINTERS pep, BOOL bRetryUntilSuccessful) {TCHAR sz[200];StringCchPrintf(sz, _countof(sz), TEXT("Violation: Attempting to %s"),bAttemptedRead ? TEXT("Read") : TEXT("Write"));SetDlgItemText(g_hWnd, IDC_LOG, sz);LONG lDisposition = EXCEPTION_EXECUTE_HANDLER;if (!bAttemptedRead) {// Return whatever the base class says to dolDisposition = CVMArray<CELL>::OnAccessViolation(pvAddrTouched,bAttemptedRead, pep, bRetryUntilSuccessful);}return lDisposition;
}//// This is the global CVMSpreadsheet object
static CVMSpreadsheet g_ssObject;// Create a global pointer that points to the entire spreadsheet region
SPREADSHEET& g_ss = *(PSPREADSHEET)(PCELL)g_ssObject;//BOOL Dlg_OnInitDialog(HWND hWnd, HWND hWndFocus, LPARAM lParam) {chSETDLGICONS(hWnd, IDI_SPREADSHEET);g_hWnd = hWnd; // Save for SEH reporting// Put default values in the dialog box controlsEdit_LimitText(GetDlgItem(hWnd, IDC_ROW), 3);Edit_LimitText(GetDlgItem(hWnd, IDC_COLUMN), 4);Edit_LimitText(GetDlgItem(hWnd, IDC_VALUE), 7);SetDlgItemInt(hWnd, IDC_ROW, 100, FALSE);SetDlgItemInt(hWnd, IDC_COLUMN, 100, FALSE);SetDlgItemInt(hWnd, IDC_VALUE, 12345, FALSE);return TRUE;
}//void Dlg_OnCommand(HWND hWnd, int id, HWND hWndCtl, UINT codeNotify) {int nRow, nCol;switch (id) {case IDCANCEL:EndDialog(hWnd, id);break;case IDC_ROW:// User modified the row, update the UInRow = GetDlgItemInt(hWnd, IDC_ROW, NULL, FALSE);EnableWindow(GetDlgItem(hWnd, IDC_READCELL),chINRANGE(0, nRow, g_nNumRows - 1));EnableWindow(GetDlgItem(hWnd, IDC_WRITECELL),chINRANGE(0, nRow, g_nNumRows - 1));break;case IDC_COLUMN:// User modified the column, update the UInCol = GetDlgItemInt(hWnd, IDC_COLUMN, NULL, FALSE);EnableWindow(GetDlgItem(hWnd, IDC_READCELL),chINRANGE(0, nCol, g_nNumCols - 1));EnableWindow(GetDlgItem(hWnd, IDC_WRITECELL),chINRANGE(0, nCol, g_nNumCols - 1));break;case IDC_READCELL:// Try to read a value from the user's selected cellSetDlgItemText(g_hWnd, IDC_LOG, TEXT("No violation raised"));nRow = GetDlgItemInt(hWnd, IDC_ROW, NULL, FALSE);nCol = GetDlgItemInt(hWnd, IDC_COLUMN, NULL, FALSE);__try{SetDlgItemInt(hWnd, IDC_VALUE, g_ss[nRow][nCol].dwValue, FALSE);}__except (g_ssObject.ExceptionFilter(GetExceptionInformation(), FALSE)) {// The cell is not backed by storage, the cell contains nothing.SetDlgItemText(hWnd, IDC_VALUE, TEXT(""));}break;case IDC_WRITECELL:// Try to read a value from the user's selected cellSetDlgItemText(g_hWnd, IDC_LOG, TEXT("No violation raised"));nRow = GetDlgItemInt(hWnd, IDC_ROW, NULL, FALSE);nCol = GetDlgItemInt(hWnd, IDC_COLUMN, NULL, FALSE);// If the cell is not backed by storage, an access violation is// raised causing storage to automatically be committed.g_ss[nRow][nCol].dwValue =GetDlgItemInt(hWnd, IDC_VALUE, NULL, FALSE);break;}
}//INT_PTR WINAPI Dlg_Proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {switch (uMsg) {chHANDLE_DLGMSG(hWnd, WM_INITDIALOG, Dlg_OnInitDialog);chHANDLE_DLGMSG(hWnd, WM_COMMAND, Dlg_OnCommand);}return FALSE;
}//int WINAPI _tWinMain(HINSTANCE hInstExe, HINSTANCE, PTSTR, int) {DialogBox(hInstExe, MAKEINTRESOURCE(IDD_SPREADSHEET), NULL, Dlg_Proc);return 0;
}End of File //

VMarry.h

/******************************************************************************
Module:  VMArray.h
Notices: Copyright (c) 2008 Jeffrey Richter & Christophe Nasarre
******************************************************************************/#pragma once///// NOTE: This C++ class is not thread safe. You cannot have multiple threads
// creating and destroying objects of this class at the same time. // However, once created, multiple threads can access different CVMArray
// objects simultaneously and you can have multiple threads accessing a single
// CVMArray object if you manually synchronize access to the object yourself.///template <class TYPE>
class CVMArray {
public:// Reserves sparse array of elementsCVMArray(DWORD dwReserveElements);// Frees sparse Array of elementsvirtual ~CVMArray();// Allows accessing an element in the arrayoperator TYPE*()               { return (m_pArray); }operator const TYPE*() const  { return (m_pArray); }// Can be called for fine-tuned handling if commit failsLONG ExceptionFilter(PEXCEPTION_POINTERS pep,BOOL bRetryUntilSuccessful = FALSE);protected:// Override this to fine-tune handling of access violationvirtual LONG OnAccessViolation(PVOID pvAddrTouched, BOOL bAttemptedRead,PEXCEPTION_POINTERS pep, BOOL bRetryUntilSuccessful);private:static CVMArray* sm_pHead;     // Address of first objectCVMArray* m_pNext;                // Address of next objectTYPE* m_pArray;                    // Pointer to reserved region arrayDWORD m_cbReserve;               // Size of reserved region array (in bytes)private:// Address of previous unhandled exception filterstatic PTOP_LEVEL_EXCEPTION_FILTER sm_pfnUnhandledExceptionFilterPrev;// Our global unhandled exception filter for instances of this classstatic LONG WINAPI UnhandledExceptionFilter(PEXCEPTION_POINTERS pep);
};//// The head of the linked-list of objects
template <class TYPE>
CVMArray<TYPE>* CVMArray<TYPE>::sm_pHead = NULL;// Address of previous unhandled exception filter
template <class TYPE>
PTOP_LEVEL_EXCEPTION_FILTER CVMArray<TYPE>::sm_pfnUnhandledExceptionFilterPrev;//template <class TYPE>
CVMArray<TYPE>::CVMArray(DWORD dwReserveElements) {if (sm_pHead == NULL) {// Install our global unhandled exception filter when// create the first instance of the class.sm_pfnUnhandledExceptionFilterPrev =SetUnhandledExceptionFilter(UnhandledExceptionFilter);}m_pNext = sm_pHead;       // The next node was at the headsm_pHead = this;       // This node is now at the headm_cbReserve = sizeof(TYPE) * dwReserveElements;// Reserve a region for the entire arraym_pArray = (TYPE*)VirtualAlloc(NULL, m_cbReserve,MEM_RESERVE | MEM_TOP_DOWN, PAGE_READWRITE);chASSERT(m_pArray != NULL);
}//template <class TYPE>
CVMArray<TYPE>::~CVMArray() {// Free the array's region (decommitting all storage within it)VirtualFree(m_pArray, 0, MEM_RELEASE);// Remove this object from the linked listCVMArray * p = sm_pHead;if (p == this) { // Removing the head nodesm_pHead = p->m_pNext;}else {BOOL bFound = FALSE;// Walk list from head and fix pointersfor (; !bFound && (p->m_pNext != NULL); p = p->m_pNext) {if (p->m_pNext == this) {// Make the node that points to us point to the next nodep->m_pNext = p->m_pNext->m_pNext;break;}}chASSERT(bFound);}
}//// Default handling of access violations attempts to commit storage
template <class TYPE>
LONG CVMArray<TYPE>::OnAccessViolation(PVOID pvAddrTouched,BOOL bAttemptedRead, PEXCEPTION_POINTERS pep, BOOL bRetryUntilSuccessful) {BOOL bCommittedStorage = FALSE;  // Assume committing storage failsdo {// Attempt to commit storagebCommittedStorage = (NULL != VirtualAlloc(pvAddrTouched,sizeof(TYPE), MEM_COMMIT, PAGE_READWRITE));// If storage could not be committed and we're supposed to keep trying// until we succeed, prompt user to free storageif (!bCommittedStorage && bRetryUntilSuccessful) {MessageBox(NULL,TEXT("Please close some other applications and Press OK."),TEXT("Insufficient Memory Available"), MB_ICONWARNING | MB_OK);}} while (!bCommittedStorage && bRetryUntilSuccessful);// If storage committed, try again. If not, execute the handlerreturn(bCommittedStorage? EXCEPTION_CONTINUE_EXECUTION : EXCEPTION_EXECUTE_HANDLER);
}//// The filter associated with a single CVMArray object
template <class TYPE>
LONG CVMArray<TYPE>::ExceptionFilter(PEXCEPTION_POINTERS pep,BOOL bRetryUntilsSuccessful) {// Default to trying another filter (safest thing to do)LONG lDisposition = EXCEPTION_CONTINUE_SEARCH;// We only fix access violationsif (pep->ExceptionRecord->ExceptionCode != EXCEPTION_ACCESS_VIOLATION)return lDisposition;// Get address of attempted access and get attempted read or writePVOID pvAddrTouched = (PVOID)pep->ExceptionRecord->ExceptionInformation[1];BOOL bAttemptedRead = (pep->ExceptionRecord->ExceptionInformation[0] == 0);// Is attempted access within this VMArray's reserved region?if ((m_pArray <= pvAddrTouched) &&(pvAddrTouched < ((PBYTE)m_pArray + m_cbReserve))) {// Access is in this array; try to fix the problemlDisposition = OnAccessViolation(pvAddrTouched, bAttemptedRead,pep, bRetryUntilsSuccessful);}return lDisposition;
}//// The filter associated with all CVMArray objects
template <class TYPE>
LONG WINAPI CVMArray<TYPE>::UnhandledExceptionFilter(PEXCEPTION_POINTERS pep) {// Default to trying another filter (safest thing to do)LONG lDisposition = EXCEPTION_CONTINUE_SEARCH;// We only fix access violationsif (pep->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) {// Walk all the nodes in the linked-listfor (CVMArray * p = sm_pHead; p != NULL; p = p->m_pNext) {// Ask this node if it can fix the problem.// NOTE: The problem MUST be fixed or the process will be terminated!lDisposition = p->ExceptionFilter(pep, TRUE);// If we found the node and it fixed the problem, stop the loopif (lDisposition != EXCEPTION_CONTINUE_SEARCH)break;}}// If no node fixed the problem, try the previous exception filterif (lDisposition == EXCEPTION_CONTINUE_SEARCH)lDisposition = sm_pfnUnhandledExceptionFilterPrev(pep);return lDisposition;
}End of File //

25.4 向量化异常和继续处理程序

23,24介绍的SEH机制是一种基于代码框(frame-based)机制,每次当线程进入一个try块(或代码框)时,系统会在链表里加入一个节点。如果发生异常,系统依次访问链表中的每个代码框--从最晚进入的try块开始一直到最早进入的try块--并寻找每个try块的catch处理程序。一旦找到一个catch处理程序,系统再次访问链表,执行finally块。当展开结束以后(或者try块由于没有发生异常而正常退出),系统从链表中移出所有代码框。

Windows也提供了向量化异常处理(vectored exception handling, VEH)机制,它同SEH协同工作。程序可以注册一个函数,而不需要依赖于语言有关的关键字,每当异常发生或者一个未处理异常脱离标准SEH的控制时,这个函数就会被调用。

WINBASEAPI
_Ret_maybenull_
PVOID
WINAPI
AddVectoredExceptionHandler(_In_ ULONG First,_In_ PVECTORED_EXCEPTION_HANDLER Handler);

Handler是指向向量化异常处理程序的指针,该函数必须有如下签名:

LONG WINAPI ExceptionHandler(
struct _EXCEPTION_POINTERS *pExceptionInfo);

如果给First参数传递0, Handler传递的异常处理函数就会添加到列表尾端。如果传递给First传递非0,函数就会被至于列表头部。当异常发生时,在执行SEH过滤程序之前,将按照顺序逐个执行这些函数。 一旦某个函数能够纠正发生的问题,应该马上返回EXCEPTION_CONTINUE_EXECUTION让抛出异常的指令再次执行。

只要某个向量处理程序返回EXCEPTION_CONTINUE_EXECUTION, SEH过滤程序便不会有处理异常的机会。 如果异常处理函数不能纠正问题,应该返回EXCEPTION_CONTINUE_SEARCH, 让列表中的其他处理函数有机会去处理异常。

如果所有函数都返回EXCEPTION_CONTINUE_SEARCH, SEH过滤程序就会执行,注意VEH过滤不能返回EXCEPTION_EXECUTION_HANDLER

可以通过以下函数来删除之前安装的VEH异常处理函数

WINBASEAPI
ULONG
WINAPI
RemoveVectoredExceptionHandler(_In_ PVOID Handle);

Handle就是之前安装的异常处理函数句柄。 从AddVectoredExceptionHandler函数返回得到。

参考以下文章“Under the Hood: New Vectored Exception Handling in Windows XP”

http://blog.csdn.net/xiaoqiangvs007/article/details/1670403

使用向量化异常处理函数来实现基于断点的进城内API Hooking

除了能在SEH之前处理异常,VEH还能让我们在未处理异常发生时能得到通知,要得到这个通知的详细信息,需要注册一个继续处理程序(continue handler)

WINBASEAPI
_Ret_maybenull_
PVOID
WINAPI
AddVectoredContinueHandler(_In_ ULONG First,_In_ PVECTORED_EXCEPTION_HANDLER Handler);

两个参数的含义和AddVectoredExceptionHandler完全相同。

通过以下函数删除之前安装的继续处理程序

WINBASEAPI
ULONG
WINAPI
RemoveVectoredContinueHandler(_In_ PVOID Handle);

Handle是通过AddVectoredContinueHandler的返回值。

25.5 C++异常与结构化异常的比较

SEH是Windows操作系统提供的功能,可以在任何语言中使用

C++异常仅限定了C++语言。编译器知道什么是一个C++对象,会自动生成代码来调用C++对象的析构函数,保证对象释放。

微软的VC++编译器通过Windows的SEH机制来实现C++的异常处理。 一个C++的try 块对应一个 SEH的__try

catch语句对应 SEH的异常过滤程序, catch 对应 __except块。

C++的 throw 语句也会生成对应Windows的 RaiseException函数调用

throw语句所使用的变量则成为了RaiseException的附加参数

以下代码演示了C++和windows的SEH异常处理的对应关系。

注意RaiseException的异常代码是 0xE06D7363 表示这是C++异常。其ASCII代码是"msc"

每个C++异常都会使用EXCEPTION_NONCONTINUABLE标记,表明不能再执行错误代码,异常过滤处理C++异常时返回EXCEPTION_CONTINUE_EXECUTION是错误的。

只会返回EXCEPTION_EXECUTION_HANDLER 或者  EXCEPTION_CONTINUE_SEARCH

RaiseException余下的参数被用作把指定变量抛出的机制。被抛出的变量信息到底是怎么传递给RaiseException的?(推测值传递? 指针传递?)

最后与__except过滤程序有关,比较throw变量的数据类型和C++ catch语句所用到变量类型。如果数据类型是一样的,过滤程序返回EXCEPTION_EXECUTE_HANDLER

让catch块(__except块)中的语句执行。如果不同,则返回EXECUTION_CONTINUE_SEARCH, 对处于调用树上层的catch过滤程序进行求值。

25.6 异常与调试器

Visual Studio的调试器为了异常调试提供了非常出色的支持。当进程中某个线程抛出异常,操作系统会马上通知调试器。(如果调试器已经附着)。这个通知被称为“首次机会通知”(first-chance notification)调试器会相应该通知,促使线程寻找异常过滤程序。如果所有异常过滤程序都返回EXCEPTION_CONTINUE_SEARCH。 操作系统会给调试器一个“最后通知”(last-chance notification)。这两个通知使得开发人员能更好的控制异常调试的过程。

针对每个.sln ,可以通过调试器的Exceptions对话框决定调试器如何响应首次机会异常通知。

在Debug->Exceptions窗口中可以进行设置。

对话框对所有异常进行归类。可以找到Win32异常,这里列出所有系统定义的异常。对话框显示了所以异常的32位代码,描述信息,已经首次机会通知(thrown复选框)和最后机会通知(User-Unhandled 复选框) 后者进对CLR适用。

比如设置0xc0000005 Access violation 的Thrown打钩。当进程运行过程发送了访问违规错误。调试器会首先受到通知并弹出类似以下消息。

这时线程还没来得及勋章异常过滤程序。但我没可以在代码里设置一个断点并检查变量值或查看函数调用栈。如果使用调试器来单步调试,会弹出以下对话框。

对于大部分异常来说,重新执行没有什么用处,只会再次引发异常。但是对一个通过RaiseException函数抛出的异常。这个选项让线程当做没有异常发生,继续往下执行。以这种方式继续对调试一个C++异常相当管用,因为看起来就好像C++的throw语句未执行一样。

选择Yes允许被调试线程寻找异常过滤程序。如果找不到的某个异常过滤程序返回EXCEPTION_EXECUTE_HANDLER 或者EXCEPTION_CONTINUE_EXECUTION,

那么线程将继续执行。如果所有过滤程序都返回EXCEPTION_CONTINUE_SEARCH, 调试器将收到最后机会通知并显示类似如下图所示的消息框

这种情况只能调试或者终止这个应用程序。

如果没有选中Thrown。一个线程抛出异常调试器并不会立即弹出框,而是在debug窗口显示一条消息。

First-chance exception at 0x013913A8 in SubProcess.exe: 0xC0000005: Access violation writing location 0x00000000.

接着调试器运行线程寻找异常过滤程序,如果异常没有得到处理最后调试器会弹出如下框。

VS的Exception对话框还可以添加自定义的异常。单击Add按钮就可以添加一条自定义异常。输入名称和代码即可。

《Windows核心编程》读书笔记二十五章 未处理异常,向量化异常处理与C++异常相关推荐

  1. 在应用程序中使用虚拟内存——Windows核心编程学习手札之十五

    在应用程序中使用虚拟内存 --Windows核心编程学习手札之十五 Windows提供了3种进行内存管理的方法: 1)  虚拟内存,最适合用来管理大量对象或结构数组: 2)  内存映射文件,最适合用来 ...

  2. C++Windows核心编程读书笔记(转)

    http://www.makaidong.com/(马开东博客) 这篇笔记是我在读<windows核心编程>第5版时做的记录和总结(部分章节是第4版的书),没有摘抄原句,包含了很多我个人的 ...

  3. windows核心编程读书笔记(一)

    第一章:错误处理 通过GetLastError函数获得更多的错误信息,或者在监视框中使用@err,hr(vs2005)获得错误信息,而不仅仅是错误编号. 第二章:字符和字符串处理 在应用程序中,应确保 ...

  4. APUE读书笔记-第十五章-进程间通信

    管道 创建管道(pipe函数) #include <unistd.h> int pipe(int fd[2); fd[0]为读打开,fd[1]为写打开 局限性 (1)管道是半双工的,数据只 ...

  5. [hadoop读书笔记] 第十五章 sqoop1.4.6小实验 - 将mysq数据导入hive

    安装hive 1.下载hive-2.1.1(搭配hadoop版本为2.7.3) 2.解压到文件夹下 /wdcloud/app/hive-2.1.1 3.配置环境变量 4.在mysql上创建元数据库hi ...

  6. 大数据之路、阿里巴巴大数据实践读书笔记 --- 第十五章、数据质量

    随着IT向DT时代的转变,数据的重要性不言而喻,数据的应用也日趋繁茂,数据正扮演着极其重要的角色.而对于被日益重视的数据,如何保障其质量是一个关注的话题: 数据质量是数据分析结论有效性和准确性的基础, ...

  7. Windows核心编程(笔记13) 第十六章--第二十六章

    改变下记录方式,只写自己觉得需要注意一下的防止出错的地方,或者一些特别重要的点,或者一些感悟. 第十六章 线程栈 第十七章 内存映射文件 1.注意写时复制在内存映射文件中的用处. 2.#pagma d ...

  8. 《深入浅出DPDK》读书笔记(十五):DPDK应用篇(Open vSwitch(OVS)中的DPDK性能加速)

    Table of Contents Open vSwitch(OVS)中的DPDK性能加速 174.虚拟交换机简介 175.OVS简介 176.DPDK加速的OVS 177.OVS的数据通路 178. ...

  9. DLL基础——Windows核心编程学习手札之十九

    DLL基础 --Windows核心编程学习手札之十九 Windows API中的所有函数都包含在DLL中,3个最重要的DLL是Kernel32.dll,它包含用于管理内存.进程和线程的各个函数:Use ...

最新文章

  1. 阿里年会的马老师说:认真生活、快乐工作、保持理想
  2. Windows 技术篇-cmd强制关闭端口、解除端口占用方法,cmd查询端口相关的进程pid并杀死进程实例演示
  3. spring boot 跨域请求_SpringBoot 系列教程 web 篇之自定义请求匹配条件 RequestCondition...
  4. 牛客网【每日一题】5月18日 「土」秘法地震
  5. python矩阵左除_matlab学习笔记
  6. mysql中的if [not] exists
  7. AI智能问答核心代码
  8. poj1815Friendship(最小割求割边)
  9. MYSQL详解及语法大全
  10. C++五子棋人机对战
  11. QuickChm出现的“不支持此接口”错误解决
  12. H5 HTML 移动端触摸拖拽drag drop 自定义拖拽样式 使用PointerEvent模拟的拖拽方案
  13. grep_sed_awl_vim
  14. 原生js制作动画效果
  15. 各类游戏对应服务端架构
  16. python urllib urllib2 urllib3 用法 区别
  17. 51中断优先级及中断嵌套
  18. 2019西电复试计科,软件机试真题
  19. 根据当前日期进行以下方面的处理: 1、取得日期的年份、月份、天、时、分、秒,并转换成大写日期格式 如:2013年8月17日 20时30分20秒 2、根据日期的不同时间段,做问候语: 早上8:00-12
  20. 软件需求说明书(文档模板)

热门文章

  1. 论文阅读:CVPR2020旷视High-Order Information Matters: Learning Relation and Topology for Occluded Person Re
  2. pc端ncnn搭建与测试
  3. 为什么苹果蓝牙耳机连上还是公放_如何测试蓝牙设备的延时
  4. 经济基础知识(初级)【5】
  5. 如何使用 Backblaze 和 Cloudflare 搭建免费 CDN - 让白-piao进行到底
  6. c语言中八进制和十六进制
  7. 1分钟解决 微信小程序 iPhone 11、iPhoneX 底部安全区域(底部小黑条)适配
  8. Arcgis地图服务切片
  9. 小米运动同步到Google Fit健身, 解决睡眠和体重不同步的情况
  10. 【人工智能项目】深度学习实现白葡萄酒品质预测