新聞中心
如果你是一個程序員,想在你的軟件增加某些功能,你首先考慮實現(xiàn)它的方法:例如寫一個方法、定義一個類,或者創(chuàng)建新的數(shù)據(jù)類型。然后你用編譯器或解釋器可以理解的編程語言來實現(xiàn)這個功能。但是,如果你覺得你所有代碼都正確,但是編譯器或解釋器依然無法理解你的指令怎么辦?如果軟件大多數(shù)情況下都運行良好,但是在某些環(huán)境下出現(xiàn)缺陷怎么辦?這種情況下,你得知道如何正確使用調(diào)試器找到問題的根源。

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


咨詢
建站咨詢
