C++異常中的堆棧跟蹤


 C++語言的運行時環境是基於棧的環境,堆棧跟蹤(trace stack)就是程序運行時能夠跟蹤並打印所調用的函數、變量及返回地址等,C++異常中的堆棧跟蹤就是當程序拋出異常時,能夠把導致拋出異常的語句所在的文件名和行號打印出來,以及把調用拋出異常的語句的函數以及其它上層函數信息都打印出來。
1. 為什么需要堆棧跟蹤
當你在開發程序時,你是否曾遇到過程序運行過程中突然當機,而你不知道哪一行代碼出的問題;你是否曾遇到過程序調試過程中突然拋出異常,而你不知道哪一行代碼出的問題;你是否曾遇到過當你在單步調試時突然拋出異常而你卻忘了單步執行到哪一步時拋出的異常,於是你只好重來一次。Beta程序在客戶那里試運行當中,突然當機,而你不能調試,只能依據客戶報告的一些信息來找bug,而客戶大多不熟悉程序開發,所以他們報告的信息太少使你感覺無從下手、一籌莫展。
如果你碰到過以上情況,你就只好痛苦地一條一條單步執行語句,看拋出異常的語句在哪,檢查非法訪問內存的語句在哪里,糟糕的是根據海森堡不確定原理,有時當你調試時又不出問題了。所以幸運的話,你能很快就找到bug,不幸的話,幾小時或幾天都不能找出問題所在,並將成為你的夢魘。我在程序開發過程中,就經常碰到以上這些情況。
眾所周知,在程序開發中發現一個bug將比改正這個bug難度大很多。所以如果有一個方法能夠在程序出錯時把出錯信息打印出來,這樣將大大方便找到bug,加快程序開發速度,提高程序的質量。這樣,當客戶報告程序出錯時,你只需要客戶把日志發送給你,你根據這個日志里的異常堆棧信息就能輕松發現問題所在。
在java中就有堆棧跟蹤功能,它能在程序拋出異常時,能夠打印出能夠把導致拋出異常的語句所在的文件名和行號,C#中也有這個功能。很多人認為用java開發程序比用C++開發程序要快,我認為java有拋出異常時能夠跟蹤堆棧這個功能是其中的一個重要原因。

2. 如何實現C++異常中的堆棧跟蹤
要實現堆棧跟蹤,必須依賴於底層機制即操作系統或虛擬平台,java與jvm虛擬平台綁定,C#與.NET虛擬平台綁定,它們都提供了堆棧跟蹤的功能,而C++與操作系統或平台無關,所以沒有提供這個功能,但是否能夠利用操作系統的系統函數實現這個功能呢?下面簡要介紹如何在Windows2000下實現C++異常中的堆棧跟蹤。
在Windows中,C++異常底層的實現是通過Windows中的結構化異常SEH來實現的,結構化異常包括如除0溢出、非法內存訪問、堆棧溢出等,雖然用catch( … )能夠捕獲結構化異常,但不能知道是哪種結構化異常,所以第一步就是要把結構化異常轉化為C++異常,Windows中的_set_se_translator()函數可以實現這個功能。先建立一個轉化函數:void _cdecl TranslateSEHtoCE( UINT code, PEXCEPTION_POINTERS pep ) ;在這個轉化函數中拋出一個繼承C++標准異常的類,如CRecoverableSEHException(可以恢復的結構化異常類)和CUnRecoverableSEHException(不可以恢復的結構化異常類),這兩個類繼承CSEHException,CSEHException繼承標准C++異常的基類exception。然后在main函數開始處調用 _set_se_translator(TranslateSEHtoCE ),這樣就可以把結構化異常轉換為C++異常。
另外,由於VC中默認new失敗時並不拋出異常,所以需要讓new失敗時拋出異常,這樣可以統一處理,可以使用WINDOWS中的_set_new_handler( )轉化,讓new失敗時拋出異常。同上,先建立一個轉化函數 int NewHandler( size_t size ),在這個轉化函數中拋出C++標准異常的類bad_alloc,在main函數開始處調用 _set_new_handler (NewHandler)。
接着在CSEHException的構造函數中跟蹤堆棧,把導致拋出結構化異常的語句所在的文件名和行號打印出來,調用void ShowStack( HANDLE hThread, CONTEXT& c )。ShowStack函數封裝了跟蹤堆棧所需調用的各種系統API。它的功能就是根據參數c(線程的上下文),得到當前程序的路徑,枚舉所調用的系統動態連接庫,然后按照從里到外的順序打印出所有執行的函數名及其所在的文件名和行號。
創建自己的異常類使其具有堆棧跟蹤的功能,定義自己使用的異常基類如CMyException(當然,如果你願意,你可以修改其命名),令其繼承標准C++異常類domain_error(當然也可以繼承exception),然后在CMyException的構造函數中調用void ShowStack( HANDLE hThread, CONTEXT& c ),這樣就可以實現堆棧跟蹤,其它自定義的異常繼承CMyException,就自動獲得堆棧跟蹤的功能。這樣就形成了一個完整的類層次。

 exception

logic_error runtime_error   
length_error
    out_of_range          bad_alloc          bad_cast            range_error
     invalid_argument        bad_exception                overflow_error
       domain_error                 ios_base:failure     underflow_error
    
CMyException(自定義異常基類)      CSEHException(結構化異常基類)      
         
                          CRecoverableSEHException   CUnRecoverableSEHException

CSocketException(與socket相關的異常)   
CConfigException(與配置文件相關的異常)
注:CMyException上面的異常類均為標准C++的異常類。
注:以上異常類的基類均為exception。


本人實現的具有堆棧跟蹤的C++異常類庫和測試程序可以從www.smiling.com.cn中的umlchina小組中下載StackTraceInC.zip文件。
3. 如何使用C++異常中的堆棧跟蹤類庫
下載的文件包括Exception.h Exception.cpp(具有堆棧跟蹤功能的異常類庫), main.cpp, Test1.h, Test1.cpp (測試代碼)。
讓我們先感受一下堆棧跟蹤的威力,運行下載的示例程序,將打印出如下結果(因為輸出太長,所以只節選了其中一部分)。主程序為:
void main(){

// 在每個線程函數的入口加上以下語句。
// 檢查內存泄露。
CWinUtil::vCheckMemoryLeak();
// 使new函數失敗時拋出異常。
CWinUtil::vSetThrowNewException();
// 把WINDOWS中的結構化異常轉化為C++異常。
CWinUtil::vMapSEHtoCE();
// 初始化。
CWinUtil::vInitStackEnviroment();
try {
// 捕獲非法訪問內存的結構化異常。
int* pInt;  // 故意不分配內存
*pInt = 5;  // 應該顯示出這一行出錯。
}
// 捕獲可恢復的結構化異常。
catch ( const CRecoverableSEHException &bug ) {
cout << bug.what() << endl;
}
// 捕獲不可恢復的結構化異常。
catch ( const CUnRecoverableSEHException &bug ) {
cout << bug.what() << endl;
}
// 捕獲標准C++異常。
catch ( const exception& e ) {
cout << e.what() << endl;
}
// 捕獲其它不是繼承exception的異常。
catch ( ... ) {
cout << " else exception." << endl;
}
try {
// 捕獲自定義的異常。
throw CMyException( " my exception" ); // 應該顯示出這一行出錯。
}
catch ( const CRecoverableSEHException &bug ) {
cout << bug.what() << endl;
}
catch ( const CUnRecoverableSEHException &bug ) {
cout << bug.what() << endl;
}
catch ( const exception& e ) {
cout << e.what() << endl;
}
catch ( ... ) {
cout << " else exception." << endl;
}
try {
// 捕獲函數中的異常。
vDivideByZero(); // 應該顯示出這個函數拋出的異常。
int i = 1;
}
catch ( const CRecoverableSEHException &bug ) {
cout << bug.what() << endl;
}
catch ( const CUnRecoverableSEHException &bug ) {
cout << bug.what() << endl;
}
catch ( const exception& e ) {
cout << e.what() << endl;
}
catch ( ... ) {
cout << " else exception." << endl;
}
try {
// 捕獲另一源文件Test1.cpp中的函數拋出的異常。
vTestVectorThrow();// 應該顯示出在這個函數拋出的異常。
int i = 1;
}
catch ( const CRecoverableSEHException &bug ) {
cout << bug.what() << endl;
}
catch ( const CUnRecoverableSEHException &bug ) {
cout << bug.what() << endl;
}
catch ( const exception& e ) {
cout << e.what() << endl;
}
catch ( ... ) {
cout << "else exception." << endl;
}
int i;
cin >> i; // 防止無意中按鍵使程序退出。
}


免責聲明!

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



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