探索 Windows 平臺(tái)下的 C++ 異常捕獲策略:如何讓W(xué)indows C++應(yīng)用程序盡可能捕獲所有異常?

前言

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

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

程序示例

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

void InstallUnexceptedExceptionHandler()
{
    //SEH(Windows 結(jié)構(gòu)化異常處理),屬于Win32 API
    ::SetUnhandledExceptionFilter(UnhandledStructuredException);
    //C 運(yùn)行時(shí)庫 (CRT) 異常處理,由 CRT 提供的異常處理機(jī)制。
    _set_purecall_handler(PureCallHandler);
    _set_new_handler(NewHandler);
    _set_invalid_parameter_handler(InvalidParameterHandler); 
    _set_abort_behavior(_CALL_REPORTFAULT, _CALL_REPORTFAULT);
    //C 運(yùn)行時(shí)信號(hào)處理,由 CRT 提供的信號(hào)處理機(jī)制。
    signal(SIGABRT, SigabrtHandler);
    signal(SIGINT, SigintHandler);
    signal(SIGTERM, SigtermHandler);
    signal(SIGILL, SigillHandler);
    //C++ 運(yùn)行時(shí)異常處理,API由標(biāo)準(zhǔn)庫提供
    set_terminate(TerminateHandler);
    set_unexpected(UnexpectedHandler);
}

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

原理簡介

這段程序使用了多種技術(shù)來捕獲異常??梢詫?duì)它們進(jìn)行分類,并解釋它們是由哪個(gè)技術(shù)層面提供的:

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

這段程序的主要目的是捕獲各種類型的異常,包括 Windows 結(jié)構(gòu)化異常(SEH)、C 運(yùn)行時(shí)庫異常、C 運(yùn)行時(shí)信號(hào)以及 C++ 運(yùn)行時(shí)異常。這些異常處理機(jī)制分別由操作系統(tǒng)、C 運(yùn)行時(shí)庫和 C++ 語言標(biāo)準(zhǔn)提供。通過使用這些技術(shù),程序能夠更全面地捕獲和處理異常。

可以看到,這就是Windows平臺(tái)下C++異常捕獲處理的棘手之處,有好幾個(gè)技術(shù)層面的異常機(jī)制需要處理,才能做到盡可能捕獲更多的異常。

進(jìn)一步解析

windows平臺(tái)下的C++運(yùn)行時(shí)異常,大部分情況是會(huì)被SEH和C++ 運(yùn)行時(shí)異常處理機(jī)制捕獲。

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

C 運(yùn)行時(shí)庫 (CRT) 異常處理和 C 運(yùn)行時(shí)信號(hào)處理通常用于處理和 C 語言相關(guān)的問題。C++ 程序可能會(huì)使用 C 語言功能或調(diào)用 C 語言庫,因此在某些情況下,這些處理機(jī)制也可能捕獲到異常。然而,對(duì)于大部分使用 C++ 標(biāo)準(zhǔn)庫和特性的程序來說,這些情況相對(duì)較少。所以,在 Windows 平臺(tái)下的 C++ 程序中,C++ 運(yùn)行時(shí)異常處理和 SEH 通常可以捕獲大部分異常,而 C 運(yùn)行時(shí)庫 (CRT) 異常處理和 C 運(yùn)行時(shí)信號(hào)處理捕獲的異常情況相對(duì)較少。盡管如此,我們還是應(yīng)該要處理CRT異常。

SEH具體能捕獲哪一些運(yùn)行時(shí)異常?

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

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

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

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

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

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

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

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

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

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

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

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

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

例如,堆棧溢出異??赡軐?dǎo)致程序立即崩潰,而無法執(zhí)行任何異常處理程序(SEH(結(jié)構(gòu)化異常處理)理論上可以捕獲堆棧溢出異常,但在某些情況下可能無法捕獲所有堆棧溢出異常。堆棧溢出是一種特殊的異常,因?yàn)楫?dāng)堆棧溢出時(shí),程序的堆??臻g已經(jīng)耗盡。這可能導(dǎo)致在嘗試處理異常時(shí)遇到問題,因?yàn)楫惓L幚沓绦虮旧砜赡苄枰褂枚褩?臻g。這就是為什么在某些情況下,SEH可能無法捕獲堆棧溢出異常。)。

如果在異常處理程序本身中引發(fā)了另一個(gè)異常,也可能導(dǎo)致程序崩潰。這是因?yàn)楫惓L幚沓绦虻闹饕康氖翘幚懋惓2⒒謴?fù)程序的執(zhí)行。如果異常處理程序本身引發(fā)了異常,那么它無法完成其預(yù)期的任務(wù)。為了避免這種情況,應(yīng)確保異常處理程序盡可能簡單并且穩(wěn)定。在異常處理程序中避免引入可能導(dǎo)致新異常的代碼,例如分配大量內(nèi)存、執(zhí)行復(fù)雜的算法等。在異常處理程序中進(jìn)行最小化的操作,并在處理異常時(shí)盡量謹(jǐn)慎。

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

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

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

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

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

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