除錯是一個反覆提出假設,接著驗證假設是否成立的過程。大多數的情況下,你會先基於測試者所回報重新複製問題的方法、問題的症狀,以及過去的經驗,猜想問題的可能原因,然後依據這個猜想收集資訊,藉以驗證你的假設是否正確。

除錯的過程類似科學實驗
這個過程和科學實驗有點接近。科學實驗是從觀察到的現象,建立起一個假設的模型,接著設計一些實驗來驗證自己的假設,倘若實驗結果成立,那麼便得到一些支持假設的證據,但若實驗結果推翻了假設,就必須綜合實驗結果,以及原先所觀察到的現象,修正甚至是重新再做假設。反覆以上的過程,直到能夠確定自己的假設與實驗結果相符。

而除錯亦同,依據重新複製問題的方法、問題的症狀,以及過去的經驗來建立起假設,接著進行實驗,也就是執行程式嘗試重新複製問題,而程式的執行路徑,以及變數之值便是實驗的結果。

依據實驗結果,你可以知道自己的假設是否成立。倘若所觀察到的執行路徑及變數之值並不能支持你的假設,那麼你就必須依據這次所觀察到的結果,結合之前所觀察到的所有現象,重新提出假設或修正假設,然後再進行除錯。

遇到難解的臭蟲,留意自認絕對沒有問題的地方
所以,有效而準確的假設能力,在除錯的過程中會十分有幫助,因為這樣的能力可以節省大量的除錯時間。簡單的臭蟲,甚至只要在重新複製問題的過程中一步一步檢視,便可以找到問題,即使不做任何的假設。但涉及層面以及程式碼過於廣泛的臭蟲,無法像大海撈針般地遍設中斷點於程式碼中,想要縮小範圍,就必須倚靠有效且準確的假設。

一旦假設完全偏離了方向,想要找到真正的問題根本是緣木求魚。對於難解的臭蟲,剛開始除錯時,你可能會連續做幾個最有可能的假設,但如果接連證明你的假設錯誤時,會逐漸陷入僵局。這有時已經不是技術層面的問題,而是心理層面的問題。對此,我有一些建議。

首先,你在假設的過程中,可能會將要除錯的對象程式碼,切分為幾個區塊,然後挑出最有可能出錯的幾個區域,接著在這幾個區塊的邊界進行檢查。例如,你覺得最有可能出錯的是幾個函式,所以你會接著在呼叫這幾個函式的之前、之後,檢查它們的前置條件(Precondition)及後置條件(Postcondition)是否符合預期。

倘若前置條件不正確,代表進入此函式之前便已發錯誤,那麼問題在之前便已發生。如果前置條件正確,但後置條件不正確,代表問題極有可能出在這個函式,接著便可以更進一步展開,遞迴式地套用同樣的方法,檢視此函式內部的運作,找出出錯的原因。

這是典型除錯的方法:在疑似有問題的區塊邊界進行檢查,找出問題究竟是在之前還是之後發生的,決定下次檢查的方向是往前或是往後,然後不斷地縮小檢查的範圍,最後逼近真正問題的所在。

利用這樣子的方法,姑且不論究竟需要花費多少的時間,只要懷疑的假設命中,那麼總是可以找到問題的原因。

但最悲慘的事,莫過於你所設想的幾個區塊,實驗之後,反應都很正常。很多除錯者在這邊會有一個難以跨越的心理障礙──不願假設自己認為絕對沒有問題的區塊會有問題。

許多除錯者會在潛意識裡,直接將某些程式區塊,從嫌疑者的名單中剔除,因為他們覺得這些區塊「絕對」不可能會有問題。所以,會反覆檢查他們覺得有問題的區塊,但永遠不考慮那些他們認為「絕對」不會有問題的區塊,這不僅耗費了大量無謂的時間,在檢查根本是對的東西,而且沒有機會碰觸真正有問題的地方。

當你將自己心目中覺得會有問題的區塊都逐一檢視,卻一無所獲的時候,要記得,問題很有可能是隱藏在你覺得絕對沒有問題的地方。

對於除錯,我最常對除錯者說的是:「既然你覺得這Bug如此不可思議,那麼造成這問題的原因,應該也是同樣的不可思議吧!」當臭蟲的存在,在除錯的過程中顯得越來越離奇時,請放下執著,檢查每一個你認為絕對不可能出錯的程式區塊吧!

除錯範圍大且找不到問題時,改採地毯式搜索
對於需要除錯範圍很大的程式碼,經歷過多次的假設仍然捕捉不到問題時,還有另一種方式──地毯式搜索。

在這種做法底下,完全不倚靠任何假設,因為除錯者對於問題的可能原因,已經沒有任何的靈感了,這時候,與其持續胡亂地亂槍打鳥,不如放空一切,把每一段程式碼出錯的可能性都視為相等,然後開始檢查。

要怎麼做地毯式的搜索呢?基本上,便是在程式碼的範圍中,劃出若干個點,在每一個點上,盡可能檢查程式的狀態以及流程,倘若在A點和B點之間出了錯,那麼便可以將範圍縮小到A與B之間,遞迴式地繼續上述這個流程。

不可能每一點的狀態都正確。倘若都沒錯,代表你所檢查的狀態(也就是變數)還不夠多,以致於你沒有找到出錯的狀態。當發生這種情況時,就必須考慮增加更多要觀察的狀態。

日誌檔是協助除錯的好工具
你不一定要使用除錯器,來一步步在中斷點上檢查程式的狀態,有時這種情況錯誤反而不會出現,例如一些競速條件(Race Condition)下才會發生的問題。另一個很有用的工具,是程式的日誌程式庫(Logging Library),你可以利用它來協助記錄程式運行中的各種事件,以及發生該事件時,程式的相關狀態。

所以,在除錯器裡所設的中斷點,以及要觀察的變數值,都可以轉換為在程式碼中增加日誌內容的動作,進而在執行程式的過程中記錄變化的過程。而在除錯時,便只需要檢視日誌中的記錄,即可得知整個過程中所發生的事件,以及發生這些事件時的程式狀態。

日誌的記錄是協助除錯的好工具,除了毋需測試者或除錯者人為介入控制程式的單步執行之外,它也適合用來解決難以重新複製問題的臭蟲。因為運作除錯器時,大多數情況會需要除錯者介入觀察,對於難以重新複製問題的臭蟲,得反覆執行程式非常多次,才能讓臭蟲重現一次。

倘若程式中已建立日誌的記錄,只需要讓系統一如往昔地運行,接著檢視日誌檔便能夠收集相關的資訊,做為除錯時的線索。

此外,更積極一點,日誌記錄不應該只是除錯時才添加進程式碼裡,應該是撰寫程式時就已經試著建立的基礎設施。也就是說,程式人應該主動地將日後可能會需要查閱的日誌,在撰寫程式碼的第一時間,便已經加到程式碼裡,讓你的程式碼保有自我記錄的機制。

而對於已經上線的系統,而且錯誤只有在上線時才會發生的情況,更不太可能臨時為了除錯,才去反覆加入日誌記錄。在這種情況下,平日就建立好的基礎設施更顯得重要。

除錯是程式設計者難以避免的活動。這個活動不是隨興所至、隨著靈感而進行的工作,你可以更有系統地進行這個工作。

專欄作者

熱門新聞

Advertisement