探索 Windows 平臺下的 C++ 異常捕獲策略:如何讓Windows C++應用程序盡可能捕獲所有異常?

前言

這個標題起的有點糾結,感覺不太好起。實際上本文想要討論的場景,是一個比較經(jīng)典的Windows C++商業(yè)應用軟件的開發(fā)需求:我們希望能夠在程序發(fā)生異常并崩潰時,能夠彈出對用戶比較優(yōu)化的崩潰提示窗口,并且生成dump文件上傳到服務器上,讓開發(fā)人員能夠獲取并分析。

因此,本文提出一套捕獲Windows平臺下C++程序異常的方案,經(jīng)過長時間的線上驗證,是可以捕獲到絕大多數(shù)的異常的。至于為什么不是所有異常,我們后面再討論。

程序示例

先給出程序示例,再討論其中的原理。

void InstallUnexceptedExceptionHandler()
{
    //SEH(Windows 結構化異常處理),屬于Win32 API
    ::SetUnhandledExceptionFilter(UnhandledStructuredException);
    //C 運行時庫 (CRT) 異常處理,由 CRT 提供的異常處理機制。
    _set_purecall_handler(PureCallHandler);
    _set_new_handler(NewHandler);
    _set_invalid_parameter_handler(InvalidParameterHandler); 
    _set_abort_behavior(_CALL_REPORTFAULT, _CALL_REPORTFAULT);
    //C 運行時信號處理,由 CRT 提供的信號處理機制。
    signal(SIGABRT, SigabrtHandler);
    signal(SIGINT, SigintHandler);
    signal(SIGTERM, SigtermHandler);
    signal(SIGILL, SigillHandler);
    //C++ 運行時異常處理,API由標準庫提供
    set_terminate(TerminateHandler);
    set_unexpected(UnexpectedHandler);
}

可以看到,這些函數(shù)調(diào)用都會傳入一個回調(diào)函數(shù),比如UnhandledStructuredException、PureCallHandler等。這些回調(diào)函數(shù)在項目中,實際只是起到轉發(fā)作用,最后會調(diào)用到統(tǒng)一的異常處理函數(shù)中,進行我們想要的統(tǒng)一邏輯,包括彈出用戶友好的崩潰提示界面,并生成dump文件等。這主要是因為這些API需要的回調(diào)函數(shù)簽名不一致,需要程序員定義各自需要的回調(diào)函數(shù),再在各自的回調(diào)函數(shù)中調(diào)用統(tǒng)一的異常處理函數(shù)。在各自的回調(diào)函數(shù)中調(diào)用統(tǒng)一的異常處理函數(shù),并彈出用戶友好的崩潰提示界面,并生成dump文件等程序邏輯,這里不進行羅列,這里只進行異常捕獲機制相關的討論。

原理簡介

這段程序使用了多種技術來捕獲異常??梢詫λ鼈冞M行分類,并解釋它們是由哪個技術層面提供的:

  1. Windows 結構化異常處理 (SEH):由操作系統(tǒng)提供的異常處理機制。
  • SetUnhandledExceptionFilter(UnhandledStructuredException): 為程序設置一個未處理的結構化異常過濾器,當發(fā)生 Windows 結構化異常時(如訪問違規(guī)、整數(shù)溢出等),該過濾器會被調(diào)用。
  1. C 運行時庫 (CRT) 異常處理:由 CRT 提供的異常處理機制。
  • _set_purecall_handler(PureCallHandler): 設置一個純虛函數(shù)調(diào)用處理程序,當調(diào)用純虛函數(shù)時(未實現(xiàn)的虛函數(shù)),該處理程序會被調(diào)用。
  • _set_new_handler(NewHandler): 設置一個內(nèi)存分配失敗的處理程序,當 new 運算符無法分配內(nèi)存時,該處理程序會被調(diào)用。
  • _set_invalid_parameter_handler(InvalidParameterHandler): 設置一個無效參數(shù)處理程序,當程序中的某個函數(shù)調(diào)用時傳入了無效參數(shù),該處理程序會被調(diào)用。
  • _set_abort_behavior(_CALL_REPORTFAULT, _CALL_REPORTFAULT): 設置 abort() 函數(shù)的行為,在調(diào)用 abort() 時將觸發(fā)。
  1. C 運行時信號處理:由 CRT 提供的信號處理機制。
  • signal(SIGABRT, SigabrtHandler): 設置應用程序終止(abort)信號的處理程序。
  • signal(SIGINT, SigintHandler): 設置鍵盤中斷(interrupt)信號的處理程序。
  • signal(SIGTERM, SigtermHandler): 設置終止(terminate)信號的處理程序。
  • signal(SIGILL, SigillHandler): 設置非法指令(illegal instruction)信號的處理程序。
  1. C++ 運行時異常處理:由 C++ 語言標準提供的異常處理機制。
  • set_terminate(TerminateHandler): 設置未捕獲的 C++ 異常導致程序終止時調(diào)用的函數(shù)。
  • set_unexpected(UnexpectedHandler): 設置異常規(guī)格不匹配時調(diào)用的函數(shù)(在 C++11 之前的 C++ 標準中使用)。

這段程序的主要目的是捕獲各種類型的異常,包括 Windows 結構化異常(SEH)、C 運行時庫異常、C 運行時信號以及 C++ 運行時異常。這些異常處理機制分別由操作系統(tǒng)、C 運行時庫和 C++ 語言標準提供。通過使用這些技術,程序能夠更全面地捕獲和處理異常。

可以看到,這就是Windows平臺下C++異常捕獲處理的棘手之處,有好幾個技術層面的異常機制需要處理,才能做到盡可能捕獲更多的異常。

進一步解析

windows平臺下的C++運行時異常,大部分情況是會被SEH和C++ 運行時異常處理機制捕獲。

在 Windows 平臺下,C++ 運行時異常(如 std::bad_alloc、std::out_of_range 等)通常會被 C++ 運行時異常處理機制捕獲,如 try/catch 塊,如果C++運行時異常沒有被catch塊處理,則會走到set_terminate設置的回調(diào)函數(shù)中。SEH 主要用于捕獲硬件異常、操作系統(tǒng)產(chǎn)生的異常(如訪問違規(guī)、整數(shù)除以零等)以及其他一些異常情況。

C 運行時庫 (CRT) 異常處理和 C 運行時信號處理通常用于處理和 C 語言相關的問題。C++ 程序可能會使用 C 語言功能或調(diào)用 C 語言庫,因此在某些情況下,這些處理機制也可能捕獲到異常。然而,對于大部分使用 C++ 標準庫和特性的程序來說,這些情況相對較少。所以,在 Windows 平臺下的 C++ 程序中,C++ 運行時異常處理和 SEH 通??梢圆东@大部分異常,而 C 運行時庫 (CRT) 異常處理和 C 運行時信號處理捕獲的異常情況相對較少。盡管如此,我們還是應該要處理CRT異常。

SEH具體能捕獲哪一些運行時異常?

SEH(Structured Exception Handling)是 Windows 平臺上的一種異常處理機制,它主要用于捕獲由操作系統(tǒng)引發(fā)的異常。以下是一些 SEH 可以捕獲的運行時異常:

  • 訪問違規(guī)(Access Violation):當程序嘗試訪問非法內(nèi)存地址時,如空指針解引用、越界訪問或使用已釋放的內(nèi)存。

  • 無效操作(Invalid Operation):當程序嘗試執(zhí)行非法指令時,如無效的機器代碼或執(zhí)行不支持的指令集。

  • 數(shù)據(jù)類型不匹配(Datatype Misalignment):當程序嘗試訪問未對齊(Alignment)的數(shù)據(jù)時,這在某些處理器體系結構(如 ARM 和 Itanium)上可能導致異常。

對齊(Alignment)是指數(shù)據(jù)在內(nèi)存中的起始地址應滿足某種特定的邊界要求。這些要求通常取決于底層硬件和處理器體系結構。對齊可以幫助優(yōu)化處理器訪問內(nèi)存的性能,因為處理器通常更高效地訪問對齊的數(shù)據(jù)。例如,假設 int 類型的數(shù)據(jù)需要以 4 字節(jié)邊界對齊。這意味著 int 類型數(shù)據(jù)的起始地址應該是 4 的倍數(shù)(如 0x1000、0x1004、0x1008 等)。如果 int 類型數(shù)據(jù)位于非 4 字節(jié)邊界的地址(如 0x1001、0x1005 等),則該數(shù)據(jù)被認為是未對齊的。在某些處理器體系結構(如 ARM、Itanium)上,訪問未對齊的數(shù)據(jù)可能導致數(shù)據(jù)類型不匹配異常。在其他體系結構(如 x86、x64)上,處理器通??梢栽L問未對齊的數(shù)據(jù),但這可能導致性能下降。在 C 和 C++ 中,編譯器通常會自動處理數(shù)據(jù)對齊,確保數(shù)據(jù)位于正確的邊界上。但在某些情況下,程序員可能需要手動處理對齊問題,例如在指針類型轉換、使用自定義內(nèi)存分配器或處理硬件相關數(shù)據(jù)結構時。

  • 整數(shù)除以零:當程序嘗試執(zhí)行整數(shù)除法時,除數(shù)為零。

  • 堆棧溢出(Stack Overflow):當程序的堆棧使用超過了分配的空間時,如深度遞歸或分配大量的局部變量。

  • 其他硬件異常:如浮點數(shù)操作的異常,比如除以零、無窮大相減、非數(shù)字(NaN)之間的比較等。

需要強調(diào)的是,SEH 主要處理由操作系統(tǒng)引發(fā)的異常,而非 C++ 異常。C++ 異常是由 C++ 運行時系統(tǒng)引發(fā)的,需要使用 C++ 的 try/catch/throw 語句和set_terminate來捕獲和處理。我們在開發(fā)時,最經(jīng)常遇到的崩潰類型是訪問違規(guī)。這里有必要提一提可能導致訪問違規(guī)的常見場景。

  1. 空指針解引用:當程序嘗試通過空指針訪問內(nèi)存時,將觸發(fā)訪問違規(guī)異常。例如:
int* ptr = nullptr;
int a = *ptr; // 訪問違規(guī),因為 ptr 是空指針
  1. 越界訪問:當程序嘗試訪問數(shù)組或容器的邊界之外的內(nèi)存時,將觸發(fā)訪問違規(guī)異常。例如:
int arr[10];
int a = arr[20]; // 訪問違規(guī),因為數(shù)組索引越界
  1. 釋放后使用:當程序嘗試訪問已經(jīng)釋放的內(nèi)存時,將觸發(fā)訪問違規(guī)異常。例如:
int* ptr = new int;
delete ptr;
int a = *ptr; // 訪問違規(guī),因為內(nèi)存已被釋放
  1. 未初始化指針解引用:當程序嘗試訪問未初始化的指針時,將觸發(fā)訪問違規(guī)異常。例如:
int* ptr;
int a = *ptr; // 訪問違規(guī),因為 ptr 未初始化
  1. 無效類型轉換:當程序嘗試執(zhí)行無效的指針類型轉換時,可能導致訪問違規(guī)。例如:
int a = 42;
char* ptr = reinterpret_cast<char*>(&a);
int* invalid_ptr = reinterpret_cast<int*>(ptr + 1);
int b = *invalid_ptr; // 訪問違規(guī),因為 invalid_ptr 指向非法內(nèi)存地址

這些場景僅僅是訪問違規(guī)可能發(fā)生的一部分情況,在實際編程過程中,可能還會有其他導致訪問違規(guī)的情形,而且更加隱蔽。比如,我們使用懸掛的類指針時,可能不會馬上在使用懸掛的類指針的位置崩潰,而是在調(diào)用成員函數(shù)的某一處崩潰,這和操作系統(tǒng)的內(nèi)存回收機制有關系(Windows操作系統(tǒng)可能不會馬上將delete掉的堆區(qū)內(nèi)存馬上回收,并在頁表上聲明為不可訪問,這和操作系統(tǒng)的性能優(yōu)化機制有關系)。為了避免訪問違規(guī),C++程序員應該確保指針操作的正確性、內(nèi)存分配和釋放的正確使用以及遵循類型轉換的規(guī)范。

為什么使用以上機制仍不能捕獲所有異常?

有一些異常是發(fā)生在操作系統(tǒng)內(nèi)核層面的,以及硬件層面的。雖然上述程序也能夠監(jiān)控到部分這類異常,但由于異常機制設計上的原因,并非都能捕獲。

例如,堆棧溢出異??赡軐е鲁绦蛄⒓幢罎ⅲ鵁o法執(zhí)行任何異常處理程序(SEH(結構化異常處理)理論上可以捕獲堆棧溢出異常,但在某些情況下可能無法捕獲所有堆棧溢出異常。堆棧溢出是一種特殊的異常,因為當堆棧溢出時,程序的堆??臻g已經(jīng)耗盡。這可能導致在嘗試處理異常時遇到問題,因為異常處理程序本身可能需要使用堆??臻g。這就是為什么在某些情況下,SEH可能無法捕獲堆棧溢出異常。)。

如果在異常處理程序本身中引發(fā)了另一個異常,也可能導致程序崩潰。這是因為異常處理程序的主要目的是處理異常并恢復程序的執(zhí)行。如果異常處理程序本身引發(fā)了異常,那么它無法完成其預期的任務。為了避免這種情況,應確保異常處理程序盡可能簡單并且穩(wěn)定。在異常處理程序中避免引入可能導致新異常的代碼,例如分配大量內(nèi)存、執(zhí)行復雜的算法等。在異常處理程序中進行最小化的操作,并在處理異常時盡量謹慎。

有一些異常,雖然使用上述方案仍捕獲不到,但使用WinDbg可以捕獲到(當我們使用WinDbg啟動應用程序并監(jiān)控運行,期間發(fā)生崩潰的場景)。WinDbg 的工作原理是,它在操作系統(tǒng)級別附加到目標進程,監(jiān)視進程的執(zhí)行并捕獲異常。當異常發(fā)生時,WinDbg 可以暫停目標進程,分析進程的狀態(tài),并讓開發(fā)者進行調(diào)試操作。作為一個內(nèi)核級調(diào)試器,WinDbg 可以直接與操作系統(tǒng)內(nèi)核交互,訪問和控制底層系統(tǒng)資源。這使得 WinDbg 能夠在更低級別的層次上監(jiān)視應用程序的執(zhí)行,從而捕獲那些無法通過應用程序內(nèi)部異常處理程序捕獲的異常。

但是程序發(fā)生異常的情況很復雜,使用WinDbg也不一定能捕獲所有異常。對于Windows C++應用程序開發(fā)者而言,如果用戶機器上發(fā)現(xiàn)了無法被捕獲的異常,嘗試在用戶環(huán)境下使用WinDbg啟動程序,或許是值得嘗試的方案(但是這也看用戶的心情以及工程師的溝通能力了,被拒絕也是常事)。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • 異常的拋出 在C++中,通過throw一個表達式來引發(fā)異常,被拋出的表達式的類型以及當前的調(diào)用鏈共同決定了哪段處理...
    土豆吞噬者閱讀 1,319評論 0 2
  • 使用GTEST編寫C++測試用例進階教程 [TOC] 更多的斷言 這章覆蓋了一些使用頻率較少但是仍然很重要的斷言 ...
    愿以光散黑閱讀 15,898評論 0 3
  • 1、C語言異常處理 1.1、異常的概念 異常:程序在運行過程中可能產(chǎn)生異常(是程序運行時可預料的執(zhí)行分支),如:運...
    金色888閱讀 657評論 0 0
  • C++ Builder 參考手冊[http://www.itdecent.cn/p/d059131d1c4c] ...
    玄坴閱讀 1,570評論 0 4
  • C語言異常處理 異常的概念 異常的說明程序在運行過程中可能產(chǎn)生異常異常(Exception)與Bug的區(qū)別異常是程...
    nethanhan閱讀 276評論 0 0

友情鏈接更多精彩內(nèi)容