除錯是每個程式設計師都逃不過的宿命!
程式除錯是一件非常考驗耐心的事情,因為除錯過程中經常會需要反覆的修改原始碼,重新編譯、重新部署、重新執行,這個過程通常是非常枯燥和繁瑣的。尤其對於大型專案,光是編譯可能需要幾十分鐘,甚至幾個小時,部署過程則可能更為複雜漫長!
那麼,有沒有一種更高效的除錯手段,可以避免反覆修改程式碼和編譯呢?
這個真的有!本文將介紹一種除錯技巧,可以一邊除錯,一邊修復Bug,能夠在不修改程式碼、不重新編譯的前提下修復BUG,並且驗證解決方案,大幅提高除錯效率!
先看下最終效果吧!
本文預期效果
如下圖,氣泡排序中,有三個常見的BUG:
圖中已經把三個BUG都標註了出來。編譯執行,結果如下:
GDB中執行時:
不管是正常方式執行,還是在GDB中執行,程式都異常終止,無法得到正常結果。
但是,利用本文介紹的除錯技巧,可以利用GDB給這個程式製作一個“熱補丁”,在不修改程式碼、不重新編譯的前提下,解決掉程式中的三個BUG,讓程式正常執行,並得到預期結果!
最終效果,如下圖所示:
所有的黑魔法,都在這個補丁檔案bubble.fix
中!
是不是很有趣呢?下面開始介紹!
關於GDB
我之前寫了幾篇文章,專門介紹GDB的一些非常實用卻鮮為人知的高階用法,感興趣的小夥伴可以去翻看下除錯系列專題文章。
GDB的基本用法,相信大家都很熟悉了,就不過多介紹了,直接講重點吧!
Breakpoint Command Lists
GDB支援在斷點觸發後,自動執行使用者預設的一組除錯命令。使用方法:
commands [bp_id...]
command-list
end
其中:
commands
是GDB內建關鍵字。bp_id
是i(info)
命令顯示出來的斷點ID,可以指定多個,也可以不指定。不指定時,預設只對最近一次設定的那個斷點有效。command-list
是使用者預設的一組命令,當bp_id
指定的斷點被觸發時,GDB會自動執行這些命令。- end表示結束。
簡單來說,就是當bp_id
所表示的斷點被觸發時,GDB會自動執行command-list
中所指定的命令。
這個功能適用於各種型別的斷點,如breakpoint、watchpoint、catchpoint
等。
適用場景舉例
利用GDB的breakpoint commands lists
這個特性可以做很多有趣的事情,本文僅列舉其中的幾個。
隨時隨地printf,不需修改程式碼和重新編譯
我之前寫過一篇文章,詳細介紹過GDB的動態列印(Dynamic Printf)功能,可以用dprintf
命令在程式碼的任意地方設定動態列印斷點,並自動進行格式化列印。相當於在不修改程式碼,不重新編譯的情況下,可以讓你隨意新增printf列印日誌資訊。
利用GDB的breakpoint commands lists
,可以實現一樣的功能,而且除了格式化列印之外,還可以做其它更多的操作,比如dump記憶體,dump暫存器等。
修改程式執行邏輯
在GDB中可以做很多有趣的事情,比如修改變數、修改暫存器、呼叫函式等。結合breakpoint command list
功能,可以在除錯的同時,修改程式執行邏輯,給程式打上"熱補丁"。從而可以在除錯過程中,快速修復Bug和驗證解決方案,避免重新修改程式碼和重新編譯,大大提高程式除錯的效率!
這也是本文重點講解的場景,稍後會演示如何利用這個功能,在除錯的過程中,不修改程式碼,就能修復掉上文氣泡排序程式中的三個Bug。
進行自動化除錯,提高除錯效率
很多童鞋可能不知道,GDB支援非常強大的指令碼功能,除了GDB自己特定的指令碼外,它甚至還支援Python指令碼!
有了breakpoint commands lists
功能,結合GDB支援的指令碼功能,以及自定義命令功能,甚至可以實現除錯自動化。
其他還有很多非常有趣且實用的功能場景,限於篇幅,不再展開,有機會再寫文章專門介紹吧!
接下來,正式開始解決氣泡排序的三個Bug!
給氣泡排序打上"熱補丁"
現在,我們利用GDB的breakpoint command lists
功能,給文中的氣泡排序程式打上"熱補丁",演示如何在不修改原始碼、不重新編譯的前提下,解決掉程式中的三個BUG。
再看一下示例程式:
解決第一個BUG
先解決第22行的BUG:陣列arr元素個數是10,但是傳遞給了bubble_sort()
的引數卻是sizeof(arr)
,也就是40。
要解決這個BUG,我們只需要把引數修改成正確的值就行了。
我們知道,在x64上,優先採用暫存器傳遞函式引數。那麼,有這幾種方式可以選擇:
- • 把斷點設定在
bubble_sort()
入口第一條指令,然後直接修改存放陣列長度n的那個暫存器中的值。 - • 把斷點設定在
bubble_sort()
入口處(不必是第一條指令),在第7行for迴圈之前,把存放陣列長度的變數n的值改掉。 - • 把斷點設定在
main()
函式第22行,也就是呼叫bubble_sort()
的地方,然後以正確的引數手動呼叫bubble_sort()
函式,並利用GDB的jump
命令,跳過第22行程式碼的執行。
考慮到有些童鞋對x64 CPU不是非常瞭解,或者對GDB的jump
命令不熟悉,我們採用第2種方式。而且,這種方式也更簡單通用。
我們先在bubble_sort()
函式設定斷點,然後利用commands
命令預設一條命令,把變數n
的值修改為10。命令如下:
b bubble_sort
commands 1
set var n=10
end
設定完之後,用run命令開始執行程式。結果如下:
bubble_sort()
處的斷點被觸發後,程式暫停,用p(print)
命令檢視變數n的值,已經被修改成了正確的值:10。
可見,我們的設定是有效的。
斷點觸發後,讓程式自動恢復執行
bubble_sort()
處斷點被觸發,程式停了下來,修改完變數n的值後,怎麼自動恢復執行呢?
很簡單,只需要在預設的命令中新增一個continue
命令就可以了。為了證明我們的設定確實是生效的,在修改變數n
的前後,各新增一個格式化列印語句,把變數n
的值列印出來:
b bubble_sort
commands 1
printf "The original value of n is %d\n",n
set var n=10
printf "Current value of n is %d\n",n
continue
end
結果如下圖:
從執行結果可以看出,斷點被觸發後,我們預設的語句被正確執行,變數n
的值被修改為10,然後程式自動恢復執行。雖然最終程式不會發生segfault了,但列印出來的排序結果仍然是錯的!不著急,還有兩個BUG沒解決呢!
到此,第一個BUG已經解決了。
解決第二個BUG
下面,開始解決第7行的陣列訪問越界BUG:陣列的元素個數是n
,但是bubble_sort()
中第一個for迴圈的終止條件是i<=n
,明顯會造成訪問越界,正確的條件應該是i<n
。
要解決這個BUG也很簡單,只需要在執行第8行程式碼之前,判斷如果i
的值等於n
,就跳出迴圈。對於這個簡單的程式,我們直接從bubble_sort()
函式return
就可以了。
命令如下:
b 8 if i==n
command 2
printf "Current i = %d, n = %d\n",i,n
return
continue
end
在第8行設定條件斷點,當i==n
時斷點被觸發,然後自動把i
和n
的值列印出來,再行return
命令,從bubble_sort()
返回,然後continue
命令自動恢復程式執行。
執行結果如下圖:
解決第三個BUG
下面,解決最後一個BUG,第23行陣列訪問越界錯誤:陣列arr
的長度應該是10,不是sizeof(arr)
。
解決思路與第二個BUG類似,在第24行設定條件斷點,當i==10
時觸發斷點,然後用jump
命令跳出迴圈,讓程式跳轉到第26行繼續執行。命令如下:
b 24 if i==10
commands 3
printf "i=%d, exit from for loop!\n",i
jump 26
continue
end
執行結果如下圖所示:
從圖中可以看出,三個斷點全部被觸發,並且預設的命令都正常執行。最終程式正常結束,我們終於得到了正確的執行結果!
雖然,現在程式可以正常執行了,但每次都要手動輸入這麼多命令,想想都覺得麻煩!我之前文章介紹過,GDB支援除錯指令碼,可以從指令碼中載入並執行除錯命令。
下面,利用GDB指令碼,來製作我們的“熱補丁”檔案。
製作"熱補丁"指令碼
把上文中用來解決三個BUG的命令儲存在一個指令碼檔案中:
vi bubble.fix
指令碼內容如下圖:
bubble.fix指令碼中的命令,與上文在GDB中直接輸入的命令有幾個區別:
- • 刪除了格式化列印資訊。
- • 刪除了
commands
後面的斷點ID。上文講過,commands
後面的斷點ID可以省略,表示對最近一次設定的斷點有效。為了讓指令碼更加通用,每個commands
都緊跟在break
命令之後,因此直接省略了斷點ID。
GDB的指令碼可以透過兩種方式執行:
- • 啟動GDB時,用
-x
引數指定要執行的指令碼檔案。 - • 啟動GDB後,執行
source
命令執行指定的指令碼。
下面,我們用第二種方式演示一下,如下圖所示:
使用source命令載入並執行bubble.fix,然後用run
命令執行程式,三個斷點均被觸發,且預設的命令全部被正確執行,最後程式執行正常,得到期望的結果!
我們現在可以利用我們製作的"熱補丁"指令碼,在不修改程式碼、不重新編譯和部署的前提下,成功修復程式中的BUG!是不是很有趣呢?
不過,做到這種程度,還是有點瑕疵。雖然得到了正確的結果,但程式執行時,總是會列印斷點資訊,造成視覺干擾,作為典型的"偽完美主義者",這怎麼能忍!
最後,我們來解決這個問題,讓我們的"熱補丁"更加完美!
最佳化"熱補丁"指令碼,隱藏斷點資訊
在預設的命令中,如果第一條命令是silent
,斷點被觸發的列印資訊會被遮蔽掉。
我們把bubble.fix做些修改,把silent
命令加進去,如下圖所示:
此外,在最後面加了一個run
命令,這樣就不用每次手動執行了。
然後,我們換一種方式來執行:
這樣,看起來,清爽多了!
到此,我們終於實現了最終的目標:一邊debug,一邊修復BUG,並驗證解決方案,避免反覆修改程式碼、重新編譯和部署、提高除錯效率!
原文連結:https://zhuanlan.zhihu.com/p/698084327?utm_campaign=shareopn&utm_medium=social&utm_psn=1782817623383240705&utm_source=wechat_session