當前位置:
首頁 > 科技 > 雲風:斷點單步跟蹤是一種低效的調試方法

雲風:斷點單步跟蹤是一種低效的調試方法

作者|雲風

編輯|小智

斷點單步跟蹤的互動式調試器是軟體開發史上的一項重大發明。但我認為,它和圖形交互界面一樣,都是用犧牲效率來降低學習門檻。本質上是一種極其低效的調試方法。

我在年少的時候( 2005 年以前的十多年開發經歷)都極度依賴這類調試器,從 Turbo C 到 Visual C++ ,各個版本都仔細用過。任何工具用上十年後熟能生巧是很自然的事。我認為自己已經可以隨心所欲用這類工具高效的定位出 bug 了。但在 2005 年之後轉向跨平台開發後,或許是因為一開始沒能找到 Linux 平台上合適的圖形工具,我有了一些時間反思調試方法的問題。GDB 固然強大,但當時的圖形交互外殼並不像今天的版本這麼完善。當時比較主流的 insight ddd 都有些小問題,用起來不是十分順手。我開始轉換自己平時做開發的方式。除了盡量提高自己的代碼質量:寫簡潔的、明顯沒有問題的代碼之外,多採用不斷的代碼複核(Code Review),有意識地增加日誌輸出,來定位 Bug 。

後來開發重心從客戶端圖形開發逐步轉向伺服器,更加顯露出用調試器中斷程序運行的劣勢來。對於 C/S 結構的軟體,中斷一邊的代碼運行,用人的交互頻率單步跟蹤運行,而另一邊是以機器的交互頻率運作,像讓軟體運行流程保持正常是非常困難的。

這些年的工作中又慢慢加入一些 Windows 下的開發工作。我發現經過了再一個十年的訓練,即使偶爾用上互動式調試器,也體會不到什麼優勢了。往往手指按在跟蹤調試按鍵上機械的操作,腦子裡想的卻不是眼前看到的屏幕上的代碼。往往都沒執行到觸發 Bug 的位置,已經恍然大悟發現寫錯的地方了。這種事情多了,自然會對過去的方法質疑,是什麼導致了調試器的低效。

有時和人聊天,談及該怎麼定位 Bug 。我總是半開玩笑的說,你就打開編輯器,盯著代碼看啊。盯久了,Bug 自然就高亮出來了。這固然是玩笑,但我的理念中,一切調試方法都比不上 Code Review 。無論是自己寫的代碼,還是半途介入的別人的代碼。第一要務就是要先理解程序的總體結構。

程序總是由一段段順序執行的小片代碼段輔以分支結構構成。順序執行的代碼段是很穩定的,它的代碼段入口的輸入狀態決定了輸出結果。我們關心的是輸入狀態是什麼,多半可以跳過過程,直接看結果。因為這樣一段代碼無論多長,都有唯一的執行流程。而分支結構的存在會讓執行流依據不同的中間狀態做不同的數據處理。考慮代碼的正確性時,所有的分支點都需要考慮。是什麼條件導致代碼會走向這條分支,什麼條件導致代碼走向那條分支。可以說分支的多少決定了代碼的複雜度。現在比較主流的衡量代碼複雜度的方法 McCabe 代碼複雜度大致就是這樣。

一個軟體的整體 McCabe 複雜度一定遠超人腦可以一次處理的極限。但通常我們可以對軟體進行模塊劃分,高內聚低耦合的結構能減少軟體複雜度。一個高內聚的模塊,可以和外部隔離,方便我們聚焦到模塊內部來分析。當焦點代碼的規模足夠小的時候,包含一切分支結構的所有流程就能一次性的被大腦處理了。對於用調試器輔助觀察程序的執行流程來說,每次用真實的輸入數據驅動的執行過程一定是沿唯一的路徑運行的。為了定位 Bug ,我們需要設計出可以觸發 Bug 的輸入狀態。對於一個局部模塊來說,這並不總是容易的事。但靠大腦分析一個模塊則不同,在 McCabe 複雜度不高時,幾乎是可以並行的處理所有的執行路徑的。也就是說,你在掃描代碼的同時,大腦其實是在同時分析所有可能的情況,同時還能對不太重要的分支做剪枝。

當然,和所有技能一樣,分析速度和能分析的寬度(複雜度)以及剪枝的正確性是需要反覆訓練才能拓展的。過於依賴互動式調試工具會影響這種訓練,大腦受工具的影響,會更關心眼下的狀態:目前運行到哪裡了,(為了提高調試效率)下個斷點設到哪裡去,現在這組變數的值是什麼…… 而不太關心:如果輸入是另外一種情況,程序將怎麼運行。因為工具已經把這些沒有發生的過程剪掉了,等著你設計另一組輸入下次再展示給你。

交互調試工具通常缺乏回溯能力,也就是它們通常反應當下的狀態,而不記錄過去的。這有些可以通過改進工具來完善,有些則不能。一個常見的場景是,你定下了下一個斷點的位置,當調試器停下來的時候,發現狀態異常,只能確定問題出在上次斷點到當前的位置之間,但想回溯到底發生了什麼,某個中間狀態是什麼,工具卻無能為力。而靠大腦推演程序的運行過程的話,一切都是靜態圖譜,回溯和前行並無太大區別,只是聚焦到時間軸上某個位置而已。這就是為什麼受過良好訓練的程序員可以一眼看出 Bug 在哪裡,而調試器運用高手卻需要反覆運行兩三次才能找到 Bug 的緣故。

在大腦中正確運行程序當然需要足夠的訓練,比訓練使用調試器難的多,但卻是值得的。不知道其它同學有沒有類似經歷:我在中學時代參加信息學競賽的時候,考卷並不全是編程題,尤其是初賽階段,一般是紙面考卷,有很多題目都是給出程序和輸入,寫出輸出結果。感謝這段經歷,我不得不在初學編程的時候就進行這類訓練。初中的時候,每天可以摸到真機的時間是按小時計的,大部分時間還是在傳統的學業上。為了編寫自己玩的遊戲程序,我只能在上課的時候偷偷的在本子上手寫代碼。寫完了後如果沒有下課,我會在大腦中模擬運行一下,看看有沒有 bug ,能在上機前改過來,就可以更有效的利用每天有限的上機時間。這些經歷讓我覺得讀代碼其實沒那麼枯燥,是提高效率的一種方法。

用 Code Review 作為主要的定位 Bug 的手段,可以促進你寫出複雜度更小(更不容易出錯)的程序。因為知道以你目前的能力大腦能一次處理的複雜極限在哪。在減少分支方面,我看過 Linus 的一個訪談節目。他談及代碼品位,舉了一個很小的例子:一段對鏈表的處理程序。鏈表的頭部通常和中間的結構不同,頭部之外的節點都有一個 next 指針引用下一個節點,而頭節點是個例外,是由不同的數據結構引用的。在 Linus 列出的反面例子中,代碼判斷了頭指針是否為空;而在正面例子中,next 指針是用一個指針引用變數實現的,對於頭節點,它引用在不同的數據結構變數上,這樣就迴避了多一次的例外(對於頭節點)判斷。代碼可以一致的處理。在那個只有 5,6 行代碼的小片段中,似乎判斷語義非常清晰,多一次判斷微不足道,但 Linus 強調這是品位選擇的問題。我認為,這其實就是將減少代碼複雜度提升到書寫代碼的本能中。

對於中途介入的他人的項目,你無法控制代碼的質量。但長期的 Code Review 訓練可以幫助你快速切分軟體的模塊。通常,你需要運用你對相關領域的知識,和同類軟體通常的設計模式,預設軟體可能的模塊劃分方式。這個過程需要對領域的理解,不應過度陷入代碼實現細節。一上手就開調試器先跑跑軟體的大致運行流程是我不太推薦的方法。這樣視野太狹窄了,花了不少時間只觀察到了局部。其實不必執著於從頂向下還是從下置上。可以先大致看看源代碼的文件結構做個模塊劃分猜測,然後隨便挑選一個模塊,找到關聯的部分再順藤摸瓜。對於需要構建的項目,摸清程序脈絡的時間甚至可以在第一次等待編譯構建的時間同步完成,而不需要等待構建完畢在一步步跟蹤運行,甚至不需要下載代碼到本地,github 這種友好的 web 界面已經可以舒適的在瀏覽器里閱讀了,有個 ipad 就可以舒服的躺在床上進行。

我不太喜歡 C++ 的一個原因是:C++ 代碼從一個局部去閱讀,很難有唯一的解釋。它的代碼字面意思很可能對應有多種實際操作含義,確定性不足。函數名重載、操作符重載都是隱藏在局部代碼之外的。甚至你看到一個變數名,不去同時翻閱上下文及頭文件的話,都很難確定這是一個局部變數還是一個類成員變數(前者的影響範圍和後者大為不同,大腦在做分析的時候剪枝的策略完全不同);看到一個變數,原本以為是一個輸入值,直到看到最後,發現它還可以做輸出,回頭一看函數聲明,其實它是一個引用量。如果用到模板泛型就更可怕,連數據類型都不確定。只從局部代碼無法得知模板實例化之後那些關聯的操作到底做了些什麼。閱讀 C++ 項目往往需要在代碼間相互參考,增加了大腦太多的負擔。

那麼,光靠大腦 Code Review 是不是就夠了呢?如果自身能力無限提高,我認為有可能。通過積累經驗,我這些年能直接分度閱讀的代碼複雜程度明顯超過往年。但總有人力所不及的時候。這時候最好的方法是加入日誌輸出作為輔助手段。

試想我們在用交互調試工具時,其實是想知道些什麼?無非是程序的運行路徑,是不是真的走到了這裡,以及程序運行到這裡的時候,變數的狀態是怎樣的,有沒有異常情況。日誌輸出其實在做同樣的工作。關鍵路徑上輸出一行日誌,可以表達程序的運行路徑。把重要的變數輸出在日誌里,可以查詢當時的程序運行狀態。怎樣有效的輸出日誌自然也是需要訓練的技能。不要過於擔心日誌輸出對性能的影響,最終軟體有 20% 上下的性能波動對於軟體的可維護性來說是微不足道的。

和外掛的調試工具相比,日誌具備良好的回溯查詢能力。作為 Code Review 的一個輔助,我們大腦其實需要的只是對判斷的一個修正:確認程序是否是沿著腦中模擬的路線在行進,內部狀態是否一致正常。和調試工具不同,日誌不會打斷運行過程,對多個程序並行運行的軟體,例如 C/S 結構的系統就更為重要了。

其實保留狀態信息在交互調試工具中也是非常重要的技巧。我相信很多人和我一樣,在調試程序時有時會增加一些臨時的全局變數,把一些中間狀態寫到這些變數中。在交互調試過程中偶爾需要去查看這些狀態值。這種臨時狀態暫存變數,其實也充當了日誌的功能。

文本日誌的好處是可以利用文本處理工具做信息二次提取。grep awk vim python lua 都是分析日誌的好手段。如果日誌巨大,且存在在遠程機器上,你很可能找不到更有效快捷的手段。很多時候,不斷的重新運行有 bug 的程序的代價,是遠超一次運行得到詳細日誌後再對日誌做分析的。

那麼,學會使用交互調試工具重要嗎?我認為依然重要。偶爾用之,也能起到奇效。尤其是程序崩潰的時候,attach 到進程中觀察崩潰時的狀態。操作系統大多也能 dump 出崩潰時的進程狀態供事後分析。這些都需要你會用調試工具。但通過靜態狀態的草灰蛇線反推出崩潰前到底發生了些什麼,卻也更需要對代碼本身有足夠的理解。因為用的時機不多,我認為命令行的 gdb 就足夠用了。在分析損壞的棧幀、編寫腳本分析一些複雜數據結構方面,命令行版本更具靈活性,應用範圍也較廣。而交互上的不便,增加的學習成本,都是可以接受的。

https://blog.codingnow.com/2018/05/ineffective_debugger.html

今日薦文


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 InfoQ 的精彩文章:

如何在NLP領域快速學會第一個技能?| 在線研討會
分散式ExpressNet SDN在盛大雲平台的網路部署實踐

TAG:InfoQ |