1. 程式人生 > >【轉】C++異常中的堆疊跟蹤

【轉】C++異常中的堆疊跟蹤

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; // 防止無意中按鍵使程式退出。
}
對於第1個異常輸出為:
0 .V 004066d5 0040779f 0012ff70 00000000 _main + 85 bytes
Sig: _main
Decl: _main
Line: H:\C++ Test\StackWalk\Test\main.cpp(50) + 3 bytes
Mod: Test[H:\C++ Test\StackWalk\Test\Debug\Test.exe], base: 0x00400000h
Sym: type: PDB, file: H:\C++ Test\StackWalk\Test\Debug\Test.exe
從上面第4行可以知道在main.cpp檔案的第50行丟擲的異常,找到這一行就是*pInt = 5;然後檢查上下文,哦,沒有分配記憶體,於是“臭名昭著”的非法記憶體訪問就輕易發現了!Is it powerful?
對於第2個異常輸出為:
1 .V 0040683d 0040779f 0012ff70 00000000 _main + 445 bytes
Sig: _main
Decl: _main
Line: H:\C++ Test\StackWalk\Test\main.cpp(72) + 49 bytes
Mod: Test[H:\C++ Test\StackWalk\Test\Debug\Test.exe], base: 0x00400000h
Sym: type: PDB, file: H:\C++ Test\StackWalk\Test\Debug\Test.exe
從上面第4行可以知道在main.cpp檔案的第72行丟擲的異常,找到這一行就是throw CMyException( " my exception" ); 哦,是自定義異常。
對於第3個異常輸出為:
0 .V 004065f5 0040697c 0012fe98 00000000 void __cdecl vDivideByZero(void) + 37 b
ytes
Sig:
Decl: void __cdecl vDivideByZero(void)
Line: H:\C++ Test\StackWalk\Test\main.cpp(26) + 6 bytes
Mod: Test[H:\C++ Test\StackWalk\Test\Debug\Test.exe], base: 0x00400000h
Sym: type: PDB, file: H:\C++ Test\StackWalk\Test\Debug\Test.exe

1 .V 0040697c 0040779f 0012ff70 00000000 _main + 764 bytes
Sig: _main
Decl: _main
Line: H:\C++ Test\StackWalk\Test\main.cpp(100) + 0 bytes
Mod: Test[H:\C++ Test\StackWalk\Test\Debug\Test.exe], base: 0x00400000h
Sym: type: PDB, file: H:\C++ Test\StackWalk\Test\Debug\Test.exe
從上面第5行可以知道在main.cpp檔案的第26行丟擲的異常,找到這一行就是int iRet = 5 / iZero; 哦,是除零異常。然後從上面第12行可以知道呼叫這個函式是在main.cpp檔案的第100行,就是vDivideByZero();的下一行(注意因為vDivideByZero();函式已經呼叫了,所以顯示的行數都是它的下一行)。這樣,我們就可以知道一個異常發生的完整過程。
對於第4個異常輸出為:
0 .V 004070ca 00406ab9 0012fe98 00000000 void __cdecl vTestVectorThrow(void) + 7
4 bytes
Sig:
Decl: void __cdecl vTestVectorThrow(void)
Line: h:\c++ test\stackwalk\test\test1.cpp(13) + 10 bytes
Mod: Test[H:\C++ Test\StackWalk\Test\Debug\Test.exe], base: 0x00400000h
Sym: type: PDB, file: H:\C++ Test\StackWalk\Test\Debug\Test.exe

1 .V 00406ab9 0040779f 0012ff70 00000000 _main + 1081 bytes
Sig: _main
Decl: _main
Line: H:\C++ Test\StackWalk\Test\main.cpp(118) + 0 bytes
Mod: Test[H:\C++ Test\StackWalk\Test\Debug\Test.exe], base: 0x00400000h
Sym: type: PDB, file: H:\C++ Test\StackWalk\Test\Debug\Test.exe
從上面第5行可以知道在test1.cpp檔案的第13行丟擲的異常,找到這一行就是 vectInt[ 3 ] = 100; 檢查上下文,發現沒有給vectInt分配空間。然後從上面第12行可以知道呼叫這個函式是在main.cpp檔案的第118行,就是vTestVectorThrow();的下一行。
那麼如何使用這個類庫呢?對於新工程,首先把exception.h和exception.cpp加入工程,你需要把自定義的異常類繼承自CMyException,這樣自定義的異常類就具有堆疊跟蹤功能,其次在每個執行緒的入口函式加上以下幾個函式呼叫(注意:必須在每個執行緒的入口都要呼叫,當然如CWinUtil::vInitStackEnviroment();不需要,只需要在main入口即可,但如果在每個執行緒的入口都要呼叫也不會有副作用):
` // 在每個執行緒函式的入口加上以下語句。
// 檢查記憶體洩露。
CWinUtil::vCheckMemoryLeak();
// 使new函式失敗時丟擲異常。
CWinUtil::vSetThrowNewException();?
// 把WINDOWS中的結構化異常轉化為C++異常。
CWinUtil::vMapSEHtoCE();
// 初始化。
CWinUtil::vInitStackEnviroment();
然後如下所示捕獲異常:
try {
vTest();// 假設要捕獲vTest()函式可能丟擲的異常。
}
catch ( const CRecoverableSEHException &bug ) {// 用於捕獲可恢復的結構化異常。
cout << bug.what() << endl;
}
catch ( const CUnRecoverableSEHException &bug ) {// 用於捕獲不可恢復的結構化異常。
cout << bug.what() << endl;
}
catch ( const exception& e ) {// 用於捕獲標準C++異常及其子類。
cout << e.what() << endl;
}
catch ( ... ) { // 用於捕獲那些丟擲非結構化異常和不是繼承exception的異常。
cout << " else exception." << endl;
}
當然你對於結構化異常沒有其它特別的處理策略,也可以簡化為:
try {
vTest();// 假設要捕獲vTest()函式可能丟擲的異常。
}
// 用於捕獲標準C++異常及其子類。因為結構化異常繼承自exception,所以這裡也能捕獲// 結構化異常。
catch ( const exception& bug ) {
cout << bug.what() << endl;
}

對於已有的工程,首先把exception.h和exception.cpp加入工程,把原來的自定義的異常類繼承自CMyException,然後同上的方法捕獲異常,每個執行緒入口增加初始化函式即可,可以與你原來的異常處理完美整合。
對於在MFC中的用法,可以按如下方式捕獲異常:
try {
vTest();
}
catch ( const exception& e ) {
cout << e.what() << endl;
}?
// CException是MFC中異常基類,MFC中的異常通常從堆中分配,所以應通過指標捕獲,// 而且使用完之後還應該呼叫delete函式清除記憶體。
catch ( CException* e ) {
// hadle exception
e->delete();?
}
即在MFC異常上加一層捕獲標準C++異常和結構化異常以及自定義異常。另外由於MFC中已經自動有了處理記憶體洩露的機制,所以需要刪除exception.h檔案的第34行到第40行(有關記憶體洩露的說明見下面),由於在MFC中每個.cpp檔案開始處都要包含stdafx.h,所以還需要在exception.cpp檔案開始處加上#include “stdafx.h”,不然會編譯不通過。
如果你希望把堆疊資訊輸出在檔案中,以防丟失,可以使用IO重定向功能(有關IO重定向可參考Jim Hyslop and Herb Sutter的文http://www.cuj.com/experts/1903/hyslop.htm ),即在main()函式開頭加入以下語句:
ofstream ofLog( "exception.txt", ios_base::app );
streambuf *outbuf = cout.rdbuf( ofLog.rdbuf() );
這樣,所有輸出到console的資訊就重定向到exception.txt檔案中了。如果你想恢復,則可以加入以下語句:
// restore the buffers
cout.rdbuf( outbuf );
對於release版本,如果你執行,你會發現程式不能捕獲非法記憶體訪問、除零等結構化異常,這是因為VC在release版預設是同步異常,不捕獲結構化異常,只能捕獲C++的異常,所以你需要修改編譯選項,採用非同步異常模型,在project->setting->c/c++->project options框中增加/EHa的編譯選項。另外,release版預設不生成除錯符號檔案,這樣你就不能不能打印出丟擲異常的程式碼的行號等資訊,所以你需要修改編譯配置,方法如下:Project->Settings->c/c++頁中的debug info列表選項中選擇program database項。這樣release版本也能實現堆疊跟蹤。當然,這樣會使release版本減慢速度,而且還要帶一個debug info檔案,因為有些bug只有在release版本中才會出現,而且release版是真正給客戶使用的,所以必須測試release版,可以考慮release的beta1和beta2版本帶這些除錯資訊,這樣的話,因為debug版和release版都測試通過,發行給客戶的最終正式版可以通過設定一個巨集註釋掉這些除錯資訊,恢復成同步異常模型,即恢復成VC預設的release版配置。
4. 其他需要注意的問題
本類庫還有檢查記憶體洩露的功能,只要你在每個.cpp檔案的所有#include之後,加上以下語句:
// 以下幾行是能夠定義到發生記憶體洩露的程式碼行。在每個.cpp檔案都應該宣告。
#include “Exception.h”
#ifdef _DEBUG?
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
然後以debug方式啟動程式,當程式正常退出或關閉時(注意不能用stop debug的命令停止,否則將不會打印出記憶體洩露的資訊),在VC的debug視窗將會打印出有可能產生記憶體洩露的原始碼資訊,包括檔名和行號。由於MFC程式自動會生成這些程式碼,所以在MFC程式中不需要手工新增這些程式碼。例如,當你以debug方式執行下載的測試程式,當程式正常退出後,在debug視窗會顯示如下語句:
Detected memory leaks!
Dumping objects ->
H:\C++ Test\StackWalk\Test\main.cpp(88) : {184} normal block at 0x00632D50, 100 bytes long.
Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD?
Object dump complete.
從上面第3行可以知道在main.cpp檔案的第88行產生了記憶體洩露,找到這一行就是 char* pcLeak = new char[ 100 ]; 檢查上下文,發現果然沒有釋放記憶體。
當然如同你使用Purify、BoundsChecker等工具檢查記憶體洩露一樣,它也會謊報軍情,有些不會記憶體洩露的地方,它也告訴你記憶體洩露了,尤其當你使用了大量STL類庫時,這就需要你細心檢查上下文,以確定是否是記憶體洩露了。
本類庫由於使用了一些VC中特有除錯符號特性,所以可能不能在其它編譯器下通過。另外,本文討論的堆疊跟蹤實現都是基於Windows 2000以上,Win98和Win95將不能輸出導致丟擲異常的語句所在的檔名和行號。本類庫也不能在Unix或Linux下執行。有在Unix或Linux平臺工作的讀者朋友如果有興趣,可以實現一個在Unix或Linux平臺執行的C++異常堆疊跟蹤類庫。
本類庫不能跟蹤標準C++異常及其它你不能控制的異常的堆疊資訊,即當這些異常丟擲時,不能輸出丟擲異常語句的檔名和行號資訊。這是因為標準C++異常是語言內建的,而其它類庫的異常你不能控制其建構函式。這是一個小小的遺憾,不過你可以通過異常說明知道它是哪一種異常,可能在程式的哪一些C++函式中丟擲,這樣你也能很快地找到錯誤之處。
有了完善的異常類層次,你可以在程式中幹任何事,異常過濾機制和堆疊跟蹤會忠實的記錄任何錯誤,除了你在解構函式丟擲異常以及重複刪除不為NULL的指標,這兩種情況下程式還是會當掉,而且不能記錄堆疊資訊。當然,對於緩衝區溢位、堆寫越界等情況,本類庫還是無能為力,不過,你可以藉助Purify、BoundsChecker等工具來檢查程式執行中是否有這類問題。
5. 小結
誰說C++就不能有堆疊跟蹤的功能。有了這個具有堆疊跟蹤功能的異常類庫,你將如虎添翼,必將加快你開發程式的程序。
Enjoy!

http://hi.baidu.com/barrypp/item/6d63573c580995637d034bc2