用邏輯改變世界

軟體工程師是如何Debug的

我不喜歡幫人Debug…畢竟程式從頭到尾都不是我寫的,甚至我根本不是專案的成員。我總是會先問:「所以你想做的事情是什麼?」但不見得會得到答案,彷彿我不需要知道正確的、他們期待的結果,就能夠幫他們找出問題。
我想我需要的是一份通靈的本領…

過幾年我終於想通了,應該寫一篇教人Debug的文章。也許不盡完美,至少可以提供一些方向。拜託,自己的bug自己修好嗎?

尋找產生錯誤的第一段程式碼

1、逐行/區域測試是基本
找bug最基本的就是要先知道產生錯誤的程式位在那一行/區域。有些錯誤是電腦就會告訴我們的,有些則是結果不如預期。我以前寫JS,只要錯一行全部的程式就會掛掉,而且電腦不會告訴你錯在那裡。假設可能出錯的程式有100行,我會先留20行執行看看,再留40行…依此類推,先抓出一個大概可能錯誤的區域,再逐步縮小範圍。剩下的行數不多,就是下斷點,或是逐行檢查執行結果。

2、重新堆疊
有的時候程式太複雜、包了很多層物件,可能沒辦法像腳本語言那樣用刪去法來找bug,所以另一種方式就是重新堆疊。我們至少會知道出錯的功能是什麼,所以可以開一個全新的專案,單獨把這項功能實作上去。而且在實作的過程中,要一直不斷地檢查中間過程是否正確。
就像堆積木一樣,慢慢堆慢慢檢查,通常我們會找到一塊積木堆上去的時候,程式就發生預期外的錯誤。此時就可以先鎖定這個錯誤的範圍。

判斷錯誤類型並加以修正

有些程式的錯誤,就算知道它在那裡,也沒辦法將它修好。通常會需要Call Help也是這類型的bug。這種bug的產生,通常是A和B單獨執行沒問題,但A+B放在一起跑就是有問題。而我們在上一步中只能找到A,卻不知道B--另一個導致它錯誤的地方在那裡。
還有一種是人類盲點的問題,即使知道錯在某一行,但我們就是「看」不到錯誤。例如我在學生時期,曾經有位同學在程式結尾的後面多加了一個全形的空白。當時是DOS環境沒有滑鼠,所以很難發現在一片空白的地方有一個程式無法接受的全形空白在那裡。如果是現在,只要全部選取就會發現多了不該出現的東西。
簡單地說,Debug之所以困難,就是即使錯誤擺在眼前,也不代表能修復它。

1、版本型錯誤-判斷是自己的bug還是官方的bug
這年頭變化速度很快,所以即使是官方提供的功能,可能也有bug。這在某些領域特別明顯,例如手機上的iOS/Android系統、瀏覽器Chrome等等。初步的判斷方式有二種:一、原本正常的程式在沒有任何修改的情況下壞掉了,並且相關的環境(系統、外掛)有更新。二、這是一支非常簡單、基礎的程式,理論上不太可能產生錯誤。
這種類型的錯誤通常會在第一階段找到標準物件、函式的名稱,因為它是官方的錯誤,搭配版號下去搜尋就可能找到答案。
不過,有的時候是不同版本執行結果不同。所以最簡單確認的方式,就是直接換個版本試試,而且變更的版號最好不要太接近,有時候官方的bug也會延續很久。
如果是硬體廠商的bug那又更難找了。這時候可能要從官方的Sample Code開始,逐步將程式堆疊上去,找到發生錯誤的關鍵地方後再拿去問廠商。

2、資源型錯誤-當環境改變程式就錯了
如果你的程式原本跑起來都沒問題,但過了一段時間,而且是不定期的時間後,就會產生錯誤;此時只要重新執行,或重新開機又回復正常--這種就是資源型的錯誤,最常見的是記憶體佔用的問題。
這類型的錯誤要能確保資源的控管,例如能不能判斷目前資源的狀況來決定要不要繼續執行?結束執行的資源是否有確實釋放?當重要程式執行時,能不能強制釋放資源?
有時候這些都是無法控制的,只能用一些額外的邏輯繞道。例如當前一個程式無法立即結束時,下一個程式就要晚一點執行;定時重新啟動程式以回收記憶體等等。
在與硬體相關的控制中也很容易遇到這類型的問題,例如當五支機器手臂都在工作,第六個命令當然無法執行。

3、結構型錯誤-比較差異中的小問題
我常常被問的一種問題是,為什麼相同程式在A地可以,B地不行,也就是有一份正常、但另一份有錯誤。這種問題代表對方大致知道錯誤的區域,但是對於關鍵錯誤的因素是不清楚的。
通常我會請對方確認二邊的程式100%一樣,甚至直接複製正確的程式碼一字不改到異常的地方,找到最後也真的有所不同。不過,這年頭理論上不該有「相同程式出現在二個地方」的情形,如果寫成函式或物件,通常就能避免這種情況。
當然,還有一種情形是邏輯相同但程式細節不同。所以是做法一樣,但有一邊出現錯誤。如果有一份正確的程式,最後的辦法就是把錯誤程式打掉重練,並且按正確的程式邏輯一步一步加上去,和重新堆疊一樣。如果真的是這種差異性的錯誤,通常照著正確的程式/步驟重做,就會有正確的結果了。
記得學生時期學VB,老師開了一個課堂練習,比賽看誰先做完。我按照步驟拉了一些程式,結果出現錯誤,老師也檢查不出原因,砍掉重拉一次也就正確了。這種程式很多是電腦幫你寫的,我確定自己的步驟都一樣,但結果也有所不同。所以也是有發生這種神秘情況的時候。
這種結構型問題通常可以透過單元測試找出來,並且只要有正確的程式/步驟可以對照(例如Sample Code),就能進行初步的比較,檢查錯誤與正確的差異在那裡。如果還是找不到,就用大絕招重新堆疊的方法修正。

4、邏輯型錯誤-無藥可救的死胡同
大部份的人都不會認為自己邏輯有問題,所以當程式遇到邏輯錯誤時,往往會很難察覺--對新手來說更是如此。所以工程師要有客觀的精神,執行結果有錯就是有問題,不要明明問題擺在眼前,還死鴨子嘴硬:「我沒有錯」。
我在做系統分析的工作,有時候會規劃大部份的邏輯。常常有一些沒考慮到的細節,工程師就會在開工之後跑來找我討論。邏輯有錯誤或疏失是很正常的,重點是看到問題就要想辦法解決,堅持自己的邏輯對事情並沒有幫助。
解決邏輯錯誤基本上就是要思考新的邏輯。可用的部份還是能保留,有問題就全部砍掉。對新手來說要想新邏輯可能有點困難,此時適當求助還是可以接受的。

我在協助處理UI特效問題的時候,最常遇到的就是「憑感覺設參數」,如果參數是憑感覺設定的,那麼在某些情況下就會有非預期的錯誤。事實上UI特效是很吃數學的一個項目,所有的參數、公式都必須要合理計算,才不會有非預期的畫面產生。

5、衝突型錯誤-跳出框架思考
當你確認結構邏輯都沒有問題,卻始終找不到bug在那裡時,可能就是因為bug不在你的眼前。這個時候要去檢查每一個相關的外部參數/物件,它們可能才是導致錯誤的主因。一般來說,良好模組化的黑盒子比較不容易產生這類型的錯誤,因為黑盒子的特色就是,只要輸入正確,就會有對應的輸出。不過有些東西畢竟沒辦法用黑盒子做,還是會產生這種情形。
我最常遇到的是二種動畫互相衝突的情況,單邊執行都是沒有問題的,放在一起就會出錯。此時有幾個觀察的重點,包括變數的內容是否有異常,或是直接觀察執行的結果,通常多少可以找到一些蛛絲馬跡。面對這種bug最重要的是不要一直在原地打轉,思考外部錯誤的可能性,才有機會找到真正的問題。

我知道這四種面向看完,也不代表工程師就能判斷是那一種。像我遇過一種工程師,千錯萬錯都是別人的錯,那麼到底bug在那裡對他來說也不重要。另外,新手工程師能夠自行修正版本型與結構型就已經不錯了,資源型和邏輯型則是跟領域知識有關。通常我在判斷是衝突型錯誤的時候,會直接幫忙對方修bug;資源型會一起想解決的辦法;邏輯型的錯誤則是會引導對方思考邏輯。如果是結構型錯誤,我就會很懶得花時間幫忙處理--答案都在那裡了還抄不好,我有什麼辦法呢?

最後想要提醒大家,不要在過度疲倦的時候Debug。以前學生做專題的時候程式寫很兇,暑假起床以後,除了吃飯廁所就是寫程式,從早上寫到晚上半夜。有次一個bug找一、二個小時找不到,撐不住只好先休息了,沒想到隔天早上起床電腦打開,10分鐘就解決了。休息是為了走更長遠的路。如果因為Debug感到頭頂烏煙瘴氣烏雲密布,那麼是時候該休息一下了。
這也是為什麼我反對常態加班的原因。不要以為知識工作者沒有體力活,就可以少少的休息,大腦大量地消耗腦力,沒有休息運作的效率就會變差。想像一下你一直在算數學,多久會開始感到疲倦?每天算8小時的數學不會累嗎?加班下去做的東西,品質肯定就是比較差,還要花比較多的薪水,我認為這CP值很低。

暸解四種類型的錯誤,並不代表將來遇到bug就有解了,畢竟不同程式領域的狀況還是差很多。但是如果能判斷每次的bug是那一種類型,找bug會更有方向、更有機會解決問題。

自己的英文自己救!大人學口說英文課程心得