GDB高級技巧:邊Debug邊修復BUG,無需修改代碼,無需重新編譯

友情提醒:本文介紹的調試技巧非常實用,但為了講解清楚,篇幅較長,請耐心看完,我保證你定會有收穫!

引言

程序調試時,你是否遇到過下面幾種情況:

1、經過定位,終於找到了程序中的一個BUG,滿心歡喜地以為找到了root cause,便迫不及待地修改源碼,然後重新編譯,重新部署。但驗證時卻發現,真正的問題並沒有解決,代碼中還隱藏著更多的問題。

2、調試時,我們找到代碼中一個可疑的地方,但是不能100%確定這真的就是個BUG。要想確定,只能修改源碼、重新編譯、重新部署,然後重新運行驗證。

3、已經找到了root cause,但不確定解決方案是否能正常工作,為了驗證,不得不反覆地修改代碼、編譯、部署。

GDB高級技巧:邊Debug邊修復BUG,無需修改代碼,無需重新編譯

對於大型項目,編譯過程可能需要幾十分鐘,甚至幾個小時,部署過程則更為複雜漫長!可想而知,如果調試過程中,不得不反覆的修改源碼,然後重新編譯和部署,會是一項多麼繁瑣和浪費時間的事情!

那麼,有沒有一種更高效的調試手段,可以避免反覆修改代碼和編譯呢?

當然有!本文將介紹一種GDB調試技巧,可以一邊調試,一邊修復Bug,可以在不修改代碼、不重新編譯的前提下即可修復BUG,驗證我們的解決方案,大幅提高調試效率!

本文預期效果

如下圖,冒泡排序程序中,有三個BUG:

GDB高級技巧:邊Debug邊修復BUG,無需修改代碼,無需重新編譯

冒泡排序示例

圖中已經把三個BUG都標註了出來。正常編譯運行時,程序執行結果如下:

GDB高級技巧:邊Debug邊修復BUG,無需修改代碼,無需重新編譯

程序執行異常

不過是普通方式執行,還是在GDB中執行,程序都異常終止,無法得到正常結果。

但是,利用本文介紹的調試技巧,可以利用GDB給這個程序製作一個“熱補丁”,在不修改代碼、不重新編譯的前提下,解決掉程序中的三個BUG,讓程序正常執行,並得到預期結果!

最終效果,如下圖所示:

GDB高級技巧:邊Debug邊修復BUG,無需修改代碼,無需重新編譯

打上“熱補丁”後,程序正常執行

是不是很有趣呢?下面開始介紹!

GDB Breakpoint Command Lists

GDB支持斷點觸發後,自動執行用戶預設的一組調試命令。使用方法:

<code>commands [bp_id...] 

command-list
end/<code>

其中:

  • commands是GDB內置關鍵字
  • bp_id是斷點的ID,也就是info命令顯示出來的斷點Num,可以指定多個,也可以不指定。當不指定時,默認只對最近一次設置的那個斷點有效。
  • command-list是用戶預設的一組命令,當bp_id指定的斷點被觸發時,GDB會自動執行這些命令。
  • end表示結束。

這個功能適用於各種類型的斷點,如breakpoint、watchpoint、catchpoint等。

適用場景舉例

利用GDB breakpoint commands lists這個特性可以做很多有趣的事情,本文僅列舉其中的幾個。

1、隨時隨地printf,不需修改代碼和重新編譯

看過我之前文章的朋友,應該還記得,我介紹過GDB的動態打印(Dynamic Printf)功能,可以用dprintf命令在代碼的任意地方添加動態打印斷點,並自動執行格式化打印操作,從而無需修改代碼和重新編譯就可以在代碼中任意增加日誌打印信息。

利用GDB breakpoint commands lists功能,可以實現一樣的功能,而且除了打印之外,還可以做其它更多的操作,比如dump內存,dump寄存器等。

GDB高級技巧:邊Debug邊修復BUG,無需修改代碼,無需重新編譯

2、修改代碼執行邏輯,避免修改代碼和重新編譯

在GDB中可以做很多有趣的事情,比如修改變量、修改寄存器、調用函數等,結合breakpoint command list功能,可以在調試的同時,修改程序執行邏輯,給程序打上“熱補丁”。從而可以在調試過程中,快速修復Bug,避免重新修改代碼和重新編譯,大大提高程序調試的效率!

這是本文重點講解的場景,稍後會演示如何利用這個功能,在GDB調試的過程中修復掉上文冒泡排序程序中的三個Bug。

3、實現自動化調試,提高調試效率

這個功能,結合GDB支持的腳本功能,以及自定義命令功能,可以實現調試自動化。

這涉及到GDB的很多其它知識,篇幅有限,不再展開討論,以後更新專門文章講解!感興趣的童鞋,不妨右上角關注一下!

給冒泡排序打上“熱補丁”

現在,我們利用GDB breakpoint command lists功能,給文中的冒泡排序程序打上“熱補丁”,演示如何在不修改源碼、不重新編譯的前提下,解決掉程序中的3個BUG。

再看一下示例程序:

GDB高級技巧:邊Debug邊修復BUG,無需修改代碼,無需重新編譯

編譯一下:

<code>gcc -g bubble.c -o bubble/<code>

先用GDB加載運行一下:

GDB高級技巧:邊Debug邊修復BUG,無需修改代碼,無需重新編譯

程序運行異常,符合我們的預期。

下面我們依次解決冒泡排序程序中的3個BUG。

1、解決第一個BUG

先解決第22行的BUG,也就是傳遞給了bubble_sort()錯誤的數組長度。

我們知道,在x64上,函數參數優先採用寄存器傳遞。那麼,我們有這麼幾種方式可以選擇:

  1. 把斷點設置在bubble_sort()入口第一條指令,然後直接修改存放數組長度n的那個寄存器中的值。
  2. 把斷點設置在bubble_sort()入口處(不必是第一條指令),在第7行for循環之前,把存放數組長度的變量n的值改掉。
  3. 把斷點設置在main()函數第22行,也就是調用bubble_sort()的地方,然後以正確的參數手動調用bubble_sort()函數,並利用GDB的jump命令,跳過第22行代碼的執行。
GDB高級技巧:邊Debug邊修復BUG,無需修改代碼,無需重新編譯

考慮到有些童鞋對x64 CPU不是非常瞭解,或者對GDB的jump命令不熟悉,我們採用第2種方式。而且,這種方式也更簡單通用。

我們先給bubble_sort()函數設置斷點,然後利用commands命令預設一條命令,把變量n的值修改為10。命令如下:

<code>b bubble_sort
commands 1
set var n=10
end/<code>

設置完之後,用run命令開始運行程序。結果如下:

GDB高級技巧:邊Debug邊修復BUG,無需修改代碼,無需重新編譯

bubble_sort()處的斷點被觸發後,程序暫停,用print命令查看變量n的值,已經被修改成了正確的值:10。

可見,我們的設置是有效的。

斷點觸發後,讓程序自動恢復執行

那麼,在bubble_sort()處斷點被觸發,變量n的值被修改之後,如何讓程序自動恢復執行呢?

很簡單,只需要在預設的命令中添加一個continue命令就可以了。為了證明我們的設置確實是生效的,我們在修改變量n的前後,各添加一個格式化打印語句,把變量n的值打印出來:

<code>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/<code>

結果如下圖:

GDB高級技巧:邊Debug邊修復BUG,無需修改代碼,無需重新編譯

解決第一個BUG

從運行結果可以看出,斷點被觸發後,我們預設的語句被正確執行,變量n的值被修改為10,然後程序自動恢復執行。

到此,第一個BUG已經解決了。

2、解決第二個BUG

下面,我們解決第7行代碼中的數組訪問越界錯誤:數組的元素個數是n,但是bubble_sort()中第一個for循環的終止條件是i<=n,明顯會造成訪問越界,正確的條件應該是i<n。

要解決這個BUG也很簡單,只需要在執行第8行代碼之前,判斷如果i的值等於n,就跳出循環。對於這個簡單的程序,我們直接從bubble_sort()函數return就可以了。

命令如下:

<code>b 8 if i==n
command 2
printf "i = %d, n = %d\\n",i,n
return
continue
end/<code>

在第8行設置條件斷點,當i==n時斷點被觸發,然後自動把i和n的值打印出來,再行return命令,從bubble_sort()返回,然後continue命令自動恢復程序執行。

執行結果如下圖:

GDB高級技巧:邊Debug邊修復BUG,無需修改代碼,無需重新編譯

解決第二個BUG

3、解決第三個BUG

下面,解決最後一個BUG,第23行數組訪問越界錯誤。

命令如下:

<code>b 24 if i==10
commands 3
printf "i=%d, exit from for loop!\\n",i
jump 26
continue

end/<code>

與第二個BUG類似,在第24行設置條件斷點,當==10時觸發斷點,然後退出循環,讓程序跳轉到第26行繼續執行。

執行結果如下圖所示:

GDB高級技巧:邊Debug邊修復BUG,無需修改代碼,無需重新編譯

解決第三個BUG

從圖中可以看出,三個斷點全部被觸發,並且預設的命令都正常執行。

我們終於得到了正確的執行結果!

雖然,現在程序可以正常執行了,但是每次手動輸入命令還是比較麻煩的。我之前文章介紹過,GDB支持調試腳本,從腳本中加載並執行調試命令。

下面,我們利用GDB腳本,來製作我們的“熱補丁”腳本。

製作“熱補丁”腳本

我們把上文中用來解決三個BUG的命令保存在一個腳本文件中:

<code>vi bubble.fix/<code>

腳本內容如下圖:

GDB高級技巧:邊Debug邊修復BUG,無需修改代碼,無需重新編譯

bubble.fix 熱補丁腳本

bubble.fix腳本中的命令,與上文在GDB中直接輸入的命令有幾個區別:

  1. 刪除了格式化打印信息。
  2. 刪除了commands後面的斷點ID。上文講過,commands後面的斷點ID可以省略,表示對最近一次設置的斷點有效。為了讓腳本更加通用,每個commands都緊跟在break命令之後,因此直接省略了斷點ID。

GDB的腳本可以通過兩種方式執行:

  1. 啟動GDB時,用-x參數指定要執行的腳本文件。
  2. 啟動GDB後,執行source命令執行指定的腳本。

下面,我們用第二種方式演示一下,如下圖所示:

GDB高級技巧:邊Debug邊修復BUG,無需修改代碼,無需重新編譯

執行bubble.fix腳本

使用source命令加載並執行bubble.fix,然後用run命令執行程序,三個斷點均被觸發,且預設的命令全部被正確執行,最後程序運行正常,得到期望的結果!

我們現在可以利用我們製作的“熱補丁”腳本,在不修改代碼、不重新編譯和部署的前提下,成功修復程序中的BUG!是不是很有趣呢?

不過,做到這種程度,還不算完美!

儘管得到了正確的結果,但程序執行時,總是會打印我們設置的斷點信息,看起來還是有些視覺干擾的。

最後,我們來解決這個問題,讓我們的“熱補丁”更加完美!

優化“熱補丁”腳本,隱藏斷點信息

在預設的命令中,如果第一條命令是silent,斷點被觸發的打印信息會被屏蔽掉。

我們把bubble.fix做些修改,把silent命令加進去,如下圖所示:

GDB高級技巧:邊Debug邊修復BUG,無需修改代碼,無需重新編譯

最終版bubble.fix 腳本

然後,重新執行一下:

GDB高級技巧:邊Debug邊修復BUG,無需修改代碼,無需重新編譯

這樣,看起來,清爽多了!

到此,我們終於實現了本文的目標:一邊debug,一邊修復BUG,避免反覆修改代碼、重新編譯和部署、提高調試效率!

結語

本文重點介紹瞭如何利用GDB breakpoint command lists功能,製作“調試熱補丁”,修改代碼BUG。還可以利用這個功能,快速驗證我們的猜想和解決方案,避免反覆修改代碼和重新編譯。

巧用GDB breakpoint command lists功能,可以做很多有趣的事情,如實現調試自動化,提高調試效率等。


本文是調試系列專題文章的第七篇,如果對調試感興趣的話,歡迎圍觀其它已更新的內容,相信一定會有收穫的:

段錯誤(segmentation fault ):9種實用調試方法,你用過幾種?

GDB動態打印:讓你隨時隨地printf,不需修改代碼,不需重新編譯

調試引入的不確定性:必現的BUG神秘消失,斷點改變代碼執行邏輯

Linux調試技巧:GDB自定義命令,按需定製適合自己的調試工具

C語言:當GDB遇到複雜數據結構,兩分鐘帶你掌握四個高效調試技巧

C語言:GDB調試時遇到宏定義怎麼辦?一個小技巧幫你一秒鐘搞定

對編譯、鏈接、OS內核等感謝的童鞋,歡迎圍觀另外一個系列專題:

你真的理解"Hello world"嗎? 從編譯鏈接到OS內核系列專題(已更新三篇)


有任何疑問、建議,歡迎留言討論!

原創不易,別忘了轉發點贊,把知識分享給志同道合的朋友,謝謝!

對編譯器、OS內核、性能調優、虛擬化等技術感興趣的童鞋,歡迎右上角關注!

版權聲明:未經允許,禁止轉載。文中部分圖片來源網絡,如有侵權,請通知刪除!


分享到:


相關文章: