本節書摘來自華章出版社《effective debugging:軟體和系統調試的66個有效方法》一書中的第1章,第1.5節,作[希]迪歐米迪斯·斯賓奈裡斯(diomidis spinellis),更多章節内容可以通路雲栖社群“華章計算機”公衆号檢視
我們通常都能夠同時通路這樣兩個系統,其中一個是發生故障的系統,另一個是與之相似但卻可以正常運作的系統。當我們實作了某項新功能、更新了某些工具或基礎元件,或是把系統部署在某個新的平台上面時,就可能會遇到新系統無法正常運作的問題,此時如果舊系統依然正常,那麼我們通常可以通過尋找(下面就會講到如何尋找)或盡量縮小(參見第45條)新舊兩個系統之間的差别來鎖定問題的原因。
之是以能根據新舊系統間的差距來進行調試,其原因在于:盡管各人所經曆的問題有所不同,但計算機的底層運作方式卻是十分确定的,也就是說,同樣的輸入會産生同樣的輸出。是以,隻要能夠深入故障系統中,并對其進行足夠的探查,我們就遲早能夠找到相關的bug,進而揭示出該系統為什麼會在行為上與正常系統有所不同。
其實有很多時候,系統的故障原因都會非常明确地出現在你面前,隻要你肯打開程式的日志檔案(參見第56條),就有可能發現裡面有一條消息告訴你,clients.conf這個配置檔案有錯誤:

在另外一些情況下,錯誤的原因可能會隐藏得比較深,此時你必須提升系統日志的詳細程度(verbosity),才能把它暴露出來。
如果系統沒有提供足夠詳細的日志機制,那我們就需要用追蹤工具來梳理其運作時的行為。除了dtrace和systemtap等通用的工具,還有一些專門的工具可以用來追蹤對作業系統的調用(strace、truss、procmon)、對動态連結庫的調用(ltrace、procmon)、網絡包(tcpdump、wireshark)以及sql資料庫調用(參見第58條)。有很多unix應用程式(如r project)是借助複雜的shell腳本來啟動的,是以可能會以極其隐晦的方式出錯。針對這樣的錯誤,在大多數情況下,我們都可以通過給相應shell傳入-x選項的辦法來進行追蹤,這樣得到的資料通常很龐大,所幸現在的系統都有很大的容量能夠存放這兩份日志(以其中一份表示那個可以正常運作的系統,另一份表示出現了故障的系統),而且都有很強的cpu能夠對其進行處理與比較。
就系統的操作環境而言,我們應該盡量確定這兩個系統擁有相似的環境,因為這樣能夠更加友善地對比日志檔案或追蹤資訊,有時甚至可以直接找到造成bug的原因。我們可以先從一些較為明顯的部分入手,例如,程式的輸入以及指令行參數等。與早前所說的原則一樣,我們也要親自進行驗證,而不能想當然地接受假設。例如,應該在兩個系統的輸入檔案之間進行對比,如果它們都比較龐大并且離得比較遠,那可以考慮對比它們的md5校驗和。
然後,我們應該把重點放在代碼上。首先對源代碼進行對比,我們可能要挖得深一些才能找到bug所在的地方。可以通過ldd指令(适用于unix系統)或是帶有/dependents選項的dumpbin指令(适用于visual studio)來檢視與每個可執行檔案有關的動态程式庫,并通過nm指令(适用于unix系統)、帶有/exports/imports選項的dumpbin指令(适用于visual studio)或javap指令(适用于以java語言開發出來的程式)來檢視程式所定義和使用的符号。如果你确信問題肯定出現在代碼中,但又看不出明顯的差别,那麼可能就要往更深的層次去探查了,也就是需要對比由編譯器所生成的彙編代碼(參見第37條)。
然而在進行更深層次的探查之前,應該先考慮一下有沒有其他因素會影響程式的執行情況,環境變量就是這樣一個容易忽視的因素,即便是沒有特權的使用者,也依然可以通過設定環境變量來破壞程式的正常執行。另一個因素是作業系統。與運作着正常程式的那個作業系統相比,故障程式所在的這個作業系統,可能新了10年或是舊了10年。此外,也要考慮編譯器、開發架構、第三方連結庫、浏覽器、應用程式伺服器、資料庫系統以及其他一些中間件。至于怎樣在這麼多的因素中确定問題的根源,則是我們接下來要講的話題。
大多數情況下,我們都是在一堆幹草裡面找一根針(大海撈針),是以應該盡量使這堆幹草變得小一些,于是,就要花時間來構造一個既能展現bug,又最為簡單的測試用例(參見第10條)。(另外一種辦法是把要找的針變大一些,也就是指令這個有bug的程式輸出更多的資訊,然而這種做法很少能起到比較好的效果。)簡明的測試用例可以縮短日志檔案與追蹤資訊的長度并減少處理時間,進而令調試工作變得更加輕松。要想有條理地簡化測試用例,我們可以在確定能夠重制bug的前提下,逐漸删除用例中的元素或系統中的配置選項,直到删至最簡。
如果正常系統和故障系統的差別位于源代碼中,那麼有一種很實用的辦法,就是對這兩個版本之間的曆次修改進行二分搜尋(binary search),以确定問題所在。例如,如果正常系統的版本号是100,而故障系統的版本号是132,那我們首先測試116版的程式是否正常,如果116版正常,那就判斷它與132版之間的中點,也就是124版是否正常,如果116版有錯,則判斷它與100版之間的中點,也就是108版是否正常,并依此類推。每次修改完程式之後,我們都應該把代碼單獨送出到版本控制系統裡面,這樣做的好處之一,就是使得我們能夠進行二分搜尋。某些版本控制系統提供了可以自動執行搜尋的指令,例如,git就提供了git bisect指令(參見第26條)。
還有一個很有效的辦法,是用unix工具對比兩份日志檔案(參見第56條),以找出其中與bug有關的差別。我們在這種情況下所使用的工具,是diff指令,它可以顯示出兩份檔案的不同之處。然而日志檔案經常會在無關緊要的地方表現出差别,這會把那些與bug真正有關的差别給掩蓋掉,于是,我們可以考慮用各種辦法來過濾幹擾因素。例如,如果每一行開頭的幾個字段,都是時間戳與程序id等資訊,那我們就可以用cut或awk指令來把這些大同小異的資訊裁掉。下面這條指令可以對unix系統的messages日志檔案進行裁切,它會從每一行的第4個字段開始顯示其内容:
隻把你感興趣的那些事件選出來就可以了,例如,如果你隻對打開的檔案感興趣,那麼可以用grep'open('這樣的指令來進行篩選。你也可以用grep-v gettimeofday等指令來把對自己有幹擾的文本行過濾掉(例如,在java程式裡面,會有成千上萬次與擷取系統時間有關的調用)。此外,還可以在sed指令中指定适當的正規表達式,以便把文本行中自己不感興趣的那一部分裁掉。
最後再講一個進階的實用技巧:如果兩份檔案各自的排序方式無法使diff指令給出有效的對比結果,那我們可以把感興趣的字段提取出來,對其進行排序,然後用comm工具在排好順序的兩個集合中找尋不同的元素。例如,如果我們想對比t1和t2這兩份追蹤資訊,以找出有哪些檔案隻出現于t1中,那麼可以在unix的bash shell中輸入下列指令,它會在包含字元串open(的那些文本行裡面提取表示檔案名的第二個字段,并在提取出來的這兩個集合之間尋找差别:
兩對小括号裡面的那兩個元素,會分别生成兩份有序清單,清單中的每一項都是一個傳給open的檔案名,而comm指令(這個指令用來在兩份清單之間尋找共同的元素)則以這兩份清單為輸入值,并把隻出現在第一份清單中的内容列出來。
要點
在能夠正常運作的系統與出現故障的系統之間對比,找出行為上的差別,以求發現故障的原因。
影響系統行為的所有因素都要考慮到,包括代碼、輸入、調用時的參數、環境變量、服務以及動态連結庫。