本系列專題,旨在介紹一些非常實用,卻不為大多數人所知的調試技巧。靈活運用這些調試技巧,能夠輕鬆解決一些我們經常遇到併為之困惑的問題,大大提高程序調試的效率。
感興趣的朋友,歡迎右上角關注!
引言 - 程序調試的痛
關於程序調試,有人喜歡各種調試工具,有人喜歡用簡單直接的log打印。兩種方法各有各的優勢和不足,大多時候是可以互補的。
在Linux環境下,GDB是各種調試工具中的佼佼者,而printf則是各種日誌打印方法中的典型代表。
調試問題時候,你遇到過下面的情況嗎?
代碼添加打印信息進行調試,突然發現添加打印的位置不對,或者別的地方也需要添加打印信息。
於是,重新修改源碼,重新添加打印,重新編譯,重新部署,重新運行,重新調試,重新分析。
當我們費了九牛二虎之力把這些都弄好之後,很不幸地又發現了新的問題,然後不得不反覆進行這些過程。
而且,當問題定位出來之後,我們之前花費很大力氣添加的調試信息,還必須從程序中刪除掉。
對於簡單的程序,這些尚可接受。但是,在大型項目中,單是編譯構建過程可能就要幾十分鐘,甚至數個小時,而部署過程則更為複雜。
你能想象得出,在這樣的項目中一直重複這些過程,是一件多麼痛苦的事情嗎?
那麼,有沒有一種方法,既不需要修改源碼,又能隨時在程序中任何地方任意添加打印信息呢?
當然有!GDB的動態打印功能正式為此而生的!
GDB Dynamic Printf
GDB提供了Dynamic Printf功能,下文我們稱之為動態打印。利用這個功能,我們可以在不修改程序源碼的情況下,隨時在程序的任何地方添加格式化打印。
如此一來,當然也就不需要重新編譯和部署的過程了。
我們先看一個簡單的示例,然後再詳細介紹它的實現原理,和相關命令的用法。
示例
一個簡單示例,如下圖所示:
先編譯一下:
<code>gcc -g test.c -o test/<code>
然後用GDB進行調試:
和期望的一樣,程序沒有任何打印輸出。
現在,我們在第6行、第11行、第14行分別添加一個動態打印斷點,用下面的命令:
<code>dprintf 6,"Hello, World!\\n"
dprintf 11,"i = %d, a = %d, b = %d\\n",i,a,b
dprintf 14,"Leaving! Bye bye!\\n"/<code>
如下圖:
稍微解釋一下:
- 第6行的語句會打印一句“Hello, World!”
- 第11行會把i、a、b的值分別打印出來
- 第14行打印“Leaving! Bye bye!”
設置好之後,查看一下斷點的信息:
已經設置成功了。然後,重新運行:
看到了吧,儘管我們並沒有對源碼做任何修改,且沒有重新編譯,但程序仍然按照我們的設置,打印出了我們想要的信息!
是不是很神奇呢?GDB的動態打印功能究竟是如何工作的呢?
GDB 動態打印實現原理
在上面的示例中,在設置好動態打印的信息之後,我們可以用info break命令查看所設置的信息。
可見,GDB的動態打印,本質上也是一種特殊的斷點。但是,它與一般的斷點又有所區別。
一般的斷點被觸發後,會中斷程序執行,然後等待用戶操作,並且用戶必須輸入continue命令讓程序恢復執行。
而動態打印斷點被觸發後,程序也會暫時中斷執行,但是不需要等待用戶響應,而是直接執行用戶預設的格式化打印語句,並自動恢復程序的執行。
GDB動態打印的使用方法
設置動態打印的命令是dprintf,格式如下:
<code>dprintf location,format string,arg1,arg2,.../<code>
dprintf命令和C語言中的printf的用法很相似,支持格式化打印。
相比printf函數,dprintf命令多了一個location參數,用於指定動態打印被觸發的位置。
和break命令設置斷點時一樣,location可以是文件名:行號、函數名、或者具體的地址等。
除了location外,剩餘的幾個參數,就和printf()函數一致了。format指定字符串打印的格式,後面幾個參數指定打印的數據來源。
以上面示例中的命令為例:
<code>dprintf 6,"Hello, World!\\n"
dprintf 11,"i = %d, a = %d, b = %d\\n",i,a,b
dprintf 14,"Leaving! Bye bye!\\n"/<code>
在功能上等價於下圖中右側的代碼:
到這裡,GDB動態打印的最基本功能就介紹完了。
斷點信息丟失怎麼辦?
在實際項目的調試過程中,難免會由於各種原因而必須要反覆的調試才能定位出問題的原因,或者徹底理解程序的代碼邏輯。
然而,dprintf本質上也是一種斷點,因此,當調試結束後,本次調試時設置的斷點信息就全部丟失了。如果要再次調試的話,就不得不重新設置一遍。
如果每次調試過程中,只需要設置一兩個動態打印的話,那倒也簡單。
可是,如果需要設置十幾個甚至幾十個動態打印呢?難道每次調試都要全部重新設置一遍嗎?想想都是一件比較麻煩的事情,對吧?
其實,GDB也有對應的處理方案,很簡單就可以解決!
保存和加載GDB斷點信息
為了解決上面提到的問題,GDB很貼心地提供了對斷點信息保存和加載的功能。
GDB中,可以把當前所設置的各種類型的斷點信息全部保存在一個腳本文件中。這其中當然也包括dprintf設置的動態打印信息。
只需要執行下面的命令即可:
<code>save breakpoints file_name/<code>
這條命令會把當前所有的斷點信息都保存在file_name指定的文件中。
等下次進行調試時,可以把file_name文件中的斷點信息重新加載起來。有兩種方法:
- 啟動GDB時使用“-x file_name”參數。
- 在GDB中執行source file_name命令。
下面分別演示一下。
保存斷點信息
我們用GDB重新啟動上面示例中的test程序:
- 用dprintf設置好斷點。
- 用info break命令查看一下斷點信息。
- 執行“save breakpoints test.bp”命令,把斷點信息保存在test.bp文件中。
我們看下一下test.bp中究竟保存了什麼內容:
原來,就是我們之前執行的三條dprintf命令,並且是以文本形式存在test.bp中的。
接下來,我們用兩種方法分別加載test.bp中的斷點信息。
用-x參數加載斷點信息
可見,指定-x參數後,GDB在開始調試程序之前,會從指定的文件中把斷點信息加載進來,並重新設置在程序中。因此,執行run命令後,程序能夠按照我們的預期正常執行動態打印功能。
source命令加載斷點信息
GDB把test加載起來之後,info break並沒有顯示出任何斷點信息。然後,我們執行source test.bp命令,GDB會把斷點信息從test.bp加載進來,並重新設置在test程序中。
結語
由於篇幅所限,本文只是介紹了GDB動態打印的基本功能和使用方法。其實,它還有很多高階的用法和技巧,以後會再更新文章進行講解。
程序調試是每個程序員必須要熟練掌握的基本技能,在整個計算機知識體系結構中,它佔據著非常重要的地位。
應一些朋友的要求,我近期會更新一系列程序調試相關的系列專題文章。會涉及到調試器的實現原理、常用工具的高階技巧、以及常見問題的定位方法和思路等內容。
本文是程序調試系列專題的第三篇,感興趣的朋友,歡迎閱讀其他已更新的內容:
C語言:GDB調試時遇到宏定義怎麼辦?一個小技巧幫你一秒鐘搞定
C語言:當GDB遇到複雜數據結構,兩分鐘帶你掌握四個高效調試技巧
對編譯、鏈接、內核等技術感興趣的朋友,歡迎閱讀另外一個正在更新中的專題:
你真的理解"Hello world"嗎? 從編譯鏈接到OS內核系列專題(已更新三篇)
看完之後,如果覺得有點收穫,不是完全浪費時間的話,別忘了點贊!把知識分享給更多志同道合的人!謝謝!
對本系列專題有什麼建議,或者對哪些技術感興趣的話,也歡迎留言討論!
對編譯器、OS內核、虛擬化、性能優化、程序調試等技術感興趣的童鞋,歡迎右上角關注!
閱讀更多 江南一散人 的文章