除錯與工具

症狀與根本成因之別(symptom versus root cause)

如果你車上的警示燈亮了,那盞燈不是問題——它是在告訴你別處有個問題。把燈當成問題(用膠帶把它貼起來)會讓真正的故障去造成更多損害。臭蟲也是一樣。症狀是你觀察到的出錯之處——當機、區段錯誤、錯誤的輸出、卡住。根本成因則是那個真正的錯誤,它在好幾步之前就為那個症狀佈置好了條件。把兩者搞混,是除錯出錯最常見的單一方式。

具體來說,在系統程式設計裡症狀與成因之間的距離往往很大、而且症狀往往有誤導性。區段錯誤是經典的症狀:程式試圖使用一個它不被允許的位址、作業系統把它殺了——但當機發生在那個壞指標最終被解參考的地方,而根本成因卻在那個指標被設錯的任何地方,也許在很久以前的另一個函式裡。更糟的是,未定義行為可能完全不當機就破壞記憶體,所以症狀可能出現在離成因任意遠的地方、或要到很久以後某段不相干的程式碼絆到那個損害時才出現。一次當機、一個錯誤的數字、一次卡住——這些是後果;臭蟲是那行讓它們無可避免的程式碼。要有的紀律是:從症狀往回走(讀呼叫堆疊追蹤、檢查是誰傳了那個壞值、用監看點逮住那次破壞),直到你到達現實第一次偏離意圖的地方——那就是根本成因。

為何重要:修症狀而不修它的成因,正是臭蟲捲土重來的方式。在當機處加一個 null 檢查止住了那次當機、卻讓真正的缺陷——那個產生 null 的東西——自由地在別處造成新的失敗;臭蟲只是搬了家。一個真正的修正處理的是程式第一次做錯事的地方,在那之後症狀就無法再因為那個原因復發。誠實的紀律:「它不再當了」與「我找到並修好了臭蟲」不是同一回事;要求一個解釋,說明為什麼原本的問題再也不會發生,並對任何你無法解釋的修正抱持懷疑——它很可能掩蓋了一個症狀、而非移除了一個成因。

一個程式在 name 為 NULL 時於 print_name() 發生區段錯誤。修補 print_name 去略過 NULL 止住了當機——但真正的臭蟲是 load_user() 在記錄不存在時回傳了 NULL、而呼叫者從未檢查。修好 load_user() 裡的檢查,那個症狀(以及它未來的任何變體)就無法再復發。

當機是症狀;load_user() 那個未經檢查的 NULL 才是根本成因。

區段錯誤、錯誤的結果或卡住是症狀、不是臭蟲——而且未定義行為可能無聲地破壞,使症狀出現在很遠的地方、很久以後、或根本不出現。在一個你無法解釋的改動之後「不再當了」,通常意味著你掩蓋了一個症狀、而非移除了它的成因。

又稱
proximate cause vs root causewhere it crashes vs why表象與病因