Visual C++中的异常处理

简介

本文介绍了在Windows中运行的VisualC++程序中处理异常和错误的标准技术。
异常(或严重错误或崩溃)通常意味着程序停止正常工作,需要停止执行。例如,由于程序访问无效的内存地址(如空指针)、无法分配内存缓冲区(内存不足)、C运行时库(CRT)检测到错误并请求程序终止等,可能会发生异常。
C++程序可以处理几种例外:SEH异常,通过操作系统的结构化异常处理机制产生,由C运行库产生的CRT错误,最后是信号。每种错误类型都需要安装异常处理程序函数,该函数将截获异常并执行一些错误恢复操作。
如果应用程序有多个执行线程,事情可能会更复杂。有些异常处理程序可用于整个进程,但有些仅用于当前线程。所以必须在每个线程中安装异常处理程序。
应用程序中的每个模块(EXE或DLL)都链接到CRT库(静态或动态)。异常处理技术在很大程度上依赖于CRT链接类型。
错误类型的多样性、在多线程程序中处理异常的差异,以及异常处理对CRT链接的依赖性,需要大量的工作来处理应用程序允许处理的所有异常。本文旨在帮助您更好地理解异常处理机制,并在C++应用程序中有效地使用异常处理。
本文附带了一个小型控制台演示应用程序ExceptionHandler。演示程序可以引发和捕获不同类型的异常,并生成一个崩溃小型转储文件,允许查看发生异常的代码行。

背景

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

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

也许您已经看到Windows错误报告窗口(图2)突然出现在您的桌面上,CrashRpt库也做了同样的事情,只是它将错误报告发送到您自己的web服务器,而不是Microsoft的服务器。

浏览MSDN时,我得到了SetUnhandledExceptionFilter()函数,该函数用于处理访问冲突。但很快我发现我的应用程序中的一些异常不知怎么地没有被处理,而Watson博士的窗口仍然出现,而不是崩溃的窗口。我又浏览了MSDN,发现许多其他CRT提供的函数可以用来处理CRT错误。下面是此类函数的一些示例:set_terminate(),_set_invalid_parameter_handler(),_set_purecall_handler()。然后我发现有些CRT处理程序只对当前线程有效,但有些处理程序对进程的所有线程都有效。继续我的研究,我发现开发人员必须理解许多细微差别才能有效地使用异常处理。我的研究结果如下。

关于例外的几句话

如您所知,异常或严重错误通常意味着程序停止正常工作,需要停止其执行。例如,可能由于以下原因发生异常:

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

有两种例外,它们有不同的性质:SEH异常(结构化异常处理、SEH)和类型化C++异常。操作系统提供结构化异常处理机制(这意味着所有Windows应用程序都可以引发和处理SEH异常)。SEH例外最初是为C语言设计的,但它们也可以用在C++中。SEH异常是使用_try{}_except(){}构造处理的。程序的main()函数由这样的构造保护,因此默认情况下,所有未处理的SEH异常都会被捕获并调用Dr.Watson。SEH异常是VisualC++编译器特有的。如果编写可移植代码,则应使用#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 {}构造来处理。下面是一个例子

// 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异常都有一个关联的异常代码。您可以使用GetExceptionCode()内部函数提取exception语句内部的异常代码。可以使用GetExceptionInformation()内部函数提取exception语句内部的异常信息。要使用这些内在函数,通常要创建自定义异常筛选器,如下例所示。

以下示例演示如何使用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){}构造的缺点是,您可能忘记保护可能导致程序无法处理异常的潜在错误代码。使用带有SetUnhandledExceptionFilter()函数的top-level未处理异常筛选器集可以捕获此类未处理的SEH异常。
注意:单词top-level表示如果有人在您的调用之后调用SetUnhandledExceptionFilter()函数,则将替换异常筛选器。这是一个缺点,因为不能将顶级处理程序相互链接。这种缺点可以通过后面讨论的矢量异常处理机制来消除。异常信息(异常发生前的CPU状态)通过异常指针结构传递给异常处理程序。
下面是一个代码示例:

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 
}

top-level SEH异常处理程序适用于调用方进程的所有线程,因此在main()函数的开头调用一次就足够了。在发生异常的线程的上下文中调用顶级SEH异常处理程序。这可能会影响异常处理程序从某些异常(例如无效堆栈)中恢复的能力。如果异常处理程序函数位于DLL内部,则在使用SetUnhandledExceptionFilter()函数时应小心。如果在崩溃时卸载了DLL,则行为可能是不可预测的。
注意:在Windows7中,有一个新函数RaiseFailFastException()。此函数允许忽略所有已安装的异常处理程序(SEH或vectored),并将异常直接传递给Watson博士。通常,如果应用程序处于错误状态,并且希望立即终止应用程序并创建Windows错误报告,则调用此函数。

矢量异常处理

矢量异常处理(VEH)是结构化异常处理的扩展。它是在Windows XP中引入的。要添加矢量化异常处理程序,可以使用AddVectoredExceptionHandler()函数。缺点是VEH只在Windows XP和更高版本中可用,因此应该在运行时检查AddVectoredExceptionHandler()函数的存在。要删除以前安装的处理程序,请使用removeVectorDexceptionHandler()函数。

VEH允许监视或处理应用程序的所有SEH异常。为了保持向后兼容性,当程序的某些部分发生SEH异常时,系统依次调用已安装的VEH处理程序,然后搜索通常的SEH处理程序。VEH的一个优点是能够链接异常处理程序,因此如果有人在您的上面安装了一个向量化的异常处理程序,您仍然可以拦截异常。当需要监视所有异常时,矢量异常处理是合适的,就像调试器一样。但问题是您必须决定要处理哪个异常和跳过哪个异常。在程序代码中,一些异常可能被一个__try{}__except(){}构造有意保护,并且通过在VEH中处理这些异常而不将其传递给基于帧的SEH处理程序,您可能会在应用程序逻辑中引入错误。
我认为SetUnhandledExceptionFilter()函数比VEH更适合异常处理,因为它是顶级SEH处理程序。如果没有人处理异常,则调用顶级SEH处理程序,您不需要决定是否应跳过异常。

CRT Error Handling

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

Terminate Handler

当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()函数不用于当前VisualC++异常处理的实现。但是,也可以考虑使用set_unexpected()函数为unexpected()函数设置处理程序。注意:在多线程环境中,每个线程分别维护意外和终止函数。每个新线程都需要安装自己的意外终止函数。因此,每个线程负责自己的意外和终止处理。

Pure Call Handler

使用_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 Operator Fault Handler

使用_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];
}

Invalid Parameter Handler

当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 Abnormal termination
  • SIGFPE Floating-point error
  • SIGILL Illegal instruction
  • SIGINT CTRL+C signal
  • SIGSEGV Illegal storage access
  • SIGTERM Termination request

MSDN说,SIGILL、SIGSEGV和SIGTERM信号不是在Windows下生成的,并包含在ANSI兼容性中。但是,实践表明,如果在主线程中设置SIGSEGV信号处理程序,CRT将调用它,而不是使用SetUnhandledExceptionFilter()函数设置SEH异常处理程序,并且全局变量pxcptinfoptrs包含指向异常信息的指针。在其他线程中,使用SetUnhandledExceptionFilter()函数调用异常筛选器集而不是SIGSEGV处理程序。

注意:在Linux中,信号是异常处理的主要方式(Linux的C运行时实现glibc还提供了set_unexpected()和set_terminate()处理程序)。如您所见,在Windows中,信号的使用并没有达到应有的密集程度。代替运行信号,C运行时库提供了一些VisualC++特定的错误处理函数,例如,_invalid_parameter_handler()等。pxcptinfoptrs全局变量也可以在SIGFPE处理程序中使用。在所有其他信号处理程序中,它似乎为空。当出现浮点错误(如被零除)时,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中没有很好的文档,但是似乎应该为程序中的每个新线程安装SIGFPE、SIGILL和SIGSEGV信号处理程序。SIGABRT、SIGINT和SIGTERM信号处理程序似乎适用于调用方进程的所有线程,因此应该在main()函数中安装一次。

检索异常信息

当发生异常时,通常需要获取CPU状态来确定导致问题的代码位置。您可能希望将此信息传递给MiniDumpWriteDump()函数,以便稍后调试该问题。检索异常信息的方式因使用的异常处理程序而异。在使用SetUnhandledExceptionFilter()函数设置的SEH异常处理程序中,将从作为函数参数传递的异常指针结构中检索异常信息。
__try{}__catch(Expression){}构造中,使用GetExceptionInformation()内部函数检索异常信息,并将其作为参数传递给SEH异常筛选器函数。在SIGFPE和SIGSEGV信号处理程序中,可以从<signal.h>中声明的pxcptinfoptrs全局CRT变量检索异常信息。这个变量在MSDN中没有很好的记录。在其他信号处理程序和CRT错误处理程序中,您无法轻松提取异常信息。我在CRT代码中找到了一个解决方法(参见CRT 8.0源文件,invag.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错误处理程序(如终止处理程序、意外处理程序、纯调用处理程序、无效参数处理程序、新的运算符错误处理程序或信号处理程序)时,它们将适用于调用方模块链接到的CRT,并且不会拦截不同CRT模块(如果存在)中的异常,因为每个CRT模块都有自己的内部状态。
几个项目模块可以共享一个CRT DLL。这将使链接的CRT代码的总体大小减小到最小。CRT DLL中的所有异常都可以同时处理。这就是为什么多线程CRT DLL是推荐的CRT链接方式。但是,许多开发人员仍然喜欢静态CRT链接,因为与分发与CRT静态链接的单个可执行模块相比,更容易分发与多个动态链接的CRT库链接的同一个可执行文件。
如果计划将CRT用作静态链接库(不推荐使用),并且希望使用某些异常处理功能,则必须将该功能构建为带有/NODEFAULTLIB链接器标志的静态库,然后将该功能链接到应用程序的每个EXE和DLL模块。您还必须为应用程序的每个模块安装CRT错误处理程序,而SEH异常处理程序仍将安装一次。

Visual C++ Compiler Flags

有几种与异常处理相关的Visual C++编译器开关。如果打开Project属性->配置属性> C/C++/代码生成,您可以找到开关。

异常处理模型

您可以为VisualC++编译器使用/EHS(或EHSC)设置异常处理模型,以指定同步异常处理模型,或/EHA指定异步异常处理模型。异步模型可以用来强制try{}catch(){}结构来捕获SEH和C++类型的异常(可以用_set_se_translator()函数实现相同的效果)。如果使用同步模型,则try{}catch(){}构造不会捕获SEH异常。异步模型是VisualC++中以前版本中的默认值,但同步版本是新版本中的默认值。

Floating Point Exceptions

可以使用/fp:except编译器标志启用浮点异常。默认情况下禁用此选项,因此不会引发浮点异常。

缓冲区安全检查

默认情况下,您启用了/GS(Buffer Security Check)编译器标志,强制编译器插入检查缓冲区溢出的代码。缓冲区溢出是将大数据块写入小缓冲区的情况。注意,在VisualC++.NET(CRT 7.1)中,可以使用在检测到缓冲区溢出时CRT调用的SyStSuxSypLogyErrRoC++ HANDLE()函数。但是,在CRT的较新版本中不推荐使用此函数。
自CRT8.0以来,您无法拦截代码中的缓冲区溢出错误。当检测到缓冲区溢出时,CRT直接调用Dr.Watson,而不是调用未处理的异常筛选器。这是因为安全原因,微软不打算改变这种行为。

猜你喜欢

转载自www.cnblogs.com/yilang/p/12366962.html