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,而不是調用未處理的異常篩選器。這是因為安全原因,微軟不打算改變這種行為。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM