GNU調(diào)試器是發(fā)現(xiàn)程序缺陷的有力工具。
如果你是一個(gè)程序員,想在你的軟件增加某些功能,你首先考慮實(shí)現(xiàn)它的方法:例如寫(xiě)一個(gè)方法、定義一個(gè)類(lèi),或者創(chuàng)建新的數(shù)據(jù)類(lèi)型。然后你用編譯器或解釋器可以理解的編程語(yǔ)言來(lái)實(shí)現(xiàn)這個(gè)功能。但是,如果你覺(jué)得你所有代碼都正確,但是編譯器或解釋器依然無(wú)法理解你的指令怎么辦?如果軟件大多數(shù)情況下都運(yùn)行良好,但是在某些環(huán)境下出現(xiàn)缺陷怎么辦?這種情況下,你得知道如何正確使用調(diào)試器找到問(wèn)題的根源。
GNU 調(diào)試器GNU Project Debugger(GDB)是一個(gè)發(fā)現(xiàn)項(xiàng)目缺陷的強(qiáng)大工具。它通過(guò)追蹤程序運(yùn)行過(guò)程中發(fā)生了什么來(lái)幫助你發(fā)現(xiàn)程序錯(cuò)誤或崩潰的原因。(LCTT 校注:GDB 全程是“GNU Project Debugger”,即 “GNU 項(xiàng)目調(diào)試器”,但是通常我們簡(jiǎn)稱(chēng)為“GNU 調(diào)試器”)
本文是 GDB 基本用法的實(shí)踐教程。請(qǐng)跟隨示例,打開(kāi)命令行并克隆此倉(cāng)庫(kù):
git clone
快捷方式
GDB 的每條命令都可以縮短。例如:顯示設(shè)定的斷點(diǎn)的 info break
命令可以被縮短為i break
。你可能在其他地方看到過(guò)這種縮寫(xiě),但在本文中,為了清晰展現(xiàn)使用的函數(shù),我將所寫(xiě)出整個(gè)命令。
命令行參數(shù)
你可以將 GDB 附加到每個(gè)可執(zhí)行文件。進(jìn)入你克隆的倉(cāng)庫(kù)(core_dump_example
),運(yùn)行make
進(jìn)行編譯。你現(xiàn)在能看到一個(gè)名為coredump
的可執(zhí)行文件。(更多信息,請(qǐng)參考我的文章《創(chuàng)建和調(diào)試 Linux 的轉(zhuǎn)儲(chǔ)文件》。)
要將 GDB 附加到這個(gè)可執(zhí)行文件,請(qǐng)輸入: gdb coredump
。
你的輸出應(yīng)如下所示:
返回結(jié)果顯示沒(méi)有找到調(diào)試符號(hào)。
調(diào)試信息是目標(biāo)文件object file(可執(zhí)行文件)的組成部分,調(diào)試信息包括數(shù)據(jù)類(lèi)型、函數(shù)簽名、源代碼和操作碼之間的關(guān)系。此時(shí),你有兩種選擇:
- 繼續(xù)調(diào)試匯編代碼(參見(jiàn)下文“無(wú)符號(hào)調(diào)試”)
- 使用調(diào)試信息進(jìn)行編譯,參見(jiàn)下一節(jié)內(nèi)容
使用調(diào)試信息進(jìn)行編譯
為了在二進(jìn)制文件中包含調(diào)試信息,你必須重新編譯。打開(kāi) Makefile
,刪除第 9 行的注釋標(biāo)簽(#
)后重新編譯:
CFLAGS =-Wall -Werror -std=c++11 -g
-g
告訴編譯器包含調(diào)試信息。運(yùn)行make clean
,接著運(yùn)行make
,然后再次調(diào)用 GDB。你得到如下輸出后就可以調(diào)試代碼了:
新增的調(diào)試信息會(huì)增加可執(zhí)行文件的大小。在這種情況下,執(zhí)行文件增加了 2.5 倍(從 26,088 字節(jié) 增加到 65,480 字節(jié))。
輸入 run -c1
,使用-c1
開(kāi)關(guān)啟動(dòng)程序。當(dāng)程序運(yùn)行到達(dá)State_4
時(shí)將崩潰:
你可以檢索有關(guān)程序的其他信息,info source
命令提供了當(dāng)前文件的信息:
- 101 行代碼
- 語(yǔ)言: C++
- 編譯器(版本、調(diào)優(yōu)、架構(gòu)、調(diào)試標(biāo)志、語(yǔ)言標(biāo)準(zhǔn))
- 調(diào)試格式:DWARF 2
- 沒(méi)有預(yù)處理器宏指令(使用 GCC 編譯時(shí),宏僅在 使用 -g3 標(biāo)志編譯時(shí)可用)。
info shared
命令打印了動(dòng)態(tài)庫(kù)列表機(jī)器在虛擬地址空間的地址,它們?cè)趩?dòng)時(shí)被加載到該地址,以便程序運(yùn)行:
如果你想了解 Linux 中的庫(kù)處理方式,請(qǐng)參見(jiàn)我的文章 在 Linux 中如何處理動(dòng)態(tài)庫(kù)和靜態(tài)庫(kù)。
調(diào)試程序
你可能已經(jīng)注意到,你可以在 GDB 中使用 run
命令啟動(dòng)程序。run
命令接受命令行參數(shù),就像從控制臺(tái)啟動(dòng)程序一樣。-c1
開(kāi)關(guān)會(huì)導(dǎo)致程序在第 4 階段崩潰。要從頭開(kāi)始運(yùn)行程序,你不用退出 GDB,只需再次運(yùn)行run
命令。如果沒(méi)有-c1
開(kāi)關(guān),程序?qū)⑾萑胨姥h(huán),你必須使用Ctrl+C
來(lái)結(jié)束死循環(huán)。
你也可以一步一步運(yùn)行程序。在 C/C++ 中,入口是 main
函數(shù)。使用list main
命令打開(kāi)顯示main
函數(shù)的部分源代碼:
main
函數(shù)在第 33 行,因此可以輸入break 33
在 33 行添加斷點(diǎn):
輸入 run
運(yùn)行程序。正如預(yù)期的那樣,程序在main
函數(shù)處停止。輸入layout src
并排查看源代碼:
你現(xiàn)在處于 GDB 的文本用戶(hù)界面(TUI)模式。可以使用鍵盤(pán)向上和向下箭頭鍵滾動(dòng)查看源代碼。
GDB 高亮顯示當(dāng)前執(zhí)行行。你可以輸入 next
(n
)命令逐行執(zhí)行命令。如果你沒(méi)有指定新的命令,GBD 會(huì)執(zhí)行上一條命令。要逐行運(yùn)行代碼,只需按回車(chē)鍵。
有時(shí),你會(huì)發(fā)現(xiàn)文本的輸出有點(diǎn)顯示不正常:
如果發(fā)生這種情況,請(qǐng)按 Ctrl+L
重置屏幕。
使用 Ctrl+X+A
可以隨時(shí)進(jìn)入和退出 TUI 模式。你可以在手冊(cè)中找到其他的鍵綁定。
要退出 GDB,只需輸入 quit
。
設(shè)置監(jiān)察點(diǎn)
這個(gè)示例程序的核心是一個(gè)在無(wú)限循環(huán)中運(yùn)行的狀態(tài)機(jī)。n_state
變量枚舉了當(dāng)前所有狀態(tài):
while(true){
switch(n_state){
case State_1:
std::cout
如果你希望當(dāng) n_state
的值為State_5
時(shí)停止程序。為此,請(qǐng)?jiān)?code>main函數(shù)處停止程序并為n_state
設(shè)置監(jiān)察點(diǎn):
watch n_state == State_5
只有當(dāng)所需的變量在當(dāng)前上下文中可用時(shí),使用變量名設(shè)置監(jiān)察點(diǎn)才有效。
當(dāng)你輸入 continue
繼續(xù)運(yùn)行程序時(shí),你會(huì)得到如下輸出:
如果你繼續(xù)運(yùn)行程序,當(dāng)監(jiān)察點(diǎn)表達(dá)式評(píng)估為 false
時(shí) GDB 將停止:
你可以為一般的值變化、特定的值、讀取或?qū)懭霑r(shí)來(lái)設(shè)置監(jiān)察點(diǎn)。
更改斷點(diǎn)和監(jiān)察點(diǎn)
輸入 info watchpoints
打印先前設(shè)置的監(jiān)察點(diǎn)列表:
刪除斷點(diǎn)和監(jiān)察點(diǎn)
如你所見(jiàn),監(jiān)察點(diǎn)就是數(shù)字。要?jiǎng)h除特定的監(jiān)察點(diǎn),請(qǐng)先輸入 delete
后輸入監(jiān)察點(diǎn)的編號(hào)。例如,我的監(jiān)察點(diǎn)編號(hào)為 2;要?jiǎng)h除此監(jiān)察點(diǎn),輸入delete 2
。
注意:如果你使用delete
而沒(méi)有指定數(shù)字,所有監(jiān)察點(diǎn)和斷點(diǎn)將被刪除。
這同樣適用于斷點(diǎn)。在下面的截屏中,我添加了幾個(gè)斷點(diǎn),輸入 info breakpoint
打印斷點(diǎn)列表:
要?jiǎng)h除單個(gè)斷點(diǎn),請(qǐng)先輸入 delete
后輸入斷點(diǎn)的編號(hào)。另外一種方式:你可以通過(guò)指定斷點(diǎn)的行號(hào)來(lái)刪除斷點(diǎn)。例如,clear 78
命令將刪除第 78 行設(shè)置的斷點(diǎn)號(hào) 7。
禁用或啟用斷點(diǎn)和監(jiān)察點(diǎn)
除了刪除斷點(diǎn)或監(jiān)察點(diǎn)之外,你可以通過(guò)輸入 disable
,后輸入編號(hào)禁用斷點(diǎn)或監(jiān)察點(diǎn)。在下文中,斷點(diǎn) 3 和 4 被禁用,并在代碼窗口中用減號(hào)標(biāo)記:
也可以通過(guò)輸入類(lèi)似 disable 2 - 4
修改某個(gè)范圍內(nèi)的斷點(diǎn)或監(jiān)察點(diǎn)。如果要重新激活這些點(diǎn),請(qǐng)輸入enable
,然后輸入它們的編號(hào)。
條件斷點(diǎn)
首先,輸入 delete
刪除所有斷點(diǎn)和監(jiān)察點(diǎn)。你仍然想使程序停在main
函數(shù)處,如果你不想指定行號(hào),可以通過(guò)直接指明該函數(shù)來(lái)添加斷點(diǎn)。輸入break main
從而在main
函數(shù)處添加斷點(diǎn)。
輸入 run
從頭開(kāi)始運(yùn)行程序,程序?qū)⒃?code>main函數(shù)處停止。
main
函數(shù)包括變量n_state_3_count
,當(dāng)狀態(tài)機(jī)達(dá)到狀態(tài) 3 時(shí),該變量會(huì)遞增。
基于 n_state_3_count
的值添加一個(gè)條件斷點(diǎn),請(qǐng)輸入:
break 54 if n_state_3_count == 3
繼續(xù)運(yùn)行程序。程序?qū)⒃诘?54 行停止之前運(yùn)行狀態(tài)機(jī) 3 次。要查看 n_state_3_count
的值,請(qǐng)輸入:
print n_state_3_count
使斷點(diǎn)成為條件斷點(diǎn)
你也可以使現(xiàn)有斷點(diǎn)成為條件斷點(diǎn)。用 clear 54
命令刪除最近添加的斷點(diǎn),并通過(guò)輸入break 54
命令添加一個(gè)簡(jiǎn)單的斷點(diǎn)。你可以輸入以下內(nèi)容使此斷點(diǎn)成為條件斷點(diǎn):
condition 3 n_state_3_count == 9
3
指的是斷點(diǎn)編號(hào)。
在其他源文件中設(shè)置斷點(diǎn)
如果你的程序由多個(gè)源文件組成,你可以在行號(hào)前指定文件名來(lái)設(shè)置斷點(diǎn),例如,break main. cpp:54
。
捕捉點(diǎn)
除了斷點(diǎn)和監(jiān)察點(diǎn)之外,你還可以設(shè)置捕獲點(diǎn)。捕獲點(diǎn)適用于執(zhí)行系統(tǒng)調(diào)用、加載共享庫(kù)或引發(fā)異常等事件。
要捕獲用于寫(xiě)入 STDOUT 的 write
系統(tǒng)調(diào)用,請(qǐng)輸入:
catch syscall write
每當(dāng)程序?qū)懭肟刂婆_(tái)輸出時(shí),GDB 將中斷執(zhí)行。
在手冊(cè)中,你可以找到一整章關(guān)于 斷點(diǎn)、監(jiān)察點(diǎn)和捕捉點(diǎn)的內(nèi)容。
評(píng)估和操作符號(hào)
用 print
命令可以打印變量的值。一般語(yǔ)法是print
。修改變量的值,請(qǐng)輸入:
set variable
在下面的截屏中,我將變量 n_state_3_count
的值設(shè)為123
。
/x
表達(dá)式以十六進(jìn)制打印值;使用&
運(yùn)算符,你可以打印虛擬地址空間內(nèi)的地址。
如果你不確定某個(gè)符號(hào)的數(shù)據(jù)類(lèi)型,可以使用 whatis
來(lái)查明。
如果你要列出 main
函數(shù)范圍內(nèi)可用的所有變量,請(qǐng)輸入info scope main
:
DW_OP_fbreg
值是指基于當(dāng)前子程序的堆棧偏移量。
或者,如果你已經(jīng)在一個(gè)函數(shù)中并且想要列出當(dāng)前堆棧幀上的所有變量,你可以使用 info locals
:
查看手冊(cè)以了解更多 檢查符號(hào)的內(nèi)容。
附加調(diào)試到一個(gè)正在運(yùn)行的進(jìn)程
gdb attach
命令允許你通過(guò)指定進(jìn)程 ID(PID)附加到一個(gè)已經(jīng)在運(yùn)行的進(jìn)程進(jìn)行調(diào)試。幸運(yùn)的是,coredump
程序?qū)⑵洚?dāng)前 PID 打印到屏幕上,因此你不必使用ps或top手動(dòng)查找 PID。
啟動(dòng) coredump
應(yīng)用程序的一個(gè)實(shí)例:
./coredump
操作系統(tǒng)顯示 PID 為 2849
。打開(kāi)一個(gè)單獨(dú)的控制臺(tái)窗口,移動(dòng)到coredump
應(yīng)用程序的根目錄,然后用 GDB 附加到該進(jìn)程進(jìn)行調(diào)試:
gdb attach 2849
當(dāng)你用 GDB 附加到進(jìn)程時(shí),GDB 會(huì)立即停止進(jìn)程運(yùn)行。輸入 layout src
和backtrace
來(lái)檢查調(diào)用堆棧:
輸出顯示在 main.cpp
第 92 行調(diào)用std::this_thread::sleep_for
函數(shù)時(shí)進(jìn)程中斷。
只要你退出 GDB,該進(jìn)程將繼續(xù)運(yùn)行。
你可以在 GDB 手冊(cè)中找到有關(guān) 附加調(diào)試正在運(yùn)行的進(jìn)程的更多信息。
在堆棧中移動(dòng)
在命令窗口,輸入 up
兩次可以在堆棧中向上移動(dòng)到main.cpp
:
通常,編譯器將為每個(gè)函數(shù)或方法創(chuàng)建一個(gè)子程序。每個(gè)子程序都有自己的棧幀,所以在棧幀中向上移動(dòng)意味著在調(diào)用棧中向上移動(dòng)。
你可以在手冊(cè)中找到有關(guān) 堆棧計(jì)算的更多信息。
指定源文件
當(dāng)調(diào)試一個(gè)已經(jīng)在運(yùn)行的進(jìn)程時(shí),GDB 將在當(dāng)前工作目錄中尋找源文件。你也可以使用 目錄命令手動(dòng)指定源目錄。
評(píng)估轉(zhuǎn)儲(chǔ)文件
閱讀 創(chuàng)建和調(diào)試 Linux 的轉(zhuǎn)儲(chǔ)文件了解有關(guān)此主題的信息。
參考文章太長(zhǎng),簡(jiǎn)單來(lái)說(shuō)就是:
- 假設(shè)你使用的是最新版本的 Fedora
- 使用
-c1
開(kāi)關(guān)調(diào)用 coredump:coredump -c1
- 使用 GDB 加載最新的轉(zhuǎn)儲(chǔ)文件:
coredumpctl debug
- 打開(kāi) TUI 模式并輸入
layout src
backtrace
的輸出顯示崩潰發(fā)生在距離main.cpp
五個(gè)棧幀之外?;剀?chē)直接跳轉(zhuǎn)到main.cpp
中的錯(cuò)誤代碼行:
看源碼發(fā)現(xiàn)程序試圖釋放一個(gè)內(nèi)存管理函數(shù)沒(méi)有返回的指針。這會(huì)導(dǎo)致未定義的行為并引起 SIGABRT
。
無(wú)符號(hào)調(diào)試
如果沒(méi)有源代碼,調(diào)試就會(huì)變得非常困難。當(dāng)我在嘗試解決逆向工程的挑戰(zhàn)時(shí),我第一次體驗(yàn)到了這一點(diǎn)。了解一些 匯編語(yǔ)言的知識(shí)會(huì)很有用。
我們用例子看看它是如何運(yùn)行的。
找到根目錄,打開(kāi) Makefile
,然后像下面一樣編輯第 9 行:
CFLAGS =-Wall -Werror -std=c++11 #-g
要重新編譯程序,先運(yùn)行 make clean
,再運(yùn)行make
,最后啟動(dòng) GDB。該程序不再有任何調(diào)試符號(hào)來(lái)引導(dǎo)源代碼的走向。
info file
命令顯示二進(jìn)制文件的內(nèi)存區(qū)域和入口點(diǎn):
.text
區(qū)段始終從入口點(diǎn)開(kāi)始,其中包含實(shí)際的操作碼。要在入口點(diǎn)添加斷點(diǎn),輸入break *0x401110
然后輸入run
開(kāi)始運(yùn)行程序:
要在某個(gè)地址設(shè)置斷點(diǎn),使用取消引用運(yùn)算符 *
來(lái)指定地址。
選擇反匯編程序風(fēng)格
在深入研究匯編之前,你可以選擇要使用的 匯編風(fēng)格。 GDB 默認(rèn)是 AT&T,但我更喜歡 Intel 語(yǔ)法。變更風(fēng)格如下:
set disassembly-flavor intel
現(xiàn)在輸入 layout asm
調(diào)出匯編代碼窗口,輸入layout reg
調(diào)出寄存器窗口。你現(xiàn)在應(yīng)該看到如下輸出:
保存配置文件
盡管你已經(jīng)輸入了許多命令,但實(shí)際上還沒(méi)有開(kāi)始調(diào)試。如果你正在大量調(diào)試應(yīng)用程序或嘗試解決逆向工程的難題,則將 GDB 特定設(shè)置保存在文件中會(huì)很有用。
該項(xiàng)目的 GitHub 存儲(chǔ)庫(kù)中的 gdbinit配置文件包含最近使用的命令:
set disassembly-flavor intel
set write on
break *0x401110
run -c2
layout asm
layout reg
set write on
命令使你能夠在程序運(yùn)行期間修改二進(jìn)制文件。
退出 GDB 并使用配置文件重新啟動(dòng) GDB : gdb -x gdbinit coredump
。
閱讀指令
應(yīng)用 c2
開(kāi)關(guān)后,程序?qū)⒈罎ⅰ3绦蛟谌肟诤瘮?shù)處停止,因此你必須寫(xiě)入continue
才能繼續(xù)運(yùn)行:
idiv
指令進(jìn)行整數(shù)除法運(yùn)算:RAX
寄存器中為被除數(shù),指定參數(shù)為除數(shù)。商被加載到RAX
寄存器中,余數(shù)被加載到RDX
中。
從寄存器角度,你可以看到 RAX
包含5
,因此你必須找出存儲(chǔ)堆棧中位置為rbp-0x4
的值。
讀取內(nèi)存
要讀取原始內(nèi)存內(nèi)容,你必須指定比讀取符號(hào)更多的參數(shù)。在匯編輸出中向上滾動(dòng)一點(diǎn),可以看到堆棧的劃分:
你最感興趣的應(yīng)該是 rbp-0x4
的值,因?yàn)樗?code>idiv的存儲(chǔ)參數(shù)。你可以從截圖中看到rbp-0x8
位置的下一個(gè)變量,所以rbp-0x4
位置的變量是 4 字節(jié)寬。
在 GDB 中,你可以使用 x
命令查看任何內(nèi)存內(nèi)容:
x/
n、f
、u
> addr >
可選參數(shù):
n
:?jiǎn)卧笮〉闹貜?fù)計(jì)數(shù)(默認(rèn)值:1)f
:格式說(shuō)明符,如printfu
:?jiǎn)卧笮?ul>b
:字節(jié)h
:半字(2 個(gè)字節(jié))- w: 字(4 個(gè)字節(jié))(默認(rèn))
- g: 雙字(8 個(gè)字節(jié))
要打印 rbp-0x4
的值,請(qǐng)輸入x/u $rbp-4
:
如果你能記住這種模式,則可以直接查看內(nèi)存。參見(jiàn)手冊(cè)中的 部分。
操作匯編
子程序 zeroDivide
發(fā)生運(yùn)算異常。當(dāng)你用向上箭頭鍵向上滾動(dòng)一點(diǎn)時(shí),你會(huì)找到下面信息:
0x401211
這被稱(chēng)為 函數(shù)前言:
- 調(diào)用函數(shù)的基指針(
rbp
)存放在棧上 - 棧指針(
rsp
)的值被加載到基指針(rbp
)
完全跳過(guò)這個(gè)子程序。你可以使用 backtrace
查看調(diào)用堆棧。在main
函數(shù)之前只有一個(gè)堆棧幀,所以你可以用一次up
回到main
:
在你的 main
函數(shù)中,你會(huì)找到下面信息:
0x401431
子程序 zeroDivide
僅在jump equal (je)
為true
時(shí)進(jìn)入。你可以輕松地將其替換為jump-not-equal (jne)
指令,該指令的操作碼為0x75
(假設(shè)你使用的是 x86/64 架構(gòu);其他架構(gòu)上的操作碼不同)。輸入run
重新啟動(dòng)程序。當(dāng)程序在入口函數(shù)處停止時(shí),設(shè)置操作碼:
set *(unsigned char*)0x401435 = 0x75
最后,輸入 continue
。該程序?qū)⑻^(guò)子程序zeroDivide
并且不會(huì)再崩潰。
總結(jié)
你會(huì)在許多集成開(kāi)發(fā)環(huán)境(IDE)中發(fā)現(xiàn) GDB 運(yùn)行在后臺(tái),包括 Qt Creator 和 VSCodium 的 本地調(diào)試擴(kuò)展。
了解如何充分利用 GDB 的功能很有用。一般情況下,并非所有 GDB 的功能都可以在 IDE 中使用,因此你可以從命令行使用 GDB 的經(jīng)驗(yàn)中受益。
via:
作者:Stephan Avenwedde選題:lkxed譯者:Maisie-x校對(duì):wxy
本文由 LCTT原創(chuàng)編譯,Linux中國(guó)榮譽(yù)推出
1.《手把手教你使用 GNU 調(diào)試器》援引自互聯(lián)網(wǎng),旨在傳遞更多網(wǎng)絡(luò)信息知識(shí),僅代表作者本人觀點(diǎn),與本網(wǎng)站無(wú)關(guān),侵刪請(qǐng)聯(lián)系頁(yè)腳下方聯(lián)系方式。
2.《手把手教你使用 GNU 調(diào)試器》僅供讀者參考,本網(wǎng)站未對(duì)該內(nèi)容進(jìn)行證實(shí),對(duì)其原創(chuàng)性、真實(shí)性、完整性、及時(shí)性不作任何保證。
3.文章轉(zhuǎn)載時(shí)請(qǐng)保留本站內(nèi)容來(lái)源地址,http://f99ss.com/gl/2997673.html