65.9K
CodeProject 正在变化。 阅读更多。
Home

Visual C++ 中的高效异常处理

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (118投票s)

2011 年 6 月 7 日

CPOL

18分钟阅读

viewsIcon

358814

downloadIcon

7280

概述了 Visual C++ 提供的标准异常处理技术。

引言

本文描述了在 Windows 上运行的 Visual C++ 程序中处理异常和错误的标准技术。

异常(或严重错误、崩溃)通常意味着您的程序停止正常工作,需要停止执行。例如,异常可能由于程序访问无效内存地址(如 NULL 指针)、内存缓冲区无法分配(内存不足)、C 运行时库 (CRT) 检测到错误并请求终止程序等原因而发生。

C++ 程序可以处理几种类型的异常:通过操作系统结构化异常处理机制产生的 SEH 异常、C 运行时库产生的 CRT 错误,最后是信号。每种错误类型都需要安装一个异常处理程序函数,该函数会拦截异常并执行一些错误恢复操作。

如果您的应用程序有多个执行线程,事情可能会更复杂。一些异常处理程序适用于整个进程,而一些则仅适用于当前线程。因此,您必须在每个线程中安装异常处理程序。

您的应用程序中的每个模块(EXE 或 DLL)都链接到 CRT 库(静态链接或动态链接)。异常处理技术在很大程度上取决于 CRT 的链接类型。

错误类型的多样性、多线程程序中异常处理的差异以及异常处理对 CRT 链接的依赖性,需要大量工作才能精确处理您的应用程序允许处理的所有异常。本文旨在帮助您更好地理解异常处理机制,并在 C++ 应用程序中有效地使用异常处理。

文章附带了一个小型控制台演示应用程序ExceptionHandler。该演示可以引发和捕获不同类型的异常,并生成崩溃的小转储文件,允许查看发生异常的代码行。

背景

我曾经为我的一个开源项目 CrashRpt - Windows 应用程序的崩溃报告库 需要一种拦截异常的方法。CrashRpt 库处理应用程序中发生的异常,收集有关错误的技​​术信息(如崩溃小转储、错误日志、桌面屏幕截图),并提供用户通过 Internet 发送错误报告(图 1)。

图 1 - CrashRpt 库的错误报告窗口和错误报告详细信息对话框

crashrpt.png

您可能见过桌面突然出现的 Windows 错误报告窗口(图 2),CrashRpt 库执行类似的操作,只是它将错误报告发送到您自己的 Web 服务器而不是 Microsoft 的服务器。

图 2 - Windows 错误报告(Dr. Watson)窗口

wer.gif

查阅 MSDN 时,我找到了 SetUnhandledExceptionFilter() 函数,我用它来处理访问冲突。但很快我发现,我的应用程序中的一些异常不知何故未被处理,Dr. Watson 窗口仍然出现而不是 CrashRpt 窗口。

我进一步查阅 MSDN,找到了许多其他 CRT 提供的函数,可用于处理 CRT 错误。以下是一些此类函数的示例:set_terminate()_set_invalid_parameter_handler()_set_purecall_handler()

然后我发现,一些 CRT 处理程序仅对当前线程有效,而一些则对进程的所有线程都有效。

继续我的研究,我发现有许多细微之处是开发者必须理解才能有效使用异常处理的。我的研究结果如下。

关于异常的一些话

如您所知,异常或严重错误通常意味着程序停止正常工作,需要停止执行。

例如,异常可能由于以下原因而发生:

  • 程序访问无效内存地址(例如 NULL 指针)
  • 由于无限递归导致的堆栈溢出
  • 将大量数据写入小缓冲区
  • 调用 C++ 类的纯虚方法
  • 内存缓冲区无法分配(内存不足)
  • 将无效参数传递给 C++ 系统函数
  • C 运行时库检测到错误并请求终止程序

有两种具有不同性质的异常:SEH 异常(结构化异常处理,SEH)和类型化的 C++ 异常。您可以在 Vishal Kochhar 的精彩文章《编译器如何实现异常处理》中找到异常机制实现的深入描述。

结构化异常处理机制由操作系统提供(这意味着所有 Windows 应用程序都可以引发和处理 SEH 异常)。SEH 异常最初是为 C 语言设计的,但它们也可以在 C++ 中使用。

SEH 异常使用 __try{}__except(){} 结构进行处理。程序的 main() 函数受到此类结构的保护,因此默认情况下,所有未处理的 SEH 异常都会被捕获并调用 Dr. Watson。SEH 异常是 Visual C++ 编译器特定的。如果您编写可移植代码,则应使用 #ifdef/#endif 来保护结构化异常处理结构。

这是一个代码示例:

int* p = NULL;   // pointer to NULL
__try
{
    // Guarded code
    *p = 13; // causes an access violation exception
}
__except(EXCEPTION_EXECUTE_HANDLER) // Here is exception filter expression
{  
    // Here is exception handler
 
    // Terminate program
    ExitProcess(1);
}

另一方面,C++ 类型异常机制由 C 运行时库提供(这意味着只有 C++ 应用程序可以引发和处理此类异常)。C++ 类型异常使用 try{}catch(){} 结构进行处理。下面提供了一个示例(代码摘自 http://www.cplusplus.com/doc/tutorial/exceptions/

// exceptions
#include <iostream>
using namespace std;
int main () {
    try
    {
        throw 20;
    }
    catch (int e)
    {
        cout << "An exception occurred. Exception Nr. " << e << endl;
    }
    return 0;
}

结构化异常处理

当发生 SEH 异常时,您通常会看到 Dr. Watson 的窗口(参见图 2),它会提供将错误报告发送给 Microsoft 的选项。您甚至可以使用 RaiseException() 函数自行引发 SEH 异常。

每个 SEH 异常都有一个相关的异常代码。您可以在 __except 语句中使用 GetExceptionCode() 内置函数提取异常代码。您可以在 __except 语句中使用 GetExceptionInformation() 内置函数提取异常信息。要使用这些内置函数,通常需要创建自定义异常过滤器,如下例所示。

以下示例显示了如何使用 SEH 异常过滤器:

int seh_filter(unsigned int code, struct _EXCEPTION_POINTERS* ep)
{
  // Generate error report
  // Execute exception handler
  return EXCEPTION_EXECUTE_HANDLER;
}
void main()
{
  __try
  {
    // .. some buggy code here
  }
  __except(seh_filter(GetExceptionCode(), GetExceptionInformation()))
  {    
    // Terminate program
    ExitProcess(1);
  }
}

__try{}__except(){} 结构主要是面向 C 的。但是,您可以将 SEH 异常重定向到 C++ 类型异常,并像处理 C++ 类型异常一样处理它。这可以通过 C++ 运行时库 (CRT) 提供的 _set_se_translator() 函数来完成。

这是一个代码示例(摘自 MSDN):

// crt_settrans.cpp
// compile with: /EHa
#include <stdio.h>
#include <windows.h>
#include <eh.h>
void SEFunc();
void trans_func( unsigned int, EXCEPTION_POINTERS* );
class SE_Exception
{
private:
    unsigned int nSE;
public:
    SE_Exception() {}
    SE_Exception( unsigned int n ) : nSE( n ) {}
    ~SE_Exception() {}
    unsigned int getSeNumber() { return nSE; }
};
int main( void )
{
    try
    {
        _set_se_translator( trans_func );
        SEFunc();
    }
    catch( SE_Exception e )
    {
        printf( "Caught a __try exception with SE_Exception.\n" );
    }
}
void SEFunc()
{
    __try
    {
        int x, y=0;
        x = 5 / y;
    }
    __finally
    {
        printf( "In finally\n" );
    }
}
void trans_func( unsigned int u, EXCEPTION_POINTERS* pExp )
{
    printf( "In trans_func.\n" );
    throw SE_Exception();
}

然而,__try{}__catch(Expression){} 结构的缺点是,您可能会忘记保护可能导致未被程序处理的异常的潜在错误代码。此类未处理的 SEH 异常可以使用 SetUnhandledExceptionFilter() 函数设置的顶层未处理异常过滤器来捕获。

注意顶层一词意味着,如果有人在您调用之后调用 SetUnhandledExceptionFilter() 函数,则异常过滤器将被替换。这是一个缺点,因为您无法将顶层处理程序一个接一个地链接起来。此类缺点可以通过稍后讨论的向量化异常处理机制来消除。

异常信息(异常发生前的 CPU 状态)通过 EXCEPTION_POINTERS 结构传递给异常处理程序。

这是一个代码示例:

LONG WINAPI MyUnhandledExceptionFilter(PEXCEPTION_POINTERS pExceptionPtrs)
{
  // Do something, for example generate error report
  //..
  // Execute default exception handler next
  return EXCEPTION_EXECUTE_HANDLER; 
} 
void main()
{ 
  SetUnhandledExceptionFilter(MyUnhandledExceptionFilter);
  // .. some unsafe code here 
}

顶层 SEH 异常处理程序适用于调用进程的所有线程,因此只需在 main() 函数的开头调用一次即可。

顶层 SEH 异常处理程序在发生异常的线程的上下文中被调用。这可能会影响异常处理程序从某些异常(如无效堆栈)中恢复的能力。

如果您的异常处理程序函数位于 DLL 中,您在使用 SetUnhandledExceptionFilter() 函数时应小心。如果在崩溃时卸载了 DLL,行为可能会不可预测。

注意: 在 Windows 7 中,有一个新函数 RaiseFailFastException()。此函数允许忽略所有已安装的异常处理程序(SEH 或向量化),并将异常直接传递给 Dr. Watson。通常,当您的应用程序处于不良状态并希望立即终止应用程序并创建 Windows 错误报告时,您会调用此函数。有关更多信息,请参阅下面的参考部分。

向量化异常处理

向量化异常处理 (VEH) 是结构化异常处理的扩展。它在 Windows XP 中引入。

要添加向量化异常处理程序,您可以使用 AddVectoredExceptionHandler() 函数。缺点是 VEH 仅在 Windows XP 及更高版本中可用,因此应在运行时检查 AddVectoredExceptionHandler() 函数是否存在。

要删除先前安装的处理程序,请使用 RemoveVectoredExceptionHandler() 函数。

VEH 允许监视或处理应用程序的所有 SEH 异常。为了保持向后兼容性,当程序某部分发生 SEH 异常时,系统会依次调用已安装的 VEH 处理程序,然后搜索常规 SEH 处理程序。

VEH 的一个优点是能够链接异常处理程序,因此如果有人在您的处理程序之上安装了向量化异常处理程序,您仍然可以拦截异常。

向量化异常处理适用于您需要监视所有 SEH 异常的情况,就像调试器那样。但问题是您必须决定处理哪些异常,以及跳过哪些异常。在程序的代码中,某些异常可能被 __try{}__except(){} 结构故意保护,而在 VEH 中处理这些异常而不将其传递给基于帧的 SEH 处理程序,可能会在应用程序逻辑中引入 bug。

我认为 SetUnhandledExceptionFilter() 函数比 VEH 更适合异常处理,因为它是顶层 SEH 处理程序。如果没有人处理异常,就会调用顶层 SEH 处理程序,您无需决定是跳过异常还是不跳过。

CRT 错误处理

除了 SEH 异常和 C++ 类型异常之外,C 运行时库 (CRT) 还提供了自己的错误处理机制,您的程序应予以考虑。当发生 CRT 错误时,您通常会看到 CRT 错误消息窗口(图 3)。

图 3 - CRT 错误消息

invparam_error.png

终止处理程序

当 CRT 遇到未处理的 C++ 类型异常时,它会调用 terminate() 函数。要拦截此类调用并采取适当的操作,您应使用 set_terminate() 函数设置错误处理程序。

这是一个代码示例:

void my_terminate_handler()
{
  // Abnormal program termination (terminate() function was called)
  // Do something here
  // Finally, terminate program
  exit(1); 
}
void main()
{
  set_terminate(my_terminate_handler);
  terminate();
}

有一个 unexpected() 函数,在当前 Visual C++ 异常处理实现中未使用。但是,请考虑使用 set_unexpected() 函数为 unexpected() 函数也设置一个处理程序。

注意:在多线程环境中,unexpected 和 terminate 函数是为每个线程单独维护的。每个新线程都需要安装自己的 unexpected 和 terminate 函数。因此,每个线程都负责自己的 unexpected 和 terminate 处理。

纯虚函数调用处理程序

使用 _set_purecall_handler() 函数来处理纯虚函数调用。此函数可用于 VC++ .NET 2003 及更高版本。此函数适用于调用进程的所有线程。

这是一个代码示例(摘自 MSDN):

// _set_purecall_handler.cpp
// compile with: /W1
#include <tchar.h>
#include <stdio.h>
#include <stdlib.h>
class CDerived;
class CBase
{
public:
   CBase(CDerived *derived): m_pDerived(derived) {};
   ~CBase();
   virtual void function(void) = 0;
   CDerived * m_pDerived;
};
class CDerived : public CBase
{
public:
   CDerived() : CBase(this) {};   // C4355
   virtual void function(void) {};
};
CBase::~CBase()
{
   m_pDerived -> function();
}
void myPurecallHandler(void)
{
   printf("In _purecall_handler.");
   exit(0);
}
int _tmain(int argc, _TCHAR* argv[])
{
   _set_purecall_handler(myPurecallHandler);
   CDerived myDerived;
}

New 运算符故障处理程序

使用 _set_new_handler() 函数来处理内存分配故障。此函数可用于 VC++ .NET 2003 及更高版本。此函数适用于调用进程的所有线程。考虑使用 _set_new_mode() 函数定义 malloc() 函数的错误行为。

这是一个代码示例(摘自 MSDN):

#include <new.h>
int handle_program_memory_depletion( size_t )
{
   // Your code
}
int main( void )
{
   _set_new_handler( handle_program_memory_depletion );
   int *pi = new int[BIG_NUMBER];
}

无效参数处理程序

当 CRT 在系统函数调用中检测到无效参数时,使用 _set_invalid_parameter_handler() 函数来处理这种情况。此函数可用于 VC++ 2005 及更高版本。此函数适用于调用进程的所有线程。

这是一个代码示例(摘自 MSDN):

// crt_set_invalid_parameter_handler.c
// compile with: /Zi /MTd
#include <stdio.h>
#include <stdlib.h>
#include <crtdbg.h>  // For _CrtSetReportMode
void myInvalidParameterHandler(const wchar_t* expression,
   const wchar_t* function, 
   const wchar_t* file, 
   unsigned int line, 
   uintptr_t pReserved)
{
   wprintf(L"Invalid parameter detected in function %s."
            L" File: %s Line: %d\n", function, file, line);
   wprintf(L"Expression: %s\n", expression);
}
int main( )
{
   char* formatString;
   _invalid_parameter_handler oldHandler, newHandler;
   newHandler = myInvalidParameterHandler;
   oldHandler = _set_invalid_parameter_handler(newHandler);
   // Disable the message box for assertions.
   _CrtSetReportMode(_CRT_ASSERT, 0);
   // Call printf_s with invalid parameters.
   formatString = NULL;
   printf(formatString);
}

C++ 信号处理

C++ 提供了一种称为信号的程序中断机制。您可以使用 signal() 函数来处理信号。

在 Visual C++ 中,有六种信号类型:

  • SIGABRT 异常终止
  • SIGFPE 浮点错误
  • SIGILL 非法指令
  • SIGINT CTRL+C 信号
  • SIGSEGV 非法存储访问
  • SIGTERM 终止请求

MSDN 表示 SIGILLSIGSEGVSIGTERM 信号在 Windows 下不产生,仅为 ANSI 兼容性而包含。但是,实践表明,如果您在主线程中设置 SIGSEGV 信号处理程序,它会被 CRT 调用,而不是被 SetUnhandledExceptionFilter() 函数设置的 SEH 异常处理程序调用,并且全局变量 _pxcptinfoptrs 包含指向异常信息的指针。在其他线程中,调用的是 SetUnhandledExceptionFilter() 函数设置的异常过滤器,而不是 SIGSEGV 处理程序。

注意:在 Linux 中,信号是异常处理的主要方式(Linux 的 C 运行时实现 glibc 也提供 set_unexpected()set_terminate() 处理程序)。如您所见,在 Windows 中,信号的使用不如其应有的程度频繁。C 运行时库没有使用信号,而是提供了多个 Visual C++ 特定的错误处理函数,如 _invalid_parameter_handler() 等。

_pxcptinfoptrs 全局变量也可以在 SIGFPE 处理程序中使用。在所有其他信号处理程序中,它似乎为 NULL

当发生浮点错误(如除以零)时,CRT 会调用 SIGFPE 信号处理程序。但是,默认情况下,不生成浮点异常,而是生成 NaN 或无穷大的数字作为浮点运算的结果。使用 _controlfp_s() 函数启用浮点异常生成。

您可以使用 raise() 函数手动生成所有六个信号。

这是一个例子。

void sigabrt_handler(int)
{
  // Caught SIGABRT C++ signal
  // Terminate program
  exit(1);
}
void main()
{
  signal(SIGABRT, sigabrt_handler);
     
  // Cause abort
  abort();       
}

注意:尽管 MSDN 文档不充分,但似乎您应该为程序中的每个新线程安装 SIGFPESIGILLSIGSEGV 信号处理程序。SIGABRTSIGINTSIGTERM 信号处理程序似乎适用于调用进程的所有线程,因此您应该在 main() 函数中安装一次。

检索异常信息

当发生异常时,您通常希望获取 CPU 状态以确定导致问题的代码位置。您可能希望将此信息传递给 MiniDumpWriteDump() 函数以便稍后调试问题(有关如何执行此操作的示例,请参阅 Hans Dietrich 的文章《XCrashReport:异常处理和崩溃报告 - 第 3 部分》)。检索异常信息的方式取决于您使用的异常处理程序。

在使用 SetUnhandledExceptionFilter() 函数设置的 SEH 异常处理程序中,异常信息是从作为函数参数传递的 EXCEPTION_POINTERS 结构中检索的。

__try{}__catch(Expression){} 结构中,您可以使用 GetExceptionInformation() 内置函数检索异常信息,并将其作为参数传递给 SEH 异常处理程序函数。

SIGFPESIGSEGV 信号处理程序中,您可以从 <signal.h> 中声明的全局 CRT 变量 _pxcptinfoptrs 检索异常信息。此变量在 MSDN 中未得到充分记录。

在其他信号处理程序和 CRT 错误处理程序中,您无法轻松提取异常信息。我发现了一个 CRT 代码中使用的解决方法(请参阅 CRT 8.0 源文件,invarg.c,第 104 行)。

以下代码显示了如何获取当前 CPU 状态作为异常信息:

#if _MSC_VER>=1300
#include <rtcapi.h>
#endif
#ifndef _AddressOfReturnAddress
// Taken from: http://msdn.microsoft.com/en-us/library/s975zw7k(VS.71).aspx
#ifdef __cplusplus
#define EXTERNC extern "C"
#else
#define EXTERNC
#endif
// _ReturnAddress and _AddressOfReturnAddress should be prototyped before use 
EXTERNC void * _AddressOfReturnAddress(void);
EXTERNC void * _ReturnAddress(void);
#endif 
// The following function retrieves exception info
void GetExceptionPointers(DWORD dwExceptionCode, 
  EXCEPTION_POINTERS** ppExceptionPointers)
{
  // The following code was taken from VC++ 8.0 CRT (invarg.c: line 104)
  
  EXCEPTION_RECORD ExceptionRecord;
  CONTEXT ContextRecord;
  memset(&ContextRecord, 0, sizeof(CONTEXT));
  
#ifdef _X86_
  __asm {
      mov dword ptr [ContextRecord.Eax], eax
      mov dword ptr [ContextRecord.Ecx], ecx
      mov dword ptr [ContextRecord.Edx], edx
      mov dword ptr [ContextRecord.Ebx], ebx
      mov dword ptr [ContextRecord.Esi], esi
      mov dword ptr [ContextRecord.Edi], edi
      mov word ptr [ContextRecord.SegSs], ss
      mov word ptr [ContextRecord.SegCs], cs
      mov word ptr [ContextRecord.SegDs], ds
      mov word ptr [ContextRecord.SegEs], es
      mov word ptr [ContextRecord.SegFs], fs
      mov word ptr [ContextRecord.SegGs], gs
      pushfd
      pop [ContextRecord.EFlags]
  }
  ContextRecord.ContextFlags = CONTEXT_CONTROL;
#pragma warning(push)
#pragma warning(disable:4311)
  ContextRecord.Eip = (ULONG)_ReturnAddress();
  ContextRecord.Esp = (ULONG)_AddressOfReturnAddress();
#pragma warning(pop)
  ContextRecord.Ebp = *((ULONG *)_AddressOfReturnAddress()-1);
#elif defined (_IA64_) || defined (_AMD64_)
  /* Need to fill up the Context in IA64 and AMD64. */
  RtlCaptureContext(&ContextRecord);
#else  /* defined (_IA64_) || defined (_AMD64_) */
  ZeroMemory(&ContextRecord, sizeof(ContextRecord));
#endif  /* defined (_IA64_) || defined (_AMD64_) */
  ZeroMemory(&ExceptionRecord, sizeof(EXCEPTION_RECORD));
  ExceptionRecord.ExceptionCode = dwExceptionCode;
  ExceptionRecord.ExceptionAddress = _ReturnAddress();
  
  EXCEPTION_RECORD* pExceptionRecord = new EXCEPTION_RECORD;
  memcpy(pExceptionRecord, &ExceptionRecord, sizeof(EXCEPTION_RECORD));
  CONTEXT* pContextRecord = new CONTEXT;
  memcpy(pContextRecord, &ContextRecord, sizeof(CONTEXT));
  *ppExceptionPointers = new EXCEPTION_POINTERS;
  (*ppExceptionPointers)->ExceptionRecord = pExceptionRecord;
  (*ppExceptionPointers)->ContextRecord = pContextRecord;  
}

异常处理和 CRT 链接

您的应用程序中的每个模块(EXE、DLL)都链接到 CRT(C 运行时库)。您可以将 CRT 链接为多线程静态库,或链接为多线程动态链接库。当您设置 CRT 错误处理程序(如终止处理程序、unexpected 处理程序、纯虚函数调用处理程序、无效参数处理程序、new 运算符错误处理程序或信号处理程序)时,它们将适用于调用模块链接到的 CRT,而不会拦截不同 CRT 模块(如果存在)中的异常,因为每个 CRT 模块都有自己的内部状态。

多个项目模块可以共享单个 CRT DLL。这最大限度地减少了链接 CRT 代码的总体大小。并且该 CRT DLL 中的所有异常都可以一次性处理。这就是为什么多线程 CRT DLL 是 CRT 链接的推荐方式。然而,许多开发人员仍然偏爱静态 CRT 链接,因为它比分发与多个动态链接 CRT 库链接的同一个可执行文件更容易分发静态链接的单个可执行模块(有关更多信息,请参阅 Martin Richter 的文章《使用私有 MFC、ATL 和 CRT 程序集轻松创建项目》)。

如果您打算将 CRT 用作静态链接库(不推荐)并想使用某些异常处理功能,则必须将该功能构建为静态库,并使用 /NODEFAULTLIB 链接器标志,然后将此功能链接到应用程序的每个 EXE 和 DLL 模块。您还必须为应用程序的每个模块安装 CRT 错误处理程序,而 SEH 异常处理程序仍然只需要安装一次。

Visual C++ 编译器标志

有几个 Visual C++ 编译器开关与异常处理相关。您可以在打开项目属性->配置属性->C/C++->代码生成时找到这些开关。

异常处理模型

您可以使用 /EHs(或 EHsc)为 Visual C++ 编译器设置异常处理模型,以指定同步异常处理模型;或者使用 /EHa 来指定异步异常处理模型。异步模型可用于强制 try{}catch(){} 结构捕获 SEH 和 C++ 类型异常(使用 _set_se_translator() 函数也可以达到相同效果)。如果使用同步模型,SEH 异常不会被 try{}catch(){} 结构捕获。异步模型是早期 Visual C++ 版本的默认设置,而同步模型是较新版本的默认设置。

浮点异常

您可以使用/fp:except 编译器标志启用浮点异常。此选项默认禁用,因此不会引发浮点异常。有关更多信息,请参阅下面的参考部分中的/fp(指定浮点行为)

缓冲区安全检查

默认情况下,您启用了/GS(缓冲区安全检查)编译器标志,该标志强制编译器注入代码来检查缓冲区溢出。缓冲区溢出是指将大量数据写入小缓冲区的情况。

注意:在 Visual C++ .NET (CRT 7.1) 中,您可以使用 CRT 在检测到缓冲区溢出时调用的 _set_security_error_handler() 函数。但是,此函数在 CRT 的后期版本中已弃用。

自 CRT 8.0 起,您无法在代码中拦截缓冲区溢出错误。当检测到缓冲区溢出时,CRT 会直接调用 Dr. Watson,而不是调用未处理异常过滤器。这是出于安全原因,Microsoft 也不打算更改此行为。有关更多信息,请参阅以下链接:

Using the Code

文章附带了一个小型控制台演示应用程序ExceptionHandler。该演示可以引发和捕获不同类型的异常,并生成崩溃的小转储文件。应用程序显示在下图:

图 4 - ExceptionHandler 示例应用程序

exception_handler_demo.png

要查看其工作原理,您需要选择一个异常类型(输入 0 到 13 之间的数字)并按 Enter。然后将引发一个异常,并由异常处理程序捕获。然后,异常处理程序会调用使用 dbghelp.dll 中的 MiniDumpWriteDump() 函数生成崩溃小转储文件的代码。小转储文件将写入当前文件夹,并命名为 crashdump.dmp。您可以双击该文件在 Visual Studio 中打开它,然后按 F5 运行它,以查看发生异常的代码行。

应用程序中有两个逻辑部分:引发异常的部分和捕获异常并生成崩溃小转储的部分。这两个部分将在下面介绍。

文件 main.cpp 包含 main() 函数,其中包含异常类型选择代码和一个大 switch 语句来选择要引发的异常。要引发异常,主函数可能会调用 raise()RaiseException() 函数,抛出 C++ 类型异常,调用一些辅助代码,如 RecurseAlloc()sigfpe_test() 等。

main() 函数的开头,会创建一个 CCrashHandler 类的实例。稍后使用此实例来捕获异常。我们调用 CCrashHandler::SetProcessExceptionHandlers() 方法来设置适用于整个进程的异常处理程序(如 SEH 异常处理程序)。我们调用 CCrashHandler::SetThreadExceptionHandlers() 方法来安装仅适用于当前线程的异常处理程序(如 unexpected 或 terminate 处理程序)。

下面展示了 main 函数的代码:

void main()
{
    CCrashHandler ch;
    ch.SetProcessExceptionHandlers();
    ch.SetThreadExceptionHandlers();
    
    printf("Choose an exception type:\n");
    printf("0 - SEH exception\n");
    printf("1 - terminate\n");
    printf("2 - unexpected\n");
    printf("3 - pure virtual method call\n");
    printf("4 - invalid parameter\n");
    printf("5 - new operator fault\n");    
    printf("6 - SIGABRT\n");
    printf("7 - SIGFPE\n");
    printf("8 - SIGILL\n");
    printf("9 - SIGINT\n");
    printf("10 - SIGSEGV\n");
    printf("11 - SIGTERM\n");
    printf("12 - RaiseException\n");
    printf("13 - throw C++ typed exception\n");
    printf("Your choice >  ");

    int ExceptionType = 0;
    scanf_s("%d", &ExceptionType);

    switch(ExceptionType)
    {
    case 0: // SEH
        {
            // Access violation
            int *p = 0;
#pragma warning(disable : 6011)
// warning C6011: Dereferencing NULL pointer 'p'
            *p = 0;
#pragma warning(default : 6011)   
        }
        break;
    case 1: // terminate
        {
            // Call terminate
            terminate();
        }
        break;
    case 2: // unexpected
        {
            // Call unexpected
            unexpected();
        }
        break;
    case 3: // pure virtual method call
        {
            // pure virtual method call
            CDerived derived;
        }
        break;
    case 4: // invalid parameter
        {      
            char* formatString;
            // Call printf_s with invalid parameters.
            formatString = NULL;
#pragma warning(disable : 6387)
// warning C6387: 'argument 1' might be '0': this does
// not adhere to the specification for the function 'printf'
            printf(formatString);
#pragma warning(default : 6387)   

        }
        break;
    case 5: // new operator fault
        {
            // Cause memory allocation error
            RecurseAlloc();
        }
        break;
    case 6: // SIGABRT 
        {
            // Call abort
            abort();
        }
        break;
    case 7: // SIGFPE
        {
            // floating point exception ( /fp:except compiler option)
            sigfpe_test();            
        }    
        break;
    case 8: // SIGILL 
        {
            raise(SIGILL);              
        }    
        break;
    case 9: // SIGINT 
        {
            raise(SIGINT);              
        }    
        break;
    case 10: // SIGSEGV 
        {
            raise(SIGSEGV);              
        }    
        break;
    case 11: // SIGTERM
        {
            raise(SIGTERM);            
        }
        break;
    case 12: // RaiseException 
        {
            // Raise noncontinuable software exception
            RaiseException(123, EXCEPTION_NONCONTINUABLE, 0, NULL);        
        }
        break;
    case 13: // throw 
        {
            // Throw typed C++ exception.
            throw 13;
        }
        break;
    default:
        {
            printf("Unknown exception type specified.");    
            _getch();
        }
        break;
    }
}

文件 CrashHandler.hCrashHandler.cpp 包含异常处理和崩溃小转储生成功能的实现。下面展示了类的声明:

class CCrashHandler  
{
public:

    // Constructor
    CCrashHandler();

    // Destructor
    virtual ~CCrashHandler();

    // Sets exception handlers that work on per-process basis
    void SetProcessExceptionHandlers();

    // Installs C++ exception handlers that function on per-thread basis
    void SetThreadExceptionHandlers();

    // Collects current process state.
    static void GetExceptionPointers(
        DWORD dwExceptionCode, 
        EXCEPTION_POINTERS** pExceptionPointers);

    // This method creates minidump of the process
    static void CreateMiniDump(EXCEPTION_POINTERS* pExcPtrs);

    /* Exception handler functions. */

    static LONG WINAPI SehHandler(PEXCEPTION_POINTERS pExceptionPtrs);
    static void __cdecl TerminateHandler();
    static void __cdecl UnexpectedHandler();

    static void __cdecl PureCallHandler();

    static void __cdecl InvalidParameterHandler(const wchar_t* expression, 
           const wchar_t* function, const wchar_t* file, 
           unsigned int line, uintptr_t pReserved);

    static int __cdecl NewHandler(size_t);

    static void SigabrtHandler(int);
    static void SigfpeHandler(int /*code*/, int subcode);
    static void SigintHandler(int);
    static void SigillHandler(int);
    static void SigsegvHandler(int);
    static void SigtermHandler(int);
};

如上代码所示,CCrashHandler 类有两个设置异常处理程序的方法:SetProcessExceptionHandlers()SetThreadExceptionHandlers(),分别用于整个进程和当前线程。这两个方法的代码如下所示:

void CCrashHandler::SetProcessExceptionHandlers()
{
    // Install top-level SEH handler
    SetUnhandledExceptionFilter(SehHandler);    

    // Catch pure virtual function calls.
    // Because there is one _purecall_handler for the whole process, 
    // calling this function immediately impacts all threads. The last 
    // caller on any thread sets the handler. 
    // http://msdn.microsoft.com/en-us/library/t296ys27.aspx
    _set_purecall_handler(PureCallHandler);    

    // Catch new operator memory allocation exceptions
    _set_new_handler(NewHandler);

    // Catch invalid parameter exceptions.
    _set_invalid_parameter_handler(InvalidParameterHandler); 

    // Set up C++ signal handlers

    _set_abort_behavior(_CALL_REPORTFAULT, _CALL_REPORTFAULT);

    // Catch an abnormal program termination
    signal(SIGABRT, SigabrtHandler);  

    // Catch illegal instruction handler
    signal(SIGINT, SigintHandler);     

    // Catch a termination request
    signal(SIGTERM, SigtermHandler);          
}

void CCrashHandler::SetThreadExceptionHandlers()
{

    // Catch terminate() calls. 
    // In a multithreaded environment, terminate functions are maintained 
    // separately for each thread. Each new thread needs to install its own 
    // terminate function. Thus, each thread is in charge of its own termination handling.
    // http://msdn.microsoft.com/en-us/library/t6fk7h29.aspx
    set_terminate(TerminateHandler);       

    // Catch unexpected() calls.
    // In a multithreaded environment, unexpected functions are maintained 
    // separately for each thread. Each new thread needs to install its own 
    // unexpected function. Thus, each thread is in charge of its own unexpected handling.
    // http://msdn.microsoft.com/en-us/library/h46t5b69.aspx  
    set_unexpected(UnexpectedHandler);    

    // Catch a floating point error
    typedef void (*sigh)(int);
    signal(SIGFPE, (sigh)SigfpeHandler);     

    // Catch an illegal instruction
    signal(SIGILL, SigillHandler);     

    // Catch illegal storage access errors
    signal(SIGSEGV, SigsegvHandler);   

}

在类声明中,您还可以看到几个异常处理程序函数,如 SehHandler()TerminateHandler() 等。发生异常时,可以调用这些异常处理程序中的任何一个。处理程序函数(可选地)检索异常信息并调用崩溃小转储生成代码,然后使用 TerminateProcess() 函数调用终止进程。

静态方法 GetExceptionPointers() 用于检索异常信息。我在检索异常信息部分描述了这一点。

CreateMiniDump() 方法用于生成崩溃小转储。它接受一个指向包含异常信息的 EXCEPTION_POINTERS 结构的指针。该方法调用 Microsoft 调试帮助库中的 MiniDumpWriteDump() 函数来生成小转储文件。该方法代码如下所示:

// This method creates minidump of the process
void CCrashHandler::CreateMiniDump(EXCEPTION_POINTERS* pExcPtrs)
{   
    HMODULE hDbgHelp = NULL;
    HANDLE hFile = NULL;
    MINIDUMP_EXCEPTION_INFORMATION mei;
    MINIDUMP_CALLBACK_INFORMATION mci;
    
    // Load dbghelp.dll
    hDbgHelp = LoadLibrary(_T("dbghelp.dll"));
    if(hDbgHelp==NULL)
    {
        // Error - couldn't load dbghelp.dll
        return;
    }

    // Create the minidump file
    hFile = CreateFile(
        _T("crashdump.dmp"),
        GENERIC_WRITE,
        0,
        NULL,
        CREATE_ALWAYS,
        FILE_ATTRIBUTE_NORMAL,
        NULL);

    if(hFile==INVALID_HANDLE_VALUE)
    {
        // Couldn't create file
        return;
    }
   
    // Write minidump to the file
    mei.ThreadId = GetCurrentThreadId();
    mei.ExceptionPointers = pExcPtrs;
    mei.ClientPointers = FALSE;
    mci.CallbackRoutine = NULL;
    mci.CallbackParam = NULL;

    typedef BOOL (WINAPI *LPMINIDUMPWRITEDUMP)(
        HANDLE hProcess, 
        DWORD ProcessId, 
        HANDLE hFile, 
        MINIDUMP_TYPE DumpType, 
        CONST PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam, 
        CONST PMINIDUMP_USER_STREAM_INFORMATION UserEncoderParam, 
        CONST PMINIDUMP_CALLBACK_INFORMATION CallbackParam);

    LPMINIDUMPWRITEDUMP pfnMiniDumpWriteDump = 
        (LPMINIDUMPWRITEDUMP)GetProcAddress(hDbgHelp, "MiniDumpWriteDump");
    if(!pfnMiniDumpWriteDump)
    {    
        // Bad MiniDumpWriteDump function
        return;
    }

    HANDLE hProcess = GetCurrentProcess();
    DWORD dwProcessId = GetCurrentProcessId();

    BOOL bWriteDump = pfnMiniDumpWriteDump(
        hProcess,
        dwProcessId,
        hFile,
        MiniDumpNormal,
        &mei,
        NULL,
        &mci);

    if(!bWriteDump)
    {    
        // Error writing dump.
        return;
    }

    // Close file
    CloseHandle(hFile);

    // Unload dbghelp.dll
    FreeLibrary(hDbgHelp);
}

参考文献

历史

  • 2011 年 6 月 7 日 - 首次发布。
  • 2011 年 6 月 10 日 - 添加了 Visual C++ 编译器标志和“参考”部分。
  • 2012 年 1 月 6 日 - 添加了 ExceptionHandler 示例代码并添加了“使用代码”部分。
© . All rights reserved.