《電子技術(shù)應(yīng)用》
您所在的位置:首頁(yè) > 其他 > 業(yè)界動(dòng)態(tài) > 云風(fēng)的 BLOG:斷點(diǎn)單步跟蹤是一種低效的調(diào)試方法

云風(fēng)的 BLOG:斷點(diǎn)單步跟蹤是一種低效的調(diào)試方法

2019-10-30
來(lái)源:云風(fēng)的 BLOG


  斷點(diǎn)單步跟蹤交互式調(diào)試器是軟件開發(fā)史上的一項(xiàng)重大發(fā)明,。但我認(rèn)為,,它和圖形交互界面一樣,,都是用犧牲效率來(lái)降低學(xué)習(xí)門檻,。本質(zhì)上是一種極其低效的調(diào)試方法。

  我在年少的時(shí)候( 2005 年以前的十多年開發(fā)經(jīng)歷)都極度依賴這類調(diào)試器,,從 Turbo C 到 Visual C++ ,,各個(gè)版本都仔細(xì)用過(guò)。任何工具用上十年后熟能生巧是很自然的事。我認(rèn)為自己已經(jīng)可以隨心所欲用這類工具高效的定位出 bug 了,。但在 2005 年之后轉(zhuǎn)向跨平臺(tái)開發(fā)后,,或許是因?yàn)橐婚_始沒(méi)能找到 Linux 平臺(tái)上合適的圖形工具,我有了一些時(shí)間反思調(diào)試方法的問(wèn)題,。GDB 固然強(qiáng)大,但當(dāng)時(shí)的圖形交互外殼并不像今天的版本這么完善,。當(dāng)時(shí)比較主流的 insight ddd 都有些小問(wèn)題,,用起來(lái)不是十分順手。我開始轉(zhuǎn)換自己平時(shí)做開發(fā)的方式,。除了盡量提高自己的代碼質(zhì)量:寫簡(jiǎn)潔的,、明顯沒(méi)有問(wèn)題的代碼之外,多采用不斷的代碼復(fù)核(Code Review),,有意識(shí)地增加日志輸出,,來(lái)定位 Bug 。

  后來(lái)開發(fā)重心從客戶端圖形開發(fā)逐步轉(zhuǎn)向服務(wù)器,,更加顯露出用調(diào)試器中斷程序運(yùn)行的劣勢(shì)來(lái),。對(duì)于 C/S 結(jié)構(gòu)的軟件,中斷一邊的代碼運(yùn)行,,用人的交互頻率單步跟蹤運(yùn)行,,而另一邊是以機(jī)器的交互頻率運(yùn)作,像讓軟件運(yùn)行流程保持正常是非常困難的,。

  這些年的工作中又慢慢加入一些 Windows 下的開發(fā)工作,。我發(fā)現(xiàn)經(jīng)過(guò)了再一個(gè)十年的訓(xùn)練,即使偶爾用上交互式調(diào)試器,,也體會(huì)不到什么優(yōu)勢(shì)了,。往往手指按在跟蹤調(diào)試按鍵上機(jī)械的操作,腦子里想的卻不是眼前看到的屏幕上的代碼,。往往都沒(méi)執(zhí)行到觸發(fā) Bug 的位置,,已經(jīng)恍然大悟發(fā)現(xiàn)寫錯(cuò)的地方了。這種事情多了,,自然會(huì)對(duì)過(guò)去的方法質(zhì)疑,,是什么導(dǎo)致了調(diào)試器的低效。

  有時(shí)和人聊天,,談及該怎么定位 Bug ,。我總是半開玩笑的說(shuō),你就打開編輯器,,盯著代碼看啊,。盯久了,Bug 自然就高亮出來(lái)了。這固然是玩笑,,但我的理念中,,一切調(diào)試方法都比不上 Code Review 。無(wú)論是自己寫的代碼,,還是半途介入的別人的代碼,。第一要?jiǎng)?wù)就是要先理解程序的總體結(jié)構(gòu)。

  程序總是由一段段順序執(zhí)行的小片代碼段輔以分支結(jié)構(gòu)構(gòu)成,。順序執(zhí)行的代碼段是很穩(wěn)定的,,它的代碼段入口的輸入狀態(tài)決定了輸出結(jié)果。我們關(guān)心的是輸入狀態(tài)是什么,,多半可以跳過(guò)過(guò)程,,直接看結(jié)果。因?yàn)檫@樣一段代碼無(wú)論多長(zhǎng),,都有唯一的執(zhí)行流程,。而分支結(jié)構(gòu)的存在會(huì)讓執(zhí)行流依據(jù)不同的中間狀態(tài)做不同的數(shù)據(jù)處理??紤]代碼的正確性時(shí),,所有的分支點(diǎn)都需要考慮。是什么條件導(dǎo)致代碼會(huì)走向這條分支,,什么條件導(dǎo)致代碼走向那條分支,。可以說(shuō)分支的多少?zèng)Q定了代碼的復(fù)雜度?,F(xiàn)在比較主流的衡量代碼復(fù)雜度的方法 McCabe 代碼復(fù)雜度大致就是這樣,。

  一個(gè)軟件的整體 McCabe 復(fù)雜度一定遠(yuǎn)超人腦可以一次處理的極限。但通常我們可以對(duì)軟件進(jìn)行模塊劃分,,高內(nèi)聚低耦合的結(jié)構(gòu)能減少軟件復(fù)雜度,。一個(gè)高內(nèi)聚的模塊,可以和外部隔離,,方便我們聚焦到模塊內(nèi)部來(lái)分析,。當(dāng)焦點(diǎn)代碼的規(guī)模足夠小的時(shí)候,包含一切分支結(jié)構(gòu)的所有流程就能一次性的被大腦處理了,。對(duì)于用調(diào)試器輔助觀察程序的執(zhí)行流程來(lái)說(shuō),,每次用真實(shí)的輸入數(shù)據(jù)驅(qū)動(dòng)的執(zhí)行過(guò)程一定是沿唯一的路徑運(yùn)行的。為了定位 Bug ,,我們需要設(shè)計(jì)出可以觸發(fā) Bug 的輸入狀態(tài),。對(duì)于一個(gè)局部模塊來(lái)說(shuō),這并不總是容易的事,。但靠大腦分析一個(gè)模塊則不同,,在 McCabe 復(fù)雜度不高時(shí),,幾乎是可以并行的處理所有的執(zhí)行路徑的。也就是說(shuō),,你在掃描代碼的同時(shí),,大腦其實(shí)是在同時(shí)分析所有可能的情況,同時(shí)還能對(duì)不太重要的分支做剪枝,。當(dāng)然,,和所有技能一樣,分析速度和能分析的寬度(復(fù)雜度)以及剪枝的正確性是需要反復(fù)訓(xùn)練才能拓展的,。過(guò)于依賴交互式調(diào)試工具會(huì)影響這種訓(xùn)練,,大腦受工具的影響,會(huì)更關(guān)心眼下的狀態(tài):目前運(yùn)行到哪里了,,(為了提高調(diào)試效率)下個(gè)斷點(diǎn)設(shè)到哪里去,現(xiàn)在這組變量的值是什么…… 而不太關(guān)心:如果輸入是另外一種情況,,程序?qū)⒃趺催\(yùn)行,。因?yàn)楣ぞ咭呀?jīng)把這些沒(méi)有發(fā)生的過(guò)程剪掉了,等著你設(shè)計(jì)另一組輸入下次再展示給你,。

  交互調(diào)試工具通常缺乏回溯能力,,也就是它們通常反應(yīng)當(dāng)下的狀態(tài),而不記錄過(guò)去的,。這有些可以通過(guò)改進(jìn)工具來(lái)完善,,有些則不能。一個(gè)常見(jiàn)的場(chǎng)景是,,你定下了下一個(gè)斷點(diǎn)的位置,,當(dāng)調(diào)試器停下來(lái)的時(shí)候,發(fā)現(xiàn)狀態(tài)異常,,只能確定問(wèn)題出在上次斷點(diǎn)到當(dāng)前的位置之間,,但想回溯到底發(fā)生了什么,某個(gè)中間狀態(tài)是什么,,工具卻無(wú)能為力,。而靠大腦推演程序的運(yùn)行過(guò)程的話,一切都是靜態(tài)圖譜,,回溯和前行并無(wú)太大區(qū)別,,只是聚焦到時(shí)間軸上某個(gè)位置而已。這就是為什么受過(guò)良好訓(xùn)練的程序員可以一眼看出 Bug 在哪里,,而調(diào)試器運(yùn)用高手卻需要反復(fù)運(yùn)行兩三次才能找到 Bug 的緣故,。

  在大腦中正確運(yùn)行程序當(dāng)然需要足夠的訓(xùn)練,比訓(xùn)練使用調(diào)試器難的多,,但卻是值得的,。不知道其它同學(xué)有沒(méi)有類似經(jīng)歷:我在中學(xué)時(shí)代參加信息學(xué)競(jìng)賽的時(shí)候,,考卷并不全是編程題,尤其是初賽階段,,一般是紙面考卷,,有很多題目都是給出程序和輸入,寫出輸出結(jié)果,。感謝這段經(jīng)歷,,我不得不在初學(xué)編程的時(shí)候就進(jìn)行這類訓(xùn)練。初中的時(shí)候,,每天可以摸到真機(jī)的時(shí)間是按小時(shí)計(jì)的,,大部分時(shí)間還是在傳統(tǒng)的學(xué)業(yè)上。為了編寫自己玩的游戲程序,,我只能在上課的時(shí)候偷偷的在本子上手寫代碼,。寫完了后如果沒(méi)有下課,我會(huì)在大腦中模擬運(yùn)行一下,,看看有沒(méi)有 bug ,,能在上機(jī)前改過(guò)來(lái),就可以更有效的利用每天有限的上機(jī)時(shí)間,。這些經(jīng)歷讓我覺(jué)得讀代碼其實(shí)沒(méi)那么枯燥,,是提高效率的一種方法。

  用 Code Review 作為主要的定位 Bug 的手段,,可以促進(jìn)你寫出復(fù)雜度更?。ǜ蝗菀壮鲥e(cuò))的程序。因?yàn)橹酪阅隳壳暗哪芰Υ竽X能一次處理的復(fù)雜極限在哪,。在減少分支方面,,我看過(guò) Linus 的一個(gè)訪談節(jié)目。他談及代碼品位,,舉了一個(gè)很小的例子:一段對(duì)鏈表的處理程序,。鏈表的頭部通常和中間的結(jié)構(gòu)不同,頭部之外的節(jié)點(diǎn)都有一個(gè) next 指針引用下一個(gè)節(jié)點(diǎn),,而頭節(jié)點(diǎn)是個(gè)例外,,是由不同的數(shù)據(jù)結(jié)構(gòu)引用的。再 Linus 列出的反面例子中,,代碼判斷了頭指針是否為空,;而在正面例子中,next 指針是用一個(gè)指針引用變量實(shí)現(xiàn)的,,對(duì)于頭節(jié)點(diǎn),,它引用在不同的數(shù)據(jù)結(jié)構(gòu)變量上,這樣就回避了多一次的例外(對(duì)于頭節(jié)點(diǎn))判斷,。代碼可以一致的處理,。在那個(gè)只有 5,6 行代碼的小片段中,,似乎判斷語(yǔ)義非常清晰,多一次判斷微不足道,,但 Linus 強(qiáng)調(diào)這是品位選擇的問(wèn)題,。我認(rèn)為,這其實(shí)就是將減少代碼復(fù)雜度提升到書寫代碼的本能中,。

  對(duì)于中途介入的他人的項(xiàng)目,,你無(wú)法控制代碼的質(zhì)量。但長(zhǎng)期的 Code Review 訓(xùn)練可以幫助你快速切分軟件的模塊,。通常,,你需要運(yùn)用你對(duì)相關(guān)領(lǐng)域的知識(shí),和同類軟件通常的設(shè)計(jì)模式,,預(yù)設(shè)軟件可能的模塊劃分方式,。這個(gè)過(guò)程需要對(duì)領(lǐng)域的理解,不應(yīng)過(guò)度陷入代碼實(shí)現(xiàn)細(xì)節(jié),。一上手就開調(diào)試器先跑跑軟件的大致運(yùn)行流程是我不太推薦的方法,。這樣視野太狹窄了,花了不少時(shí)間只觀察到了局部,。其實(shí)不必執(zhí)著于從頂向下還是從下置上,??梢韵却笾驴纯丛创a的文件結(jié)構(gòu)做個(gè)模塊劃分猜測(cè),,然后隨便挑選一個(gè)模塊,找到關(guān)聯(lián)的部分再順藤摸瓜,。對(duì)于需要構(gòu)建的項(xiàng)目,,摸清程序脈絡(luò)的時(shí)間甚至可以在第一次等待編譯構(gòu)建的時(shí)間同步完成,而不需要等待構(gòu)建完畢在一步步跟蹤運(yùn)行,,甚至不需要下載代碼到本地,,github 這種友好的 web 界面已經(jīng)可以舒適的在瀏覽器里閱讀了,有個(gè) ipad 就可以舒服的躺在床上進(jìn)行,。

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

  那么,,光靠大腦 Code Review 是不是就夠了呢,?如果自身能力無(wú)限提高,我認(rèn)為有可能,。通過(guò)積累經(jīng)驗(yàn),,我這些年能直接分度閱讀的代碼復(fù)雜程度明顯超過(guò)往年。但總有人力所不及的時(shí)候,。這時(shí)候最好的方法是加入日志輸出作為輔助手段,。

  試想我們?cè)谟媒换フ{(diào)試工具時(shí),其實(shí)是想知道些什么,?無(wú)非是程序的運(yùn)行路徑,,是不是真的走到了這里,以及程序運(yùn)行到這里的時(shí)候,,變量的狀態(tài)是怎樣的,,有沒(méi)有異常情況。日志輸出其實(shí)在做同樣的工作,。關(guān)鍵路徑上輸出一行日志,,可以表達(dá)程序的運(yùn)行路徑。把重要的變量輸出在日志里,,可以查詢當(dāng)時(shí)的程序運(yùn)行狀態(tài),。怎樣有效的輸出日志自然也是需要訓(xùn)練的技能。不要過(guò)于擔(dān)心日志輸出對(duì)性能的影響,,最終軟件有 20% 上下的性能波動(dòng)對(duì)于軟件的可維護(hù)性來(lái)說(shuō)是微不足道的,。

  和外掛的調(diào)試工具相比,日志具備良好的回溯查詢能力,。作為 Code Review 的一個(gè)輔助,,我們大腦其實(shí)需要的只是對(duì)判斷的一個(gè)修正:確認(rèn)程序是否是沿著腦中模擬的路線在行進(jìn),內(nèi)部狀態(tài)是否一致正常,。和調(diào)試工具不同,,日志不會(huì)打斷運(yùn)行過(guò)程,,對(duì)多個(gè)程序并行運(yùn)行的軟件,例如 C/S 結(jié)構(gòu)的系統(tǒng)就更為重要了,。

  其實(shí)保留狀態(tài)信息在交互調(diào)試工具中也是非常重要的技巧,。我相信很多人和我一樣,在調(diào)試程序時(shí)有時(shí)會(huì)增加一些臨時(shí)的全局變量,,把一些中間狀態(tài)寫到這些變量中,。在交互調(diào)試過(guò)程中偶爾需要去查看這些狀態(tài)值。這種臨時(shí)狀態(tài)暫存變量,,其實(shí)也充當(dāng)了日志的功能,。

  文本日志的好處是可以利用文本處理工具做信息二次提取。grep awk vim python lua 都是分析日志的好手段,。如果日志巨大,,且存在在遠(yuǎn)程機(jī)器上,你很可能找不到更有效快捷的手段,。很多時(shí)候,,不斷的重新運(yùn)行有 bug 的程序的代價(jià),是遠(yuǎn)超一次運(yùn)行得到詳細(xì)日志后再對(duì)日志做分析的,。

  那么,,學(xué)會(huì)使用交互調(diào)試工具重要嗎?我認(rèn)為依然重要,。偶爾用之,,也能起到奇效。尤其是程序崩潰的時(shí)候,,attach 到進(jìn)程中觀察崩潰時(shí)的狀態(tài),。操作系統(tǒng)大多也能 dump 出崩潰時(shí)的進(jìn)程狀態(tài)供事后分析,。這些都需要你會(huì)用調(diào)試工具,。但通過(guò)靜態(tài)狀態(tài)的草灰蛇線反推出崩潰前到底發(fā)生了些什么,卻也更需要對(duì)代碼本身有足夠的理解,。因?yàn)橛玫臅r(shí)機(jī)不多,,我認(rèn)為命令行的 gdb 就足夠用了。在分析損壞的棧幀,、編寫腳本分析一些復(fù)雜數(shù)據(jù)結(jié)構(gòu)方面,,命令行版本更具靈活性,應(yīng)用范圍也較廣,。而交互上的不便,,增加的學(xué)習(xí)成本,都是可以接受的,。


本站內(nèi)容除特別聲明的原創(chuàng)文章之外,,轉(zhuǎn)載內(nèi)容只為傳遞更多信息,,并不代表本網(wǎng)站贊同其觀點(diǎn)。轉(zhuǎn)載的所有的文章,、圖片,、音/視頻文件等資料的版權(quán)歸版權(quán)所有權(quán)人所有。本站采用的非本站原創(chuàng)文章及圖片等內(nèi)容無(wú)法一一聯(lián)系確認(rèn)版權(quán)者,。如涉及作品內(nèi)容,、版權(quán)和其它問(wèn)題,請(qǐng)及時(shí)通過(guò)電子郵件或電話通知我們,,以便迅速采取適當(dāng)措施,,避免給雙方造成不必要的經(jīng)濟(jì)損失。聯(lián)系電話:010-82306118,;郵箱:[email protected],。