【轉載】GDB高階技巧:邊Debug邊修復BUG,無需修改程式碼,無需重新編譯

学习,积累,成长發表於2024-06-10

除錯是每個程式設計師都逃不過的宿命!

程式除錯是一件非常考驗耐心的事情,因為除錯過程中經常會需要反覆的修改原始碼,重新編譯、重新部署、重新執行,這個過程通常是非常枯燥和繁瑣的。尤其對於大型專案,光是編譯可能需要幾十分鐘,甚至幾個小時,部署過程則可能更為複雜漫長!

那麼,有沒有一種更高效的除錯手段,可以避免反覆修改程式碼和編譯呢?

這個真的有!本文將介紹一種除錯技巧,可以一邊除錯,一邊修復Bug,能夠在不修改程式碼、不重新編譯的前提下修復BUG,並且驗證解決方案,大幅提高除錯效率!

先看下最終效果吧!

本文預期效果

如下圖,氣泡排序中,有三個常見的BUG:

img

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

img

GDB中執行時:

img

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

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

最終效果,如下圖所示:

img

所有的黑魔法,都在這個補丁檔案bubble.fix中!

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

關於GDB

我之前寫了幾篇文章,專門介紹GDB的一些非常實用卻鮮為人知的高階用法,感興趣的小夥伴可以去翻看下除錯系列專題文章。

GDB的基本用法,相信大家都很熟悉了,就不過多介紹了,直接講重點吧!

Breakpoint Command Lists

GDB支援在斷點觸發後,自動執行使用者預設的一組除錯命令。使用方法:

commands [bp_id...]
  command-list
end

其中:

  • commands是GDB內建關鍵字。
  • bp_idi(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。

再看一下示例程式:

img

解決第一個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命令開始執行程式。結果如下:

img

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

結果如下圖:

img

從執行結果可以看出,斷點被觸發後,我們預設的語句被正確執行,變數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時斷點被觸發,然後自動把in的值列印出來,再行return命令,從bubble_sort()返回,然後continue命令自動恢復程式執行。

執行結果如下圖:

img

解決第三個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

執行結果如下圖所示:

img

從圖中可以看出,三個斷點全部被觸發,並且預設的命令都正常執行。最終程式正常結束,我們終於得到了正確的執行結果!

雖然,現在程式可以正常執行了,但每次都要手動輸入這麼多命令,想想都覺得麻煩!我之前文章介紹過,GDB支援除錯指令碼,可以從指令碼中載入並執行除錯命令。

下面,利用GDB指令碼,來製作我們的“熱補丁”檔案。

製作"熱補丁"指令碼

把上文中用來解決三個BUG的命令儲存在一個指令碼檔案中:

vi bubble.fix

指令碼內容如下圖:

img

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

  • • 刪除了格式化列印資訊。
  • • 刪除了commands後面的斷點ID。上文講過,commands後面的斷點ID可以省略,表示對最近一次設定的斷點有效。為了讓指令碼更加通用,每個commands都緊跟在break命令之後,因此直接省略了斷點ID。

GDB的指令碼可以透過兩種方式執行:

  • • 啟動GDB時,用-x引數指定要執行的指令碼檔案。
  • • 啟動GDB後,執行source命令執行指定的指令碼。

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

img

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

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

不過,做到這種程度,還是有點瑕疵。雖然得到了正確的結果,但程式執行時,總是會列印斷點資訊,造成視覺干擾,作為典型的"偽完美主義者",這怎麼能忍!

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

最佳化"熱補丁"指令碼,隱藏斷點資訊

在預設的命令中,如果第一條命令是silent,斷點被觸發的列印資訊會被遮蔽掉。

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

img

此外,在最後面加了一個run命令,這樣就不用每次手動執行了。

然後,我們換一種方式來執行:

img

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

到此,我們終於實現了最終的目標:一邊debug,一邊修復BUG,並驗證解決方案,避免反覆修改程式碼、重新編譯和部署、提高除錯效率!

原文連結:https://zhuanlan.zhihu.com/p/698084327?utm_campaign=shareopn&utm_medium=social&utm_psn=1782817623383240705&utm_source=wechat_session

相關文章