(原文:Thoughts on Debugging, Part 1 作者:Mark Dalrymple 譯者:archerlu)
有人說我是個相當不錯的調試者。朋友到我這裡解決軟件問題,偶爾我也能幫上忙。不過,怎樣才算是個好的調試者,什麼是調試呢?是讓軟件運行得更好嗎,還是設法減少軟件的不確定性呢?我們都知道“調試(Debugging)”是因為我們都做這事,盡管如此,這依然是個有趣的問題。你可以把調試過程視為向軟件輸入一種新狀態——程序像一個大型的狀態機,你只不過把它從一個狀態切換到另一個狀態。(譯者注:狀態機是大家熟悉的概念,一個狀態機因不同的狀態有不同的輸出結果。作者在這裡視出錯的程序為狀態機,正確的輸入,卻輸出了其它的結果)
換而言之,我覺得思考這種問題是一種不錯的消遣,最好能因此與朋友共享一瓶威士忌(原文為格蘭布雷頓,一種加拿大產的威士忌)。但是,最終我一無所獲。
對我來說,調試只是一種技能,一種分析技巧。根本上,它是一種可以通過實踐來學習深入的技能:你認定問題,分析系統,找到引起問題的點,作必要的修正,得到解決方案。方案能否采用,則取決於綜合角度:如果改動影響整個App,最好還是聽任不管為妙。
調試有時真的很困難。我第一個專業級的bug花了整整一個月來處理。必須說明的是,這裡有大部分時間都用來與目標軟件打交道,學習相關工具。在軟件業,一個月是很長的時間了。當然,經過練習,調試難度可以變得更容易一些。
對某一類人來說,調試可能更容易上手。這是個需要實踐的過程。你不必關心總體計劃或架構,只需要把握某些細節。當軟件系統中出現特定問題,你要做的,只是定位和更正錯誤。幸運的是,我還算是個能動手的人(我很害怕玩策略游戲)(譯者注:動手和策略的關系,類似於戰術和戰略的關系,作者認為自己比較能處理細節,而不是作出總體規劃),或許是因為我在樂隊(流行樂隊和管弦樂團)裡度過了不少年頭的關系。作曲人和樂隊指揮早已確定了樂章總體,而我作為一個樂手,只需要執行屬於我那部分就行了。
痛苦級別
我把bug分為三類。
第一類:五分鐘修理類。這一類問題是指從前遇到過、了解過的問題。布倫特·西蒙斯在一個有趣的帖子上提到過,對於那些不熟悉開發的人來說,一個災難性的bug看起來有糟糕,修復起來就有多難。其實情況正好相反。程序崩潰很多都是這種。程序出現了一個錯誤,用戶數據丟失,可能連磁盤上的數據都丟失了。因為時常發生,你就能發現端倪,知道它在何處出錯,認識到這是NSLog參數出錯;NSArray用index檢索返回了NSNotFound結果;或只是你想跳過一個未初始化的指針。
第二類:一小時至一天修復類。這一類bug在調查過程中,會得益於某些機構的提示——你應該注意那些,你別管這些。你主要靠一個出錯的數據找到了問題的源頭。
最後一類:OMG WTF(天啊,what's the f**k)。這類bug無以言表,足以動搖你所有常識。對此,我又愛又恨。最後的解決方案可以當成戰爭故事來講,不過,你的大腦在那時就應該被難以解釋的奇怪錯誤搞壞了。
在你剛開始程序員生涯的那段時間裡,所有事情都可能是這種OMG-WTF類型的。即便是那些開發老手,當他們轉換開發工具時,也是如此。不過,當你看著這類bug一次又一次地突然出現,它們變成了最好解決的垃圾。我覺得,我們本質上還是處在軟件開發的原始狀態。你可以想像一個機械工程師說出這種話嗎?“抱歉,建橋進度再次延後。自基架搭建之後,重力不斷變化,水泥加速衰變。——等等,當我戴上巫師面具之後,再來跟你說話。”定位和處理bug,沒有能夠遵從的程式。(作者援引Universal Troubleshooting Process(中譯版))
別慌!(深呼吸)
你正面對一個很糟糕的OMG-WTF類bug。可能你剛到職不久,你想給自己未來的同事一個好印象;也可能你正是這個項目的主設計,但整個項目都不對勁。
首先,不要慌亂,沒那麼糟。來個深呼吸。你可能過於激動,覺得成敗在此一舉。服務器宕機,公司的錢財每分鐘都在損失,這是個對你的職業生涯大有影響的事情。一旦出事,你將不得不花一個月時間仔細回想這個bug,而且,所有人都認為你不行。
這都可能成真。
不過,你還有一個明智清醒、善於分析的頭腦。你需要消除情緒的影響,集中注意力在當前問題上。對大部分人來說,一個bug無足輕重。這是自我感覺的問題。我處理的bug,在產品發布的時候又發生了,然後出了補丁方才解決。即便如此,地球也不會爆炸。(譯者注:作者舉了個自己身上發生的、處理的bug事例來說明情況。在Mac上拖放文件圖標到App上就會打開該App,作者處理的bug可能就是這個操作的一個bug。但為避免讀者費解,此處略去。)
你知道的一切都錯了
像Macintosh這樣一個革命性的系統,程序與其它系統不同。例如,你的程序以用戶行為來驅動(比如鼠標點擊、鍵盤輸入等),這些行為不可預測,不像其它系統以特定次序執行。你會發現,你不少編寫程序的概念在此無法適用。正因為如此,再考慮到Inside Macinosh此書的厚度,讀一下Road Map(本手冊的後半部),可以幫你找到方向,認清下一步。
(上文為援引段落,引自Inside Macintosh一書Phone Book初版,歷史可謂久遠..)
現在你冷靜下來了。你明白到你所知道的一切都錯了。如果你的所知皆為正確,你就不該處理一個bug,或者,至少它們會以“五分鐘修理類”的表現形式出現。因此,應該是你對系統的認識有錯誤。如果你認識到這點,你就會回到“五分鐘修理類”這一類的處理進程中。這就是最關鍵的一點——在我心理狀態中,這就是開始調試的起點了。消除情緒,開始收集數據。
我調試的主要方式就是處理程序區,將“錯誤的,不正確的、功能不全的”代碼塊轉變為“應該就是這樣的”代碼塊。按這種方式行進到某處,你會發現無法將此處代碼轉為“能工作的”代碼塊,這裡就是可能發現bug的地方。(譯者注:作者在這裡說的,應該是對代碼逐步排除。分析可疑的代碼,一點點確認無誤,直到發現問題所在的代碼)
解決問題的思路
一個程序大致分為三部分。整合代碼的類;工具集(即開發工具提供的功能,可能是Apple的Cocoa或CocoaTouch,或是Rail,或是.Net);除此之外,就是系統(代碼和工具集運行的平台,包括OS X, iOS, Windows RT, 或是Linux)。你的程序就由這三層之間的交互行為構成。
所謂的思路,就是收集各部分的數據,理解它們是如何交互的。如果我對系統不熟,就談不上有什麼解決思路。如果我比較了解,解決思路就比較多了。
懷疑所有事情
正因為這是一個我不了解原因的bug,所以一定是我的常識有不正確之處。因此,要懷疑所有可能,試著去深入了解。
好吧,也許不必懷疑所有事情,但如果你有個bug(你從前卻沒有見過),這一定是你的常識在某處出了問題。
要懷疑,但也別盲目懷疑。一個bug的入手,不必從Unix的基本原理開始。但是,如果需要修正思路,就應該在開始分析的時候作出修正。
我有一個出錯表,先集中精力於在這個列表的頂層,假定其下都是正確的:(譯者注:即是依次排除)
新代碼
舊代碼
library代碼(像AFNetworking這種庫)
Cocoa
編譯器
新代碼最可疑——它不像舊代碼那樣身經百戰,即是說,它還有小缺陷未被查明。當系統從能工作到不能工作,新代碼總是要第一時間查明 (和糾錯)。
舊代碼也要多加留心。新數據流經系統,可能引發軟件潛在的缺陷。比如,從web service中下載了數據庫,其中的數據含有非法字符;又比如,格式不正確的網絡爬蟲腳本發出了錯誤的HTTP頭標記,這使你的解析代碼崩潰了。
流行的第三方庫代碼很少需要檢查,因為一般情況下,它們都經過廣泛的測試,包括正式的單元測試,還有不可見的、大量的項目用戶測試。我總是先挑自己代碼的錯,但只要認為有可能,我也不介意單步中斷到這種庫中。沒那麼流行的庫可以歸類為“我的代碼”,因為它們可能沒經過那麼多測試和執行。
基本工具集,像Cocoa(Touch),一般都無責通過。我總是假定,出錯的代碼是我的代碼,工具集則工作正確(至少如文檔所列)。通常來說,要是你認為像NSArray或NSDictionary這種基礎功能都有bug,你就錯了。我一般找更高級別的對象糾錯,像MPMusicPlayerController,要是它有錯,多半你是正確的。
Apple有大量的工程師和QA團隊解決問題,新OS版本前的試行系統,周期也很長。如果證據指向工具集,我自然也想去一探究竟。你可以看 Leveling Up這篇文章來分離庫,看看是否問題出自於此。
最後,除非別無可選,我從不考慮編譯器的問題。LLVM團隊相當出色,他們的產品極其高效。有時,你的確能找到它的問題,如麥克 亞希所言(見此文章analysis of a compiler bug),但一般都只是極端個例。
下一篇文章(已翻譯為中文版),我要說說何時處理bug。
(本文為CocoaChina組織翻譯,本譯文權利歸譯者所有,未經允許禁止轉載。)