《Effective Debugging:软件和系统调试的66个有效方法》一第5条:在能够正常运作的系统与发生故障的系统之间寻找差别...
本節(jié)書(shū)摘來(lái)自華章出版社《Effective Debugging:軟件和系統(tǒng)調(diào)試的66個(gè)有效方法》一書(shū)中的第1章,第1.5節(jié),作[希]迪歐米迪斯·斯賓奈里斯(Diomidis Spinellis),更多章節(jié)內(nèi)容可以訪(fǎng)問(wèn)云棲社區(qū)“華章計(jì)算機(jī)”公眾號(hào)查看
第5條:在能夠正常運(yùn)作的系統(tǒng)與發(fā)生故障的系統(tǒng)之間尋找差別
我們通常都能夠同時(shí)訪(fǎng)問(wèn)這樣兩個(gè)系統(tǒng),其中一個(gè)是發(fā)生故障的系統(tǒng),另一個(gè)是與之相似但卻可以正常運(yùn)行的系統(tǒng)。當(dāng)我們實(shí)現(xiàn)了某項(xiàng)新功能、更新了某些工具或基礎(chǔ)組件,或是把系統(tǒng)部署在某個(gè)新的平臺(tái)上面時(shí),就可能會(huì)遇到新系統(tǒng)無(wú)法正常運(yùn)行的問(wèn)題,此時(shí)如果舊系統(tǒng)依然正常,那么我們通常可以通過(guò)尋找(下面就會(huì)講到如何尋找)或盡量縮小(參見(jiàn)第45條)新舊兩個(gè)系統(tǒng)之間的差別來(lái)鎖定問(wèn)題的原因。
之所以能根據(jù)新舊系統(tǒng)間的差距來(lái)進(jìn)行調(diào)試,其原因在于:盡管各人所經(jīng)歷的問(wèn)題有所不同,但計(jì)算機(jī)的底層運(yùn)作方式卻是十分確定的,也就是說(shuō),同樣的輸入會(huì)產(chǎn)生同樣的輸出。因此,只要能夠深入故障系統(tǒng)中,并對(duì)其進(jìn)行足夠的探查,我們就遲早能夠找到相關(guān)的bug,從而揭示出該系統(tǒng)為什么會(huì)在行為上與正常系統(tǒng)有所不同。
其實(shí)有很多時(shí)候,系統(tǒng)的故障原因都會(huì)非常明確地出現(xiàn)在你面前,只要你肯打開(kāi)程序的日志文件(參見(jiàn)第56條),就有可能發(fā)現(xiàn)里面有一條消息告訴你,clients.conf這個(gè)配置文件有錯(cuò)誤:
在另外一些情況下,錯(cuò)誤的原因可能會(huì)隱藏得比較深,此時(shí)你必須提升系統(tǒng)日志的詳細(xì)程度(verbosity),才能把它暴露出來(lái)。
如果系統(tǒng)沒(méi)有提供足夠詳細(xì)的日志機(jī)制,那我們就需要用追蹤工具來(lái)梳理其運(yùn)行時(shí)的行為。除了DTrace和SystemTap等通用的工具,還有一些專(zhuān)門(mén)的工具可以用來(lái)追蹤對(duì)操作系統(tǒng)的調(diào)用(strace、truss、Procmon)、對(duì)動(dòng)態(tài)鏈接庫(kù)的調(diào)用(ltrace、Procmon)、網(wǎng)絡(luò)包(tcpdump、Wireshark)以及SQL數(shù)據(jù)庫(kù)調(diào)用(參見(jiàn)第58條)。有很多Unix應(yīng)用程序(如R Project)是借助復(fù)雜的shell腳本來(lái)啟動(dòng)的,因此可能會(huì)以極其隱晦的方式出錯(cuò)。針對(duì)這樣的錯(cuò)誤,在大多數(shù)情況下,我們都可以通過(guò)給相應(yīng)shell傳入-x選項(xiàng)的辦法來(lái)進(jìn)行追蹤,這樣得到的數(shù)據(jù)通常很龐大,所幸現(xiàn)在的系統(tǒng)都有很大的容量能夠存放這兩份日志(以其中一份表示那個(gè)可以正常運(yùn)作的系統(tǒng),另一份表示出現(xiàn)了故障的系統(tǒng)),而且都有很強(qiáng)的CPU能夠?qū)ζ溥M(jìn)行處理與比較。
就系統(tǒng)的操作環(huán)境而言,我們應(yīng)該盡量確保這兩個(gè)系統(tǒng)擁有相似的環(huán)境,因?yàn)檫@樣能夠更加方便地對(duì)比日志文件或追蹤信息,有時(shí)甚至可以直接找到造成bug的原因。我們可以先從一些較為明顯的部分入手,例如,程序的輸入以及命令行參數(shù)等。與早前所說(shuō)的原則一樣,我們也要親自進(jìn)行驗(yàn)證,而不能想當(dāng)然地接受假設(shè)。例如,應(yīng)該在兩個(gè)系統(tǒng)的輸入文件之間進(jìn)行對(duì)比,如果它們都比較龐大并且離得比較遠(yuǎn),那可以考慮對(duì)比它們的MD5校驗(yàn)和。
然后,我們應(yīng)該把重點(diǎn)放在代碼上。首先對(duì)源代碼進(jìn)行對(duì)比,我們可能要挖得深一些才能找到bug所在的地方。可以通過(guò)ldd命令(適用于Unix系統(tǒng))或是帶有/dependents選項(xiàng)的dumpbin命令(適用于Visual Studio)來(lái)查看與每個(gè)可執(zhí)行文件有關(guān)的動(dòng)態(tài)程序庫(kù),并通過(guò)nm命令(適用于Unix系統(tǒng))、帶有/exports/imports選項(xiàng)的dumpbin命令(適用于Visual Studio)或javap命令(適用于以Java語(yǔ)言開(kāi)發(fā)出來(lái)的程序)來(lái)查看程序所定義和使用的符號(hào)。如果你確信問(wèn)題肯定出現(xiàn)在代碼中,但又看不出明顯的差別,那么可能就要往更深的層次去探查了,也就是需要對(duì)比由編譯器所生成的匯編代碼(參見(jiàn)第37條)。
然而在進(jìn)行更深層次的探查之前,應(yīng)該先考慮一下有沒(méi)有其他因素會(huì)影響程序的執(zhí)行情況,環(huán)境變量就是這樣一個(gè)容易忽視的因素,即便是沒(méi)有特權(quán)的用戶(hù),也依然可以通過(guò)設(shè)置環(huán)境變量來(lái)破壞程序的正常執(zhí)行。另一個(gè)因素是操作系統(tǒng)。與運(yùn)行著正常程序的那個(gè)操作系統(tǒng)相比,故障程序所在的這個(gè)操作系統(tǒng),可能新了10年或是舊了10年。此外,也要考慮編譯器、開(kāi)發(fā)框架、第三方鏈接庫(kù)、瀏覽器、應(yīng)用程序服務(wù)器、數(shù)據(jù)庫(kù)系統(tǒng)以及其他一些中間件。至于怎樣在這么多的因素中確定問(wèn)題的根源,則是我們接下來(lái)要講的話(huà)題。
大多數(shù)情況下,我們都是在一堆干草里面找一根針(大海撈針),因此應(yīng)該盡量使這堆干草變得小一些,于是,就要花時(shí)間來(lái)構(gòu)造一個(gè)既能體現(xiàn)bug,又最為簡(jiǎn)單的測(cè)試用例(參見(jiàn)第10條)。(另外一種辦法是把要找的針變大一些,也就是命令這個(gè)有bug的程序輸出更多的信息,然而這種做法很少能起到比較好的效果。)簡(jiǎn)明的測(cè)試用例可以縮短日志文件與追蹤信息的長(zhǎng)度并減少處理時(shí)間,從而令調(diào)試工作變得更加輕松。要想有條理地簡(jiǎn)化測(cè)試用例,我們可以在確保能夠重現(xiàn)bug的前提下,逐漸刪除用例中的元素或系統(tǒng)中的配置選項(xiàng),直到刪至最簡(jiǎn)。
如果正常系統(tǒng)和故障系統(tǒng)的區(qū)別位于源代碼中,那么有一種很實(shí)用的辦法,就是對(duì)這兩個(gè)版本之間的歷次修改進(jìn)行二分搜索(binary search),以確定問(wèn)題所在。例如,如果正常系統(tǒng)的版本號(hào)是100,而故障系統(tǒng)的版本號(hào)是132,那我們首先測(cè)試116版的程序是否正常,如果116版正常,那就判斷它與132版之間的中點(diǎn),也就是124版是否正常,如果116版有錯(cuò),則判斷它與100版之間的中點(diǎn),也就是108版是否正常,并依此類(lèi)推。每次修改完程序之后,我們都應(yīng)該把代碼單獨(dú)提交到版本控制系統(tǒng)里面,這樣做的好處之一,就是使得我們能夠進(jìn)行二分搜索。某些版本控制系統(tǒng)提供了可以自動(dòng)執(zhí)行搜索的命令,例如,Git就提供了git bisect命令(參見(jiàn)第26條)。
還有一個(gè)很有效的辦法,是用Unix工具對(duì)比兩份日志文件(參見(jiàn)第56條),以找出其中與bug有關(guān)的區(qū)別。我們?cè)谶@種情況下所使用的工具,是diff命令,它可以顯示出兩份文件的不同之處。然而日志文件經(jīng)常會(huì)在無(wú)關(guān)緊要的地方表現(xiàn)出差別,這會(huì)把那些與bug真正有關(guān)的差別給掩蓋掉,于是,我們可以考慮用各種辦法來(lái)過(guò)濾干擾因素。例如,如果每一行開(kāi)頭的幾個(gè)字段,都是時(shí)間戳與進(jìn)程ID等信息,那我們就可以用cut或awk命令來(lái)把這些大同小異的信息裁掉。下面這條命令可以對(duì)Unix系統(tǒng)的messages日志文件進(jìn)行裁切,它會(huì)從每一行的第4個(gè)字段開(kāi)始顯示其內(nèi)容:
只把你感興趣的那些事件選出來(lái)就可以了,例如,如果你只對(duì)打開(kāi)的文件感興趣,那么可以用grep'open('這樣的命令來(lái)進(jìn)行篩選。你也可以用grep-v gettimeofday等命令來(lái)把對(duì)自己有干擾的文本行過(guò)濾掉(例如,在Java程序里面,會(huì)有成千上萬(wàn)次與獲取系統(tǒng)時(shí)間有關(guān)的調(diào)用)。此外,還可以在sed命令中指定適當(dāng)?shù)恼齽t表達(dá)式,以便把文本行中自己不感興趣的那一部分裁掉。
最后再講一個(gè)高級(jí)的實(shí)用技巧:如果兩份文件各自的排序方式無(wú)法使diff命令給出有效的對(duì)比結(jié)果,那我們可以把感興趣的字段提取出來(lái),對(duì)其進(jìn)行排序,然后用comm工具在排好順序的兩個(gè)集合中找尋不同的元素。例如,如果我們想對(duì)比t1和t2這兩份追蹤信息,以找出有哪些文件只出現(xiàn)于t1中,那么可以在Unix的Bash shell中輸入下列命令,它會(huì)在包含字符串open(的那些文本行里面提取表示文件名的第二個(gè)字段,并在提取出來(lái)的這兩個(gè)集合之間尋找差別:
兩對(duì)小括號(hào)里面的那兩個(gè)元素,會(huì)分別生成兩份有序列表,列表中的每一項(xiàng)都是一個(gè)傳給open的文件名,而comm命令(這個(gè)命令用來(lái)在兩份列表之間尋找共同的元素)則以這兩份列表為輸入值,并把只出現(xiàn)在第一份列表中的內(nèi)容列出來(lái)。
要點(diǎn)
在能夠正常運(yùn)作的系統(tǒng)與出現(xiàn)故障的系統(tǒng)之間對(duì)比,找出行為上的區(qū)別,以求發(fā)現(xiàn)故障的原因。
影響系統(tǒng)行為的所有因素都要考慮到,包括代碼、輸入、調(diào)用時(shí)的參數(shù)、環(huán)境變量、服務(wù)以及動(dòng)態(tài)鏈接庫(kù)。
總結(jié)
以上是生活随笔為你收集整理的《Effective Debugging:软件和系统调试的66个有效方法》一第5条:在能够正常运作的系统与发生故障的系统之间寻找差别...的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 双曲线和直线联立公式_圆锥曲线联解公式
- 下一篇: 解决 windows10和ubuntu1