除錯bug的神兵利器:通過windbg條件斷點收集log
前段時間花了幾天一直在用windbg除錯乙個比較棘手的bug。這個bug是c# team那邊發現的,他們的testcase跑大概10分鐘左右會出乙個在clr內部的assert。比較難除錯的主要原因在於assert表明乙個全域性的資料結構出現了問題,本來不應該用完的陣列卻已經用完了(因為按照設計,這個陣列是邊使用邊清理的,是不會用完的)。初步想到的有下面幾種方案來除錯:
1. 設定資料斷點
2. 一步一步除錯
3. 新增log**
第一步:在乙個或者多個可疑處設定斷點
bu address 「command」
.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
當獲得了log資訊之後,下一步就需要分析log的內容了,這是一件需要耐心、對資料的敏感、以及一點點運氣的事情。分析的時候可能發現log的資訊不足,這時就需要新增新的斷點或者修改列印的資訊,重新收集log,再加以分析,直到log資訊足夠為止。這時windbg設定條件斷點的優勢就出來了,因為不需要修改**,編譯**,部署**這樣的乙個過程,而是只需要鍵入不同的命令而已。經過幾次調整斷點位置和列印的資訊並重新收集log,我最終通過分析發現這個bug是只有可能在特定情況下rcw沒有被gc,並且建立執行緒退出的時候才會出現,具體的內容因為涉及到.net 4.0中還沒有發布的新功能,這裡就不多說了。可以看到,如果採用常規的方法,對於這種在特定的條件下才會重現的問題是很難發現的。
總之,使用windbg來設定條件斷點,列印相關資訊,並且輸出到log檔案是一種非常強大的除錯方法,可以除錯一些非常複雜的bug,而且具有不需要修改**的靈活性,可以自由定義自己想需要列印的資訊和斷點設定的位置,主要的缺點是方法稍顯複雜,不過如果適應了之後還是很方便的。我強烈推薦大家在遇到比較複雜的bug的時候,可以嘗試使用一下這種方法,可能具有意想不到的效果哦。
如果乙個程式跑10000次只失敗一次,你會怎麼除錯?
原址: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) \"; ba w4 hello!g_arg \「.if (poi(hello!g_arg)==0) \」; 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) 意思是每次hello!func被執行的時候,列印inside hello!func,之後列印所有區域性變數和引數(包括arg),如果發現arg!=null,則繼續。注意上面命令中的\」是轉義符,代表真正的引號,避免衝突。
ba w4 hello!g_arg 「.if (poi(hello!g_arg)==0) 」意思是每次如果g_arg被修改成null,列印出callstack
g命令表示讓程式開始執行
-g:表示讓cdb忽略程式結束的時候的breakpoint,避免cdb在執行結束的時候停下,保證cdb可以持續執行不需要人工干預
-logo debug.log:表示讓cdb把每次輸出的結果放入debug.log中,並且每次都新建立檔案,也就是說,會把上一次覆蓋。這正好是我們需要的,因為我們設定了一旦程式錯誤則停止,那麼這一次的debug.log才是需要保留的
除了用-c指定初始的命令之外,也可以使用-cf來指定乙個檔案包含任意條cdb命令,如果cdb命令較多,可以採用這種方法。
本文說道的方法是比較有效的,我自己曾經使用過這種方法解決過不少比較棘手的問題。如果碰到了此種需要執行10000次才能重現問題的bug,不妨試一下本文的方法。
兩個測試小案例
案例1 測試人員在測試系統發現在系統a和系統b之間通過匯流排通訊,偶爾會出現timeout現象。反饋開發後,開發難以重現。根據簡要分析後,認為是測試系統效能不行,拍胸脯保證在生產系統,用於系統通訊的匯流排不會出現這種問題。測試人員加強了效能測試強度,發現硬體提高後,的確效能測試場景中未能重現time...
兩個測試小案例
案例1 測試人員在測試系統發現在系統a和系統b之間通過匯流排通訊,偶爾會出現timeout現象。反饋開發後,開發難以重現。根據簡要分析後,認為是測試系統效能不行,拍胸脯保證在生產系統,用於系統通訊的匯流排不會出現這種問題。測試人員加強了效能測試強度,發現硬體提高後,的確效能測試場景中未能重現time...
兩個lock的經典使用示例
示例一 public class numberprintdemo catch interruptedexception e 當state 1時,輪到執行緒1列印5次數字 for int j 0 j 5 j system.out.println 執行緒1列印完成後,將state賦值為2,表示接下來將輪...