<menu id="w8yyk"><menu id="w8yyk"></menu></menu>
  • <dd id="w8yyk"><nav id="w8yyk"></nav></dd>
    <menu id="w8yyk"></menu>
    <menu id="w8yyk"><code id="w8yyk"></code></menu>
    <menu id="w8yyk"></menu>
    <xmp id="w8yyk">
    <xmp id="w8yyk"><nav id="w8yyk"></nav>
  • 網站首頁 > 物聯資訊 > 技術分享

    C++異常中的堆棧跟蹤

    2016-09-28 00:00:00 廣州睿豐德信息科技有限公司 閱讀
    睿豐德科技 專注RFID識別技術和條碼識別技術與管理軟件的集成項目。質量追溯系統、MES系統、金蝶與條碼系統對接、用友與條碼系統對接

     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:  ?vDivideByZero@@YAXXZ
        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:  ?vTestVectorThrow@@YAXXZ
        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下
    RFID管理系統集成商 RFID中間件 條碼系統中間層 物聯網軟件集成
    最近免费观看高清韩国日本大全