如何定位導致Crash的代碼位置

1. 在開發環境下定位Crash錯誤
  1.1 普通的crash
  1.2 較難定位的crash
  1.3 注意vc的輸出日誌
2. 定位發佈在外的版本的Crash錯誤
3. 小技巧
  3.1 根據程序地址找到代碼位置
  3.2 根據消息值查看對應的windows消息
  3.3 查看GetLastError返回值
  3.4 在代碼中暫停程序
4. 編程小警示
  4.1 慎用IsBadPtr系列函數
  4.2 慎用catch(...)
5. 附錄
  5.1 爲什麼程序crash時調用堆棧是亂的
  5.2 使用Debugging tools for windows查看.dmp文件(錯誤報告)

------------------------------------------------------------------------------------------------------------------------
1. 在開發環境下定位Crash錯誤
  1.1 普通的crash
    先來看看最普通的crash
    參見圖1(c01.png)
    當你在debug模式下運行上面的程序就會彈出上面的框。vc就幫你定位到了錯誤的位置。是個對零指針的操作。非常簡單,不是嗎。

  1.2 較難定位的crash
    較難定位的crash往往是由於內存錯誤(參見5.1 爲什麼程序crash時調用堆棧是亂的)。例如以下代碼:

代碼


    char *p = new char[16];
    p[10] = 0xfd;
    delete[] p;
    printf(p);


    以上代碼有兩處錯誤,一是第2行的內存寫越界,二是第4行使用被刪除的指針。
    但以上代碼在vc的release和debug下都不會報錯。這使得這類錯誤很難定位。
    檢測這一類問題可以使用BoundsChecker工具的FinalCheck模式(BoundsChecker)

    用BoundsChecker檢測後可得到兩個錯誤:Write overrun(寫越界) 和 Dangling pointer(使用被刪除的指針)而且都精確定位到了出錯的位置。是個不錯的工具。
    參見圖2(c02.png)
    參見圖3(c03.png)

  1.3 注意vc的輸出日誌
    由於一些目前未知的原因(有可能是程序的錯誤太嚴重或是BoundsChecker本身的bug),BoundsChekcer有時不能正常工作。
    這裏vc的輸出日誌有時能提供一些有用的信息。
    在難找的crash中,有很大一部分是引用了非法的指針。

    有時在vc的輸出日誌裏可以看到類似於這樣的信息
    “emule.exe 中的 0x004277b7 處最可能的異常: 0xC0000005: 讀取位置 0xfeeeff62 時發生訪問衝突 。”
    在缺少BoundsChecker的支持時,這是一條很重要的信息。意思是說在“程序地址0x004277b7處”對“值爲0xfeeeff62的指針”進行操作。
    (怎麼通過“程序地址0x004277b7”找到對應的代碼行可參照 3.1,)
    這條信息的重要性在於,這個操作只會觸發一個警告,而不會導致crash,當crash真正發生時,很有可能不會在0x004277b7附近,
    甚至調用堆棧都已經被寫亂,讓你無從下手。(參見5.1 爲什麼程序crash時調用堆棧是亂的)

2. 定位發佈在外的版本的Crash錯誤
  發佈在外的軟件crash了,往往不好調試,所以目前很多軟件都有“發送錯誤報告”這一功能。
  實現這一功能一般分以下幾步:

  a. 使用SetUnhandledExceptionFilter函數
    使用SetUnhandledExceptionFilter設置最高一級的異常處理函數,當程序出現任何未處理的異常,都會觸發你設置的函數裏。具體使用可參照msdn和emule源碼。
  b. 使用MiniDumpWriteDump函數
    在你的異常處理函數裏,使用MiniDumpWriteDump把錯誤信息存成特定格式的文件。具體使用可參照msdn和emule源碼。
  c. 發送錯誤報告
    選用一種形式把第二步產生的錯誤報告(.dmp)文件發送給你指定的地方。
  d. 查看錯誤報告
    這裏介紹用vc查看錯誤報告的方法,還可以用windows debug tools這個工具看,方法見5.2 使用windows debug tools查看.dmp文件(錯誤報告)
    
    查看錯誤報告需要有三樣東西:對應release版的代碼,當時編譯release版所產生的.exe和.pdb文件。(這兩個文件都在編譯的輸出目錄裏。)所以當程序發佈時,要保留下這兩個文件。
    把.dmp(錯誤報告文件), .pdb, .exe. 代碼,在同一目錄下,用vc打開.dmp 文件。
    按F5運行,程序即到達crash時的狀態,可以對其進行相應的分析。
    

  一點補充:當沒有“發送錯誤報告”的功能,或是此功能失效,以致彈出了windows的“發送錯誤報告”的對話框。這時其實也是有錯誤報告的,一般在C:Documents and Settings用戶名Local SettingsTemp裏的一個.dmp文件(一般只有一個.dmp)

3. 小技巧
  3.1 根據程序地址找到代碼位置
    可按如下步驟:
    a. 使程序處於停止狀態。(比如程序運行時,在vc裏按Ctrl+Alt+Break,或設斷點使程序停下)
    b. 切換到彙編狀態。(Ctrl+F11)
    c. 在地址欄輸入程序地址,回車。 
    d. 可按Ctrl+F11切回代碼模式。
  3.2 根據消息值查看對應的windows消息
    在vc的監視窗口裏輸入“消息值,wm”即可看到對應的消息。
  3.3 查看GetLastError返回值
    在vc的監視窗口裏輸入“@err,hr”,即可看到LastError及其解釋。
  3.4 在代碼中暫停程序
    在debug版中可以在代碼中加上“AfxDebugBreak();”以暫停程序。release版可使用 “_asm int 3;”
4. 編程小警示
  4.1 慎用IsBadPtr系列函數

    當使用IsBadReadPtr, IsBadWritePtr, IsBadCodePtr一系列函數時要注意,這一類函數可能並不能達到你所想要的意圖。
    比如下面的代碼,兩個返回都是false。

代碼

    
    char *p = new char[10];
    bool b;
    b = IsBadReadPtr(p+10, 1);
    delete[] p;
    char *q = new char[10];
    b = IsBadReadPtr(p, 1);


    所以切忌在程序中以IsBadPtr函數來判斷是否可以對這個指針進行操作。這些函數只能在調試中使用,或是你確切的知道這些函數的返回值表示的是什麼意義的時候。

  4.2 慎用catch(...)
    爲了防止程序crash或是解決一個不明白的crash時,大家很容易想到一個 try{}catch(...){}來解決問題。
    的確大部分時間這樣不會出問題了,但這個try-catch很有可能隱藏掉在try裏面的錯誤,而當由此錯誤引起其他錯誤時,就很難追蹤到這個問題了。
    所以建議儘量少用catch(...),如果知道某一塊代碼會拋出異常,應該用確切的寫法。比如catch(CFileException *e), catch(int e)等等。

5. 附錄
  5.1 爲什麼程序crash時調用堆棧是亂的

    當內存被寫亂時程序很有可能出現很難定位的crash,比如調用堆棧是亂的,或走到不存在的代碼裏。這裏舉一例

代碼


    class CA
    {
    public:
        CA(){}
        ~CA(){}
        virtual f(){}
    };

    void show()
    {
        printf("shown");
    }

    int main()
    {
        // 對像被創建刪除
        CA *p = new CA;
        delete p;

        // 一些正常的操作
        int *q = new int;
        int codeAddress = (int)show;
        *q = (int)&codeAddress;

        // 調用被刪除的對像,程序有可能執行到任何地方。
        p->f();
    }


    上面代碼的結果會走到show()裏顯示出show(這跟編譯環境有關,vc2003下測試結果是這樣)。如果你瞭解c++的vtable機制就明白這是怎麼回事。
    如果不明白也沒關係,我下面說個大概。

    首先CA對象被創建又被刪除。如果此時調用p->f()多半會crash。
    第二塊代碼可視爲一些正常的操作,new了一個q,然後對它進行了一些賦值。
    如果你不明白vtable機制,那你只要知道,最後一行的 “p->f();”會執行變量q所指向的變量所指向的變量所標示的地址。(這裏沒打錯字,是兩個“所指向的變量”smile.gif
    這裏我“心地善良”的給這個值賦上了一個合法的函數地址show。但實際程序中q所指向的變量有可能是任意值,它再指向的變量就更是任意值了。
    那程序就不知道跑哪裏去了。如果這個值過於離譜,那算你運氣好,程序會立即crash,而你就知道錯誤位置在哪了。但討人厭的是,它有可能是一個合法地址,那程序就繼續走下去,
    但遲早會crash,並且調用堆棧面目全非(原因牽涉到“調用堆棧的推導”的問題這裏就不多說了),
    到時就根本無從知道原來是調用了被刪除的p對象而導致的。

  5.2 使用Debugging tools for windows查看.dmp文件(錯誤報告)
    a. 準備好程序對應的代碼,exe文件,pdb文件(編譯時在編譯輸出目錄裏)
    b. 安裝WinDbg
    c. 在winDbg裏把Symbol目錄設在.pdb所在目錄,Image目錄設在.exe所在目錄,code目錄設到代碼目錄。
    d. 打開.dmp文件
    e. 輸入命令.ecxr。(此命令使環境回到崩潰時的狀態)
    f. 打開調用堆棧(ALT + F6)查看Crash的位置
    g. 進行分析

簡介
  (FinalCheck能檢測出的錯誤列表見附錄1)
  BoundsChecker是一個很強大的調試工具。這裏只簡單介紹如何用它的FinalCheck模式定位比較難定位的錯誤。
FinalCheck模式簡單來說就是BoundsChecker在你的代碼里加一些診斷代碼來檢查平時比較難查出的內存越界,錯誤的指針使用等。
不過付出的代價就是程序跑起來會比較慢,所以在不用時最好是把FinalCheck模式關掉。特別是發佈前。

BoundsChecker下載地址
ed2k://|file|BoundsChecker.v7.2.rar|62579029|6032ED8CA789C23D1CC1553946F814A0|h=D3OR5R3FZXJSV7I5A7HWTFHSRZPTLN4N|/


啓用FinalCheck模式(基於Visual Studio 2003)
  1. 在VC的菜單裏的“工具->BoundsChecker”
    a. 選中“Error Detection” (選中此項讓你在調試運行時讓BoundsChecker同時檢測程序的錯誤,不選中就是普通的調試程序)
    b. 選中“Log Event”
    c. 去掉“Display error and pause”(出現錯誤時是否立即提示,可以試試選中它看看是什麼效果)
    d. 選中FinalCheck(編譯時加入BoundsChecker的診斷代碼,不再需要此功能時,要把這個選項去掉再把工程重新編譯一遍)
  2. 在VC的菜單裏打開“工具->BoundsChecker->Options”確認裏面的“Memory Tracking->Enable FinalCheck”被選中。
  3. 重新編譯你的工程,這時BoundsChecker會在編譯的過程中插入些診斷代碼用於之後的監測。(如果編譯不通過,參看附錄2)
  4. 按F5調試運行你的程序
  -這時你的程序就在BoundsChecker的監測下運行起來了。

查看錯誤信息
  此時你的解決方案裏會多出一個 DevPartner Sessions->BoundsChecker->BoundsChecker-Active Session
  雙擊它可以看到目前出現的錯誤。
  我們關注Errors那個頁籤,其他的可以自行研究。這裏有很多錯誤。有的會有源碼。
  不明的這個錯誤說什麼的可以右鍵點擊這個錯誤,點擊explain裏面有很詳細的解釋。

性能及相關設置
  FinalCheck是很耗cpu和內存資源的,所以如果機器不好,可能會非常慢。這裏可以做想應設置先去掉一些檢測功能來加快速度。
  打開BoundsChecker的選項“工具->BoundsChecker->Options”
  1. Resource Tracking裏的Enable resource tracking可以先去掉,因爲暫時不需要對資源的檢測
  2. Memory Tracking中是對不同的情況進行監測,可以先去掉一些你不關心的。或是一次只監測一部分。

其他問題:
  1. 如果你的解決方案裏含有多個項目,那要注意FinalCheck是對於項目的,要注意哪個是當前項目。
  2. 遇到其他問題可以查看BoundsChecker的幫助,或在網上搜索。幫助在安裝目錄下的help裏的bc7.chm
  3. 如果不使用集成到VC裏的BoundsChecker,也可以使用安裝目錄下的BC7.exe去打開你的程序exe運行。
   但編譯還是要按上面所說的編譯。另注意BC7.exe的"setting->Memory Tracking->Enable FinalCheck"要被選上。
  4. 如果過程中你遇到問題歡迎跟貼。

附錄1: BoundsChecker的FinalCheck模式能檢測出的錯誤列表

 Pointer Errors - 指針錯誤
  Array index out of range - 使用越界的數組索引
  Assigning pointer out of range - 使用越界的指針
  Expression uses dangling pointer - 使用野指針
  Expression uses unrelated pointer - 不相關指針相互比較
  Function pointer is not a function - 函數指針指向的不是函數地址

 Memory Errors - 內存錯誤
  Reading overflows memory - 越界讀內存
  Reading uninitialized memory - 讀未初始化的內存
  Writing overflows memory - 越界寫內存

 Leak Errors - 泄漏
  Memory leaked due to free - 未釋放內嵌指針導致的內存泄漏
  Memory leaked due to reassignment - 指針重賦值導致的內存泄漏
  Memory leaked leaving scope - 離開作用域導致的內存泄漏
  Returning pointer to local variable - 返回局部變量的指針

附錄2:與FinalCheck衝突的編譯參數

 在使用FinalCheck重新編譯工程的過程中可能會出現一些編譯錯誤,因爲FinalCheck跟一些編譯選項有衝突,
 目前所知的有:
 a. 關掉“常規->全程序優化”
 b. “C/C++ -> 優化 -> 內聯函數展開”設成默認。
 遇到其他問題對照它給出的信息做相應設置修改就行了

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章