天天看點

[轉]兩個經典的windbg調試案例,值得學習

1. 調試Bug的神兵利器:通過WinDbg條件斷點收集Log

原文位址:http://blogs.msdn.com/yizhang/archive/2009/03/30/bug-windbg-log.aspx

調試Bug的神兵利器:通過WinDbg條件斷點收集Log

前段時間花了幾天一直在用WinDbg調試一個比較棘手的Bug。這個Bug是C# Team那邊發現的,他們的Testcase跑大概10分鐘左右會出一個在CLR内部的ASSERT。比較難調試的主要原因在于ASSERT表明一個全局的資料結構出現了問題,本來不應該用完的數組卻已經用完了(因為按照設計,這個數組是邊使用邊清理的,是不會用完的)。初步想到的有下面幾種方案來調試:

1. 設定資料斷點

2. 一步一步調試

3. 添加Log代碼

設定資料斷點的主要問題是不太好确定到底是因為什麼原因導緻的資料結構問題,而且因為是數組被用完,很難将是到底是哪一個數組元素的加入導緻了數組被全部占用,是以無法通過設定資料斷點的方法來調試。一步一步的調試顯然也沒法解決問題,因為這個Testcase本身要跑十分鐘,可以想象單步調試運作十分鐘的程式會花費多長時間。是以兩個方案都被我否決。添加Log代碼其實是可以的,隻是需要修改代碼,每次修改之後需要重新編譯代碼,然後需要在目标機器上安裝,而且C#使用的CLR的Branch并非我們正在開發的Branch,需要重新下載下傳源代碼,相對比較麻煩。最後為了解決這個問題,我采取的方法是使用WinDbg的條件斷點+Log的方式。大緻的方法如下:

第一步:在一個或者多個可疑處設定斷點

bu address “command”

bu是WinDbg中的設定Unresolved Breakpoints指令,用起來比較友善,我比較喜歡用。address就是你所要斷的代碼位址,可以是函數開始,也可以是某一行。Command非常重要,它表示了WinDbg在每次斷到address的時候都要執行的指令,不同指令用分号隔開,如:

.echo [Function A]; dv this; kb; g

這幾條指令意思是:列印[Function A],列印this指針的值,列印目前調用棧,然後繼續執行。大家可以根據實際情況添加一些其他指令列印一些自己所需要的資訊。通過上面這套指令列印的内容大緻如下:

[FunctionA]

this = 0xABCDEFG

module!FuncA

module!FuncB

module!FuncC

可以看出,這條斷點如果反複被斷,那麼在WinDbg的指令視窗中便會把每次斷點被Hit的相關資訊通過剛才定義的指令列印出來。如果定義了很多這樣的斷點,那麼在指令視窗中就會把整個程式執行的情況列印出來,起到Log的作用,而且可以顯示調用棧等資訊,比一般的Log要強大許多。

第二步:設定Log

預設情況下,WinDbg的Buffer大小是有限的,如果程式運作時間比較長,那麼Buffer可能會不夠,我們通過條件斷點打出的資訊會被截斷。幸好,WinDbg提供了将指令視窗的内容輸出到Log中的功能。選擇Edit->Open/Close Log File菜單項,WinDbg會顯示如下對話框:

在這個對話框裡面輸入你想要儲存的Log檔案名即可。如果是添加新的内容而不是覆寫原有的,則勾上Append。

第三步:分析Log

當獲得了Log資訊之後,下一步就需要分析Log的内容了,這是一件需要耐心、對資料的敏感、以及一點點運氣的事情。分析的時候可能發現Log的資訊不足,這時就需要添加新的斷點或者修改列印的資訊,重新收集Log,再加以分析,直到Log資訊足夠為止。這時WinDbg設定條件斷點的優勢就出來了,因為不需要修改代碼,編譯代碼,部署代碼這樣的一個過程,而是隻需要鍵入不同的指令而已。經過幾次調整斷點位置和列印的資訊并重新收集Log,我最終通過分析發現這個Bug是隻有可能在特定情況下RCW沒有被GC,并且建立線程退出的時候才會出現,具體的内容因為涉及到.NET 4.0中還沒有釋出的新功能,這裡就不多說了。可以看到,如果采用正常的方法,對于這種在特定的條件下才會重制的問題是很難發現的。

總之,使用WinDbg來設定條件斷點,列印相關資訊,并且輸出到Log檔案是一種非常強大的調試方法,可以調試一些非常複雜的Bug,而且具有不需要修改代碼的靈活性,可以自由定義自己想需要列印的資訊和斷點設定的位置,主要的缺點是方法稍顯複雜,不過如果适應了之後還是很友善的。我強烈推薦大家在遇到比較複雜的Bug的時候,可以嘗試使用一下這種方法,可能具有意想不到的效果哦。

如果一個程式跑10000次隻失敗一次,你會怎麼調試?

原址:http://blogs.msdn.com/yizhang/archive/2009/08/28/9887951.aspx

CLR小組中存在着大量的回歸測試,這些回歸測試會定期執行來發現CLR中的Bug,Developer在Checkin之前,也需要執行這些測試的一部分(大概是10小時左右,如果全部跑的話估計要好幾天)。這些測試對于保證CLR的品質是至關重要的。有時候,這些測試會偶爾失敗,比如跑100次失敗大概一到兩次,有些極端的例子甚至是10000次才失敗一次。像這種問題通常是很難調試的。在前面調試Bug的神兵利器:通過WinDbg條件斷點收集Log這篇文章中,我講到了如何通過條件斷點收集各種資訊來判斷Bug究竟出在哪裡。但是,這個方法還是不太管用,因為它不能夠反複執行某個程式。下面我要講一種技巧可以用來調試類似這樣的問題,這種技巧主要适用于下面幾種情況:

在程式出錯的時候,某些資訊、狀态已經丢失,無法通過目前出錯時候的狀态推斷出之前的狀态。說的稍微具體一點就是,比如某個變量變成了NULL導緻Access Violation,但是很難直接推斷出為什麼這個變量變成了NULL

程式運作時間較長,很難直接單步調試

程式較難修改加入列印代碼(比如加入新代碼并編譯非常花時間,或者該程式沒有源代碼

該程式運作次數較多的時候才能發現問題,也就是說問題不是每次都出現

#2和#4決定了一步步調試基本上是不可能的。#1和#3則意味着我們必須得使用條件斷點來收集資訊來判斷代碼的錯誤,因為直接調試出錯的位置是不可行的。下面了我來講一下如何用CDB(其實就是WinDbg的無UI版本,WinDbg=CDB+UI)來做到:

反複執行程式

當程式出錯的時候自動暫停

通過條件斷點收集資訊,隻保留出錯時候的那一次Log

我們先假設我們需要調試的程式叫做Hello.exe,每次出問題的現象是,調用某個函數Hello!Func()的時候,其參數arg為NULL。Arg這個變量是由某個全局變量g_arg傳入而來。我們可以通過硬體的資料斷點來檢視每次将g_arg指派為NULL的情況(當然了,指派為NULL并不代表是錯誤,隻有傳入Hello!Func的時候為NULL才是錯誤)。程式一般要跑10000次才可能發現問題。使用下面的指令行可以做到反複收集Func1(Func2、Func3因為類似,這裡就不列出了)執行時候的g_arg的值并放入Log檔案中,并且如果發現調用Hello!Func的時候arg參數為NULL,則停止程式:

for /L %i in (1, 1, 10000) DO CDB.exe -c "bu Hello!Func /".echo Inside Hello!Func; dv; .if (poi(arg)!=0) { g } /"; ba w4 Hello!g_arg /“.if (poi(Hello!g_arg)==0) { .echo g_arg changes to NULL; kb; }/”; g" -G -logo debug.log Hello.exe

我們來簡單分析一下:

一開頭的For語句用于執行CDB指令10000次,也就是調試Hello.exe一萬次

-c指令指定讓CDB在程式開始的時候執行下面的指令

bu Hello!Func “.echo Inside Hello!Func; dv; .if (poi(arg)!=0) { g }意思是每次Hello!Func被執行的時候,列印Inside Hello!Func,之後列印所有局部變量和參數(包括arg),如果發現arg!=NULL,則繼續。注意上面指令中的/”是轉義符,代表真正的引号,避免沖突。

ba w4 Hello!g_arg “.if (poi(Hello!g_arg)==0) { .echo g_arg changes to NULL; kb; }”意思是每次如果g_arg被修改成NULL,列印出Callstack

g指令表示讓程式開始執行

-G:表示讓CDB忽略程式結束的時候的Breakpoint,避免CDB在運作結束的時候停下,保證CDB可以持續執行不需要人工幹預

-logo debug.log:表示讓CDB把每次輸出的結果放入Debug.log中,并且每次都建立立檔案,也就是說,會把上一次覆寫。這正好是我們需要的,因為我們設定了一旦程式錯誤則停止,那麼這一次的Debug.log才是需要保留的

除了用-c指定初始的指令之外,也可以使用-cf來指定一個檔案包含任意條CDB指令,如果CDB指令較多,可以采用這種方法。

本文說道的方法是比較有效的,我自己曾經使用過這種方法解決過不少比較棘手的問題。如果碰到了此種需要運作10000次才能重制問題的Bug,不妨試一下本文的方法。

繼續閱讀