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í)行行。你可以輸入 nextn)命令逐行執(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 srcbacktrace來(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ō)就是:

  1. 假設(shè)你使用的是最新版本的 Fedora
  2. 使用 -c1開(kāi)關(guān)調(diào)用 coredump:coredump -c1
  1. 使用 GDB 加載最新的轉(zhuǎn)儲(chǔ)文件:coredumpctl debug
  2. 打開(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、fu> addr >

可選參數(shù):

  • n:?jiǎn)卧笮〉闹貜?fù)計(jì)數(shù)(默認(rèn)值:1)
  • f:格式說(shuō)明符,如printf
  • u:?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ù)前言:

  1. 調(diào)用函數(shù)的基指針(rbp)存放在棧上
  2. 棧指針(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