如何定位導致Crash的程式碼位置

mybwu_com發表於2014-03-11
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++ -> 優化 -> 行內函數展開”設成預設。
 遇到其他問題對照它給出的資訊做相應設定修改就行了

相關文章