告別代碼Bug,GDB調(diào)試工具詳解
在軟件開(kāi)發(fā)的漫漫長(zhǎng)路上,Bug 就像隱藏在黑暗中的 “小怪獸”,時(shí)不時(shí)跳出來(lái)給開(kāi)發(fā)者們制造麻煩。曾經(jīng),歐洲航天局(ESA)首次發(fā)射阿麗亞娜 5 號(hào)火箭,這本是太空探索史上的重要時(shí)刻,卻因一行代碼導(dǎo)致災(zāi)難性故障,價(jià)值近 5 億歐元的火箭在發(fā)射 37 秒后爆炸 。經(jīng)過(guò)調(diào)查,原來(lái)是制導(dǎo)系統(tǒng)存在軟件缺陷,一段源于阿麗亞娜 4號(hào)的死代碼中,64 位浮點(diǎn)變量轉(zhuǎn)換為 16 位帶符號(hào)整數(shù)時(shí)出現(xiàn)整數(shù)溢出問(wèn)題,最終導(dǎo)致火箭自毀。這樣的故事告訴我們,一個(gè)看似不起眼的 Bug,可能會(huì)引發(fā)難以估量的后果。
當(dāng)我們?cè)陂_(kāi)發(fā)中遇到程序崩潰、結(jié)果異常等問(wèn)題時(shí),是不是常常感到無(wú)從下手?別擔(dān)心,今天要給大家介紹的 GDB 調(diào)試工具,就是我們戰(zhàn)勝這些 “小怪獸” 的有力武器,它能幫助我們深入程序內(nèi)部,揪出隱藏的 Bug,讓程序乖乖聽(tīng)話(huà)。
一、GDB是什么?
1.1GDB概述
GDB,全稱(chēng) GNU Debugger,是 GNU 開(kāi)源組織發(fā)布的一款功能強(qiáng)大的程序調(diào)試工具。自 1986 年由理查德?斯托曼(Richard Stallman)編寫(xiě)以來(lái),它不斷發(fā)展和完善,如今已成為 Linux 系統(tǒng)下調(diào)試程序的首選工具 ,在整個(gè) Linux 生態(tài)系統(tǒng)中占據(jù)著舉足輕重的地位。它就像是一位經(jīng)驗(yàn)豐富的偵探,深入程序的 “案發(fā)現(xiàn)場(chǎng)”,幫助開(kāi)發(fā)者們找到隱藏在代碼中的 “罪犯”——Bug。
GDB 支持多種編程語(yǔ)言,包括但不限于 C、C++、Fortran、Ada、Objective-C、Go、D 等,能夠與 GCC、Clang、LLVM 等一系列主流編譯器無(wú)縫集成。這意味著無(wú)論你使用哪種編程語(yǔ)言進(jìn)行開(kāi)發(fā),GDB 都能為你提供高效的調(diào)試支持,在桌面應(yīng)用程序、服務(wù)器端服務(wù),還是嵌入式系統(tǒng)的開(kāi)發(fā)中,都能以其強(qiáng)大的功能和靈活的交互方式,為開(kāi)發(fā)者提供無(wú)與倫比的調(diào)試體驗(yàn)。
- GDB官網(wǎng):https://www.gnu.org/software/gdb/(https://www.gnu.org/software/gdb/)
- GDB適用的編程語(yǔ)言: Ada / C / C++ / objective-c / Pascal 等。
- GDB的工作方式: 本地調(diào)試和遠(yuǎn)程調(diào)試。
目前release的最新版本為8.0,GDB可以運(yùn)行在Linux 和Windows 操作系統(tǒng)上。
1.2GDB 的優(yōu)勢(shì)
功能豐富:GDB 提供了全面的調(diào)試功能,如設(shè)置斷點(diǎn)(包括普通斷點(diǎn)、條件斷點(diǎn))、單步執(zhí)行(step 和 next )、查看變量值(print)、觀(guān)察內(nèi)存(x 命令)、回溯函數(shù)調(diào)用棧(backtrace)等。這些功能可以幫助開(kāi)發(fā)者深入分析程序的運(yùn)行狀態(tài),快速定位問(wèn)題。比如,在調(diào)試一個(gè)復(fù)雜的 C++ 程序時(shí),我們可以通過(guò)設(shè)置條件斷點(diǎn),當(dāng)某個(gè)變量滿(mǎn)足特定條件時(shí)程序暫停,從而精準(zhǔn)地捕捉到問(wèn)題出現(xiàn)的時(shí)刻;利用回溯函數(shù)調(diào)用棧,清晰地了解函數(shù)的調(diào)用順序和各層調(diào)用間的上下文關(guān)系,快速定位問(wèn)題發(fā)生在哪個(gè)函數(shù)調(diào)用鏈路中。
跨平臺(tái)支持:它支持廣泛的操作系統(tǒng)和平臺(tái),包括 Linux、Windows(通過(guò) MinGW 或 Cygwin)、macOS 以及多種嵌入式平臺(tái)(如 ARM、RISC-V 等)。無(wú)論你是在開(kāi)發(fā)桌面應(yīng)用、移動(dòng)應(yīng)用還是嵌入式系統(tǒng),GDB 都能發(fā)揮作用。在遠(yuǎn)程調(diào)試時(shí),GDB 非常靈活,可以與不同架構(gòu)的系統(tǒng)進(jìn)行連接,適用于跨平臺(tái)和多架構(gòu)的調(diào)試。比如,開(kāi)發(fā)一款同時(shí)在 Linux 和 Windows 系統(tǒng)上運(yùn)行的軟件,使用 GDB 可以在不同系統(tǒng)下進(jìn)行統(tǒng)一的調(diào)試操作,提高開(kāi)發(fā)效率。
強(qiáng)大的擴(kuò)展性:GDB 支持插件機(jī)制,可以通過(guò)安裝第三方插件增強(qiáng)其功能,如內(nèi)存分析、性能剖析、遠(yuǎn)程調(diào)試等。用戶(hù)還可以通過(guò) Python 腳本擴(kuò)展 GDB 的功能,進(jìn)行定制化調(diào)試操作。這對(duì)于需要在調(diào)試過(guò)程中進(jìn)行復(fù)雜計(jì)算或自動(dòng)化分析的場(chǎng)景非常有用。例如,在進(jìn)行大規(guī)模數(shù)據(jù)處理程序的調(diào)試時(shí),可以編寫(xiě) Python 腳本來(lái)自動(dòng)化分析程序運(yùn)行過(guò)程中產(chǎn)生的大量數(shù)據(jù),快速發(fā)現(xiàn)潛在問(wèn)題。
開(kāi)源免費(fèi):作為一款開(kāi)源軟件,GDB 擁有龐大的社區(qū)支持,開(kāi)發(fā)者們可以自由獲取、使用和修改它的源代碼。這不僅降低了開(kāi)發(fā)成本,還使得 GDB 能夠不斷吸收社區(qū)的智慧和力量,持續(xù)進(jìn)化和完善。同時(shí),豐富的社區(qū)資源,如文檔、教程、論壇等,也為開(kāi)發(fā)者們學(xué)習(xí)和使用 GDB 提供了便利。
與一些集成開(kāi)發(fā)環(huán)境(IDE)自帶的調(diào)試工具相比,GDB 雖然沒(méi)有華麗的圖形界面,但它更加輕量級(jí)、靈活,且無(wú)依賴(lài),不依賴(lài)于任何復(fù)雜的圖形界面或大型庫(kù),這使得它非常適合在資源受限的環(huán)境中使用,比如嵌入式開(kāi)發(fā) 。在服務(wù)器或遠(yuǎn)程開(kāi)發(fā)環(huán)境中,GDB 不需要圖形化界面,可以直接通過(guò) SSH 連接到目標(biāo)機(jī)器進(jìn)行調(diào)試。而且,GDB 能夠提供比大多數(shù) IDE 更低級(jí)別的控制和調(diào)試能力,例如,它可以直接操作內(nèi)存、寄存器,甚至直接修改程序的執(zhí)行流,這對(duì)于一些高級(jí)調(diào)試需求至關(guān)重要。
二、GDB基礎(chǔ)操作
2.1安裝與啟動(dòng)GDB
(1)安裝GDB
- gdb -v 檢查是否安裝成功,未安裝成功則安裝(必須確保編譯器已經(jīng)安裝,如 gcc) 。
- 啟動(dòng) gdb
- gdb test_file.exe 來(lái)啟動(dòng) gdb 調(diào)試, 即直接指定需要調(diào)試的可執(zhí)行文件名
- 直接輸入 gdb 啟動(dòng),進(jìn)入 gdb 之后采用命令 file test_file.exe 來(lái)指定文件名
- 如果目標(biāo)執(zhí)行文件要求出入?yún)?shù)(如 argv[] 接收參數(shù)),則可以通過(guò)三種方式指定參數(shù):
- 在啟動(dòng) gdb 時(shí),gdb --args text_file.exe
- 在進(jìn)入gdb 之后,運(yùn)行 set args param_1
- 在 進(jìn)入 gdb 調(diào)試以后,run param_1 或者 start para_1
(2)啟動(dòng) GDB
在使用 GDB 調(diào)試程序之前,我們需要先編譯程序并生成包含調(diào)試信息的可執(zhí)行文件。以 C 語(yǔ)言程序?yàn)槔?,使?GCC 編譯器時(shí),通過(guò)在編譯命令中添加 -g 參數(shù)來(lái)實(shí)現(xiàn),例如:
gcc -g -o my_program my_program.c
這樣生成的 my_program 可執(zhí)行文件就包含了調(diào)試所需的符號(hào)信息,這些符號(hào)信息就像是程序中的 “地圖標(biāo)記”,能夠幫助 GDB 在調(diào)試時(shí)準(zhǔn)確地定位到代碼中的變量、函數(shù)和行號(hào)等關(guān)鍵位置。
啟動(dòng) GDB 有多種方式,以下是幾種常見(jiàn)的方法:
①調(diào)試新程序:最直接的方式是在終端中輸入 gdb 加上可執(zhí)行文件名,例如:
gdb my_program
這種方式適用于我們需要從程序的初始狀態(tài)開(kāi)始調(diào)試,GDB 會(huì)加載程序的調(diào)試信息,并準(zhǔn)備好接受調(diào)試命令。
②附加到正在運(yùn)行的進(jìn)程:當(dāng)程序已經(jīng)在運(yùn)行,并且我們想要調(diào)試這個(gè)正在運(yùn)行的實(shí)例時(shí),可以使用 attach 命令。首先,通過(guò) ps -ef | grep my_program 命令獲取程序的進(jìn)程 ID(PID),然后使用以下命令將 GDB 附加到該進(jìn)程:
gdb
(gdb) attach <PID>
這種方式在程序出現(xiàn)運(yùn)行時(shí)錯(cuò)誤,需要在不重啟程序的情況下進(jìn)行調(diào)試時(shí)非常有用,它可以讓我們直接查看程序當(dāng)前的運(yùn)行狀態(tài),分析問(wèn)題出現(xiàn)的原因 。
③使用 core 文件調(diào)試:如果程序在運(yùn)行過(guò)程中崩潰并生成了 core 文件(系統(tǒng)默認(rèn)情況下可能不會(huì)生成 core 文件,需要通過(guò) ulimit -c unlimited 命令設(shè)置允許生成 core 文件 ),我們可以使用 GDB 加載 core 文件進(jìn)行調(diào)試。命令如下:
gdb my_program core
Core 文件就像是程序崩潰時(shí)的 “快照”,記錄了程序崩潰時(shí)的內(nèi)存狀態(tài)、寄存器值等關(guān)鍵信息,通過(guò)分析 core 文件,我們可以找到導(dǎo)致程序崩潰的原因,比如空指針引用、數(shù)組越界等問(wèn)題。
2.2gdb的使用
運(yùn)行程序
run(r)運(yùn)行程序,如果要加參數(shù),則是run arg1 arg2 ...
查看源代碼
list(l):查看最近十行源碼
list fun:查看fun函數(shù)源代碼
list file:fun:查看flie文件中的fun函數(shù)源代碼
設(shè)置斷點(diǎn)與觀(guān)察斷點(diǎn)
break 行號(hào)/fun設(shè)置斷點(diǎn)。
break file:行號(hào)/fun設(shè)置斷點(diǎn)。
break if<condition>:條件成立時(shí)程序停住。
info break(縮寫(xiě):i b):查看斷點(diǎn)。
watch expr:一旦expr值發(fā)生改變,程序停住。
delete n:刪除斷點(diǎn)。
單步調(diào)試
continue(c):運(yùn)行至下一個(gè)斷點(diǎn)。
step(s):?jiǎn)尾礁?,進(jìn)入函數(shù),類(lèi)似于VC中的step in。
next(n):?jiǎn)尾礁?,不進(jìn)入函數(shù),類(lèi)似于VC中的step out。
finish:運(yùn)行程序,知道當(dāng)前函數(shù)完成返回,并打印函數(shù)返回時(shí)的堆棧地址和返回值及參數(shù)值等信息。
until:當(dāng)厭倦了在一個(gè)循環(huán)體內(nèi)單步跟蹤時(shí),這個(gè)命令可以運(yùn)行程序知道退出循環(huán)體。
查看運(yùn)行時(shí)數(shù)據(jù)
print(p):查看運(yùn)行時(shí)的變量以及表達(dá)式。
ptype:查看類(lèi)型。
print array:打印數(shù)組所有元素。
print *array@len:查看動(dòng)態(tài)內(nèi)存。len是查看數(shù)組array的元素個(gè)數(shù)。
print x=5:改變運(yùn)行時(shí)數(shù)據(jù)。
2.3常用命令詳解
⑴設(shè)置斷點(diǎn)(break):斷點(diǎn)是調(diào)試中最常用的工具之一,它就像是在程序的執(zhí)行路徑上設(shè)置的 “路障”,當(dāng)程序執(zhí)行到斷點(diǎn)處時(shí)會(huì)暫停,以便我們檢查程序的狀態(tài)。設(shè)置斷點(diǎn)的基本命令是 break,可以簡(jiǎn)寫(xiě)為 b。例如,要在 main 函數(shù)的入口處設(shè)置斷點(diǎn),可以使用以下命令:
(gdb) b main
也可以在指定行號(hào)處設(shè)置斷點(diǎn),假設(shè)我們的代碼文件是 my_program.c,要在第 20 行設(shè)置斷點(diǎn),可以這樣操作:
(gdb) b my_program.c:20
此外,還可以設(shè)置條件斷點(diǎn),只有當(dāng)條件滿(mǎn)足時(shí),斷點(diǎn)才會(huì)生效。比如,當(dāng)變量 i 的值等于 10 時(shí)暫停程序:
(gdb) b my_program.c:30 if i == 10
⑵運(yùn)行程序(run):設(shè)置好斷點(diǎn)后,使用 run 命令(簡(jiǎn)寫(xiě)為 r)來(lái)啟動(dòng)程序。如果程序需要傳入命令行參數(shù),可以在 run 命令后面直接添加參數(shù),例如:
(gdb) run arg1 arg2
run 命令會(huì)使程序從起始位置開(kāi)始執(zhí)行,直到遇到第一個(gè)斷點(diǎn)或者程序結(jié)束。
⑶繼續(xù)運(yùn)行(continue):當(dāng)程序在斷點(diǎn)處暫停后,如果我們想讓程序繼續(xù)執(zhí)行,直到下一個(gè)斷點(diǎn)或程序結(jié)束,可以使用 continue 命令,簡(jiǎn)寫(xiě)為 c:
(gdb) c
這個(gè)命令非常實(shí)用,在我們檢查完當(dāng)前斷點(diǎn)處的程序狀態(tài)后,繼續(xù)程序的執(zhí)行,以觀(guān)察后續(xù)的運(yùn)行情況。
⑷單步執(zhí)行(next、step):
next:next 命令(簡(jiǎn)寫(xiě)為 n)用于單步執(zhí)行程序,每次執(zhí)行一行代碼,但當(dāng)遇到函數(shù)調(diào)用時(shí),不會(huì)進(jìn)入函數(shù)內(nèi)部,而是將函數(shù)調(diào)用視為一行代碼直接執(zhí)行過(guò)去。例如:
(gdb) n
假設(shè)我們有一個(gè)函數(shù)調(diào)用 result = add_numbers(a, b),使用 next 命令會(huì)直接執(zhí)行完這個(gè)函數(shù)調(diào)用,并停在下一行代碼,而不會(huì)進(jìn)入 add_numbers 函數(shù)內(nèi)部查看其執(zhí)行過(guò)程。
step:step 命令(簡(jiǎn)寫(xiě)為 s)同樣是單步執(zhí)行,但當(dāng)遇到函數(shù)調(diào)用時(shí),會(huì)進(jìn)入函數(shù)內(nèi)部,在函數(shù)的第一行代碼處暫停。比如:
(gdb) s
使用 step 命令遇到上述的 add_numbers 函數(shù)調(diào)用時(shí),會(huì)進(jìn)入 add_numbers 函數(shù)內(nèi)部,方便我們查看函數(shù)內(nèi)部的執(zhí)行邏輯,檢查每一步的變量變化和計(jì)算結(jié)果,對(duì)于調(diào)試函數(shù)內(nèi)部的問(wèn)題非常有效。
⑸ 打印變量值(print):在調(diào)試過(guò)程中,我們經(jīng)常需要查看變量的值,這時(shí)就可以使用 print 命令(簡(jiǎn)寫(xiě)為 p)。例如,要查看變量 i 的值,可以使用以下命令:
(gdb) p i
如果變量是一個(gè)復(fù)雜的數(shù)據(jù)結(jié)構(gòu),比如結(jié)構(gòu)體或?qū)ο?,print 命令也能完整地顯示其成員信息。此外,還可以對(duì)表達(dá)式進(jìn)行求值,例如:
(gdb) p a + b
這條命令會(huì)計(jì)算 a + b 的值并顯示出來(lái)。
⑹查看斷點(diǎn)信息(info break):使用 info break 命令(簡(jiǎn)寫(xiě)為 i b)可以查看當(dāng)前設(shè)置的所有斷點(diǎn)的信息,包括斷點(diǎn)的編號(hào)、位置、條件等。例如:
(gdb) i b
Num Type Disp Enb Address What
1 breakpoint keep y 0x00000000004005c8 in main at my_program.c:10
breakpoint already hit 1 time
2 breakpoint keep y 0x00000000004005e0 in main at my_program.c:20 if i == 10
通過(guò)這些信息,我們可以清楚地了解斷點(diǎn)的設(shè)置情況,方便對(duì)斷點(diǎn)進(jìn)行管理,比如刪除或禁用某些斷點(diǎn)。
2.4程序錯(cuò)誤
- 編譯錯(cuò):編寫(xiě)程序的時(shí)候沒(méi)有符合語(yǔ)言規(guī)范導(dǎo)致編譯錯(cuò)誤。比如:語(yǔ)法錯(cuò)誤。
- 運(yùn)行時(shí)錯(cuò)誤:編譯器檢查不出這種錯(cuò)誤,但在運(yùn)行時(shí)候可能會(huì)導(dǎo)致程序崩潰。比如:內(nèi)存地址非法訪(fǎng)問(wèn)。
- 邏輯錯(cuò)誤:編譯和運(yùn)行都很順利,但是程序沒(méi)有干我們期望干的事情。
2.5gdb調(diào)試段錯(cuò)誤
什么是段錯(cuò)誤?段錯(cuò)誤是由于訪(fǎng)問(wèn)非法地址而產(chǎn)生的錯(cuò)誤。
- 訪(fǎng)問(wèn)系統(tǒng)數(shù)據(jù)區(qū),尤其是往系統(tǒng)保護(hù)的內(nèi)存地址寫(xiě)數(shù)據(jù)。比如:訪(fǎng)問(wèn)地址為0的地址。
- 內(nèi)存越界(數(shù)組越界,變量類(lèi)型不一致等)訪(fǎng)問(wèn)到不屬于當(dāng)前程序的內(nèi)存區(qū)域。
gdb調(diào)試段錯(cuò)誤,可以直接運(yùn)行程序,當(dāng)程序運(yùn)行崩潰后,gdb會(huì)打印運(yùn)行的信息,比如:收到了SIGSEGV信號(hào),然后可以使用bt
命令,打印?;厮菪畔?,然后根據(jù)程序發(fā)生錯(cuò)誤的代碼,修改程序。
2.6.core文件調(diào)試
(1)core文件
在程序崩潰時(shí),一般會(huì)生成一個(gè)文件叫core
文件。core文件記錄的是程序崩潰時(shí)的內(nèi)存映像,并加入調(diào)試信息,core文件生成過(guò)程叫做core dump(核心已轉(zhuǎn)儲(chǔ))
。系統(tǒng)默認(rèn)不會(huì)生成該文件。
(2)設(shè)置生成core文件
- ulimit -c:查看core-dump狀態(tài)。
- ulimit -c xxxx:設(shè)置core文件的大小。
- ulimit -c unlimited:core文件無(wú)限制大小。
(3)gdb調(diào)試core文件
當(dāng)設(shè)置完ulimit -c xxxx
后,再次運(yùn)行程序發(fā)生段錯(cuò)誤,此時(shí)就會(huì)生成一個(gè)core
文件,使用gdb core
調(diào)試core文件,使用bt
命令打印?;厮菪畔?。
三、GDB調(diào)試程序用法
一般來(lái)說(shuō),GDB主要幫忙你完成下面四個(gè)方面的功能:
啟動(dòng)你的程序,可以按照你的自定義的要求隨心所欲的運(yùn)行程序。2、可讓被調(diào)試的程序在你所指定的調(diào)置的斷點(diǎn)處停住。(斷點(diǎn)可以是條件表達(dá)式)3、當(dāng)程序被停住時(shí),可以檢查此時(shí)你的程序中所發(fā)生的事。4、動(dòng)態(tài)的改變你程序的執(zhí)行環(huán)境。
從上面看來(lái),GDB和一般的調(diào)試工具沒(méi)有什么兩樣,基本上也是完成這些功能,不過(guò)在細(xì)節(jié)上,你會(huì)發(fā)現(xiàn)GDB這個(gè)調(diào)試工具的強(qiáng)大,大家可能比較習(xí)慣了圖形化的調(diào)試工具,但有時(shí)候,命令行的調(diào)試工具卻有著圖形化工具所不能完成的功能。讓我們一一看來(lái)。
一個(gè)調(diào)試示例:
源程序:tst.c
1 #include <stdio.h>
2
3 int func(int n)
4 {
5 int sum=0,i;
6 for(i=0; i<n; i++)
7 {
8 sum+=i;
9 }
10 return sum;
11 }
12
13
14 main()
15 {
16 int i;
17 long result = 0;
18 for(i=1; i<=100; i++)
19 {
20 result += i;
21 }
22
23 printf("result[1-100] = %d /n", result );
24 printf("result[1-250] = %d /n", func(250) );
25 }
編譯生成執(zhí)行文件:(Linux下)
hchen/test> cc -g tst.c -o tst
使用GDB調(diào)試:
hchen/test> gdb tst <---------- 啟動(dòng)GDB
GNU gdb 5.1.1
Copyright 2002 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i386-SUSE-linux"...
(gdb) l <-------------------- l命令相當(dāng)于list,從第一行開(kāi)始例出原碼。
1 #include <stdio.h>
2
3 int func(int n)
4 {
5 int sum=0,i;
6 for(i=0; i<n; i++)
7 {
8 sum+=i;
9 }
10 return sum;
(gdb) <-------------------- 直接回車(chē)表示,重復(fù)上一次命令
11 }
12
13
14 main()
15 {
16 int i;
17 long result = 0;
18 for(i=1; i<=100; i++)
19 {
20 result += i;
(gdb) break 16 <-------------------- 設(shè)置斷點(diǎn),在源程序第16行處。
Breakpoint 1 at 0x8048496: file tst.c, line 16.
(gdb) break func <-------------------- 設(shè)置斷點(diǎn),在函數(shù)func()入口處。
Breakpoint 2 at 0x8048456: file tst.c, line 5.
(gdb) info break <-------------------- 查看斷點(diǎn)信息。
Num Type Disp Enb Address What
1 breakpoint keep y 0x08048496 in main at tst.c:16
2 breakpoint keep y 0x08048456 in func at tst.c:5
(gdb) r <--------------------- 運(yùn)行程序,run命令簡(jiǎn)寫(xiě)
Starting program: /home/hchen/test/tst
Breakpoint 1, main () at tst.c:17 <---------- 在斷點(diǎn)處停住。
17 long result = 0;
(gdb) n <--------------------- 單條語(yǔ)句執(zhí)行,next命令簡(jiǎn)寫(xiě)。
18 for(i=1; i<=100; i++)
(gdb) n
20 result += i;
(gdb) n
18 for(i=1; i<=100; i++)
(gdb) n
20 result += i;
(gdb) c <--------------------- 繼續(xù)運(yùn)行程序,continue命令簡(jiǎn)寫(xiě)。
Continuing.
result[1-100] = 5050 <----------程序輸出。
Breakpoint 2, func (n=250) at tst.c:5
5 int sum=0,i;
(gdb) n
6 for(i=1; i<=n; i++)
(gdb) p i <--------------------- 打印變量i的值,print命令簡(jiǎn)寫(xiě)。
$1 = 134513808
(gdb) n
8 sum+=i;
(gdb) n
6 for(i=1; i<=n; i++)
(gdb) p sum
$2 = 1
(gdb) n
8 sum+=i;
(gdb) p i
$3 = 2
(gdb) n
6 for(i=1; i<=n; i++)
(gdb) p sum
$4 = 3
(gdb) bt <--------------------- 查看函數(shù)堆棧。
#0 func (n=250) at tst.c:5
#1 0x080484e4 in main () at tst.c:24
#2 0x400409ed in __libc_start_main () from /lib/libc.so.6
(gdb) finish <--------------------- 退出函數(shù)。
Run till exit from #0 func (n=250) at tst.c:5
0x080484e4 in main () at tst.c:24
24 printf("result[1-250] = %d /n", func(250) );
Value returned is $6 = 31375
(gdb) c <--------------------- 繼續(xù)運(yùn)行。
Continuing.
result[1-250] = 31375 <----------程序輸出。
Program exited with code 027. <--------程序退出,調(diào)試結(jié)束。
(gdb) q <--------------------- 退出gdb。
hchen/test>
好了,有了以上的感性認(rèn)識(shí),還是讓我們來(lái)系統(tǒng)地認(rèn)識(shí)一下gdb吧。
基本gdb命令:
GDB常用命令 格式 含義 簡(jiǎn)寫(xiě)
list List [開(kāi)始,結(jié)束] 列出文件的代碼清單 l
prit Print 變量名 打印變量?jī)?nèi)容 p
break Break [行號(hào)或函數(shù)名] 設(shè)置斷點(diǎn) b
continue Continue [開(kāi)始,結(jié)束] 繼續(xù)運(yùn)行 c
info Info 變量名 列出信息 i
next Next 下一行 n
step Step 進(jìn)入函數(shù)(步入) S
display Display 變量名 顯示參數(shù)
file File 文件名(可以是絕對(duì)路徑和相對(duì)路徑) 加載文件
run Run args 運(yùn)行程序 r
四、GDB進(jìn)階功能
4.1回溯追蹤(backtrace)
在程序調(diào)試過(guò)程中,了解函數(shù)調(diào)用順序及各層調(diào)用間的上下文關(guān)系至關(guān)重要。有時(shí)候程序出現(xiàn)錯(cuò)誤,但我們并不知道錯(cuò)誤是在哪個(gè)函數(shù)調(diào)用鏈路中產(chǎn)生的,這時(shí)候回溯追蹤功能就派上用場(chǎng)了。GDB 提供了backtrace命令,簡(jiǎn)寫(xiě)為bt,用于展示當(dāng)前的調(diào)用棧信息。
當(dāng)程序運(yùn)行出現(xiàn)異常或者在斷點(diǎn)處暫停時(shí),輸入bt命令,GDB 會(huì)按深度由淺至深列出各個(gè)棧幀,每個(gè)棧幀包含了函數(shù)名、源文件名、行號(hào)及參數(shù)值等關(guān)鍵信息。例如,我們有一個(gè)包含多個(gè)函數(shù)調(diào)用的程序:
#include <stdio.h>
void function_c(int num) {
int result = num * 2;
printf("Function C: result = %d\n", result);
}
void function_b(int num) {
function_c(num + 1);
}
void function_a() {
int num = 5;
function_b(num);
}
int main() {
function_a();
return 0;
}
在 GDB 中調(diào)試這個(gè)程序,當(dāng)程序在function_c函數(shù)內(nèi)暫停時(shí),輸入bt命令,輸出結(jié)果可能如下:
(gdb) bt
#0 function_c (num=6) at test.c:5
#1 0x000000000040056d in function_b (num=5) at test.c:9
#2 0x0000000000400588 in function_a () at test.c:13
#3 0x00000000004005a4 in main () at test.c:17
從輸出中可以清晰地看到函數(shù)的調(diào)用順序:main調(diào)用function_a,function_a調(diào)用function_b,function_b調(diào)用function_c,并且還能看到每個(gè)函數(shù)調(diào)用時(shí)的參數(shù)值 。這對(duì)于我們快速定位問(wèn)題發(fā)生的位置非常有幫助,比如如果function_c中出現(xiàn)了除零錯(cuò)誤,我們就可以通過(guò)回溯追蹤信息,從調(diào)用鏈路上查找傳入function_c的參數(shù)是如何計(jì)算得出的,進(jìn)而找到問(wèn)題的根源。
4.2動(dòng)態(tài)內(nèi)存檢測(cè)
內(nèi)存泄漏、非法訪(fǎng)問(wèn)等內(nèi)存問(wèn)題是程序健壯性的隱形殺手,它們可能會(huì)導(dǎo)致程序運(yùn)行一段時(shí)間后出現(xiàn)性能下降甚至崩潰。雖然有像 Valgrind 這樣專(zhuān)門(mén)的內(nèi)存分析工具,但 GDB 自身也具備一定的內(nèi)存檢測(cè)能力,尤其是結(jié)合 heap 插件,可以對(duì)程序的堆內(nèi)存使用情況進(jìn)行初步排查。
首先,我們需要獲取并加載 heap 插件,假設(shè)插件文件為gdbheap.py,使用以下命令加載插件:
(gdb) source /path/to/gdbheap.py
然后,我們可以將 GDB 附加到正在運(yùn)行的進(jìn)程上(假設(shè)進(jìn)程 ID 為<pid>),并使用插件提供的命令來(lái)查看堆內(nèi)存分配情況:
(gdb) attach <pid>
(gdb) monitor heap
執(zhí)行上述命令后,GDB 會(huì)顯示堆內(nèi)存的相關(guān)信息,比如內(nèi)存塊的數(shù)量、大小、分配狀態(tài)等。通過(guò)觀(guān)察這些信息,我們可以發(fā)現(xiàn)一些潛在的內(nèi)存問(wèn)題。例如,如果發(fā)現(xiàn)有大量的小內(nèi)存塊被分配且長(zhǎng)時(shí)間沒(méi)有釋放,可能存在內(nèi)存泄漏的風(fēng)險(xiǎn);如果看到內(nèi)存塊的分配和釋放順序異常,可能存在非法內(nèi)存訪(fǎng)問(wèn)的問(wèn)題。
下面是一個(gè)簡(jiǎn)單的示例,展示如何使用 GDB 和 heap 插件檢測(cè)內(nèi)存問(wèn)題:
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr1 = (int *)malloc(10 * sizeof(int));
int *ptr2 = (int *)malloc(20 * sizeof(int));
free(ptr1);
// 故意不釋放ptr2,制造內(nèi)存泄漏
return 0;
}
在程序運(yùn)行后,使用 GDB 和 heap 插件進(jìn)行檢測(cè),通過(guò)分析插件輸出的堆內(nèi)存信息,我們就有可能發(fā)現(xiàn)ptr2所指向的內(nèi)存沒(méi)有被釋放,從而定位到內(nèi)存泄漏問(wèn)題。
4.3條件斷點(diǎn)與觀(guān)察點(diǎn)
條件斷點(diǎn):在一些復(fù)雜的程序中,我們可能不希望程序在每個(gè)斷點(diǎn)處都暫停,而是希望當(dāng)滿(mǎn)足特定條件時(shí)才暫停程序執(zhí)行,這時(shí)候就可以使用條件斷點(diǎn)。例如,在一個(gè)處理數(shù)組的程序中,我們懷疑當(dāng)數(shù)組下標(biāo)i大于數(shù)組大小時(shí)會(huì)出現(xiàn)數(shù)組越界問(wèn)題,我們可以設(shè)置如下條件斷點(diǎn):
(gdb) break array_processing_function if i >= array_size
這樣,只有當(dāng)i大于或等于array_size時(shí),程序才會(huì)在array_processing_function處暫停,大大提高了調(diào)試效率,避免了在無(wú)關(guān)斷點(diǎn)處頻繁暫停程序,讓我們能夠更精準(zhǔn)地捕捉到問(wèn)題出現(xiàn)的時(shí)刻 。
觀(guān)察點(diǎn):觀(guān)察點(diǎn)(Watchpoint)用于監(jiān)控變量值的變化。當(dāng)觀(guān)察的變量被修改時(shí),GDB 會(huì)自動(dòng)暫停程序,這對(duì)于追蹤難以復(fù)現(xiàn)的偶發(fā)問(wèn)題尤為有用。比如,在一個(gè)多線(xiàn)程程序中,某個(gè)全局變量的值被意外修改,但我們不確定是哪個(gè)線(xiàn)程在什么情況下修改的,就可以為這個(gè)全局變量設(shè)置觀(guān)察點(diǎn):
(gdb) watch global_variable
當(dāng)global_variable的值發(fā)生改變時(shí),程序會(huì)立即暫停,此時(shí)我們可以查看當(dāng)前的線(xiàn)程狀態(tài)、調(diào)用棧等信息,來(lái)確定變量是如何被修改的,從而找到問(wèn)題的根源。此外,還可以設(shè)置讀觀(guān)察點(diǎn)(rwatch)和讀寫(xiě)觀(guān)察點(diǎn)(awatch),rwatch在變量被讀取時(shí)暫停程序,awatch在變量被讀取或修改時(shí)暫停程序,根據(jù)具體的調(diào)試需求選擇合適的觀(guān)察點(diǎn)類(lèi)型 。
4.4遠(yuǎn)程調(diào)試
在實(shí)際開(kāi)發(fā)中,我們經(jīng)常會(huì)遇到需要調(diào)試部署在遠(yuǎn)程服務(wù)器或嵌入式設(shè)備上的程序的情況,GDB 支持通過(guò)網(wǎng)絡(luò)進(jìn)行遠(yuǎn)程調(diào)試,這極大地簡(jiǎn)化了跨設(shè)備調(diào)試的復(fù)雜性。
遠(yuǎn)程調(diào)試的基本原理是在遠(yuǎn)程設(shè)備上運(yùn)行 GDB 的服務(wù)器端(gdbserver),并在本地 GDB 客戶(hù)端連接至服務(wù)器端。具體操作步驟如下:
⑴在遠(yuǎn)程設(shè)備上:首先確保遠(yuǎn)程設(shè)備上安裝了gdbserver,可以通過(guò)gdbserver --version命令檢查是否安裝。然后啟動(dòng)gdbserver,并指定調(diào)試的程序和監(jiān)聽(tīng)端口,例如:
gdbserver :<port> /path/to/remote_program
其中<port>是未被占用的端口號(hào),可以根據(jù)實(shí)際情況任意指定,/path/to/remote_program是要調(diào)試的程序路徑。啟動(dòng)成功后,gdbserver會(huì)監(jiān)聽(tīng)指定端口,等待本地 GDB 客戶(hù)端連接。
⑵在本地 GDB 客戶(hù)端:在本地啟動(dòng) GDB,并加載本地保存的與遠(yuǎn)程程序相同的可執(zhí)行文件副本(確保編譯時(shí)帶有調(diào)試信息),然后使用target remote命令連接到遠(yuǎn)程gdbserver:
gdb ./local_program
(gdb) target remote <remote_host>:<port>
<remote_host>是遠(yuǎn)程設(shè)備的 IP 地址或主機(jī)名,<port>是在遠(yuǎn)程設(shè)備上啟動(dòng)gdbserver時(shí)指定的端口號(hào)。連接成功后,就可以像在本地調(diào)試程序一樣,在本地 GDB 客戶(hù)端使用各種調(diào)試命令,如設(shè)置斷點(diǎn)、單步執(zhí)行、查看變量值等,GDB 會(huì)通過(guò)網(wǎng)絡(luò)與遠(yuǎn)程gdbserver通信,實(shí)現(xiàn)對(duì)遠(yuǎn)程程序的調(diào)試 。
例如,在開(kāi)發(fā)一款嵌入式系統(tǒng)程序時(shí),我們可以在開(kāi)發(fā)板(遠(yuǎn)程設(shè)備)上運(yùn)行g(shù)dbserver,在本地 PC 上使用 GDB 客戶(hù)端進(jìn)行調(diào)試,通過(guò)這種方式,能夠在本地環(huán)境中方便地調(diào)試運(yùn)行在遠(yuǎn)程嵌入式設(shè)備上的程序,提高開(kāi)發(fā)效率 。
五、實(shí)戰(zhàn)技巧
5.1利用 TUI 模式提升效率
GDB 的 TUI(Terminal User Interface,終端用戶(hù)界面)模式提供了一種基于文本交互和圖形用戶(hù)交互之間的折中方法,在調(diào)試過(guò)程中能顯著提升效率。在 TUI 模式中,GDB 將終端屏幕劃分為源文本窗口和控制臺(tái)窗口,讓我們可以直觀(guān)地看到代碼的執(zhí)行情況 。
啟動(dòng) TUI 模式非常簡(jiǎn)單,只需在啟動(dòng) GDB 時(shí)加上 -tui 參數(shù)即可。例如:
gdb -tui my_program
如果已經(jīng)在普通 GDB 模式下,還可以通過(guò)快捷鍵 Ctrl + X + A 來(lái)切換到 TUI 模式,再次按下該快捷鍵則可以返回普通模式。
進(jìn)入 TUI 模式后,我們可以使用一系列快捷鍵和命令來(lái)進(jìn)行調(diào)試操作。比如,使用 n(next)命令單步執(zhí)行代碼時(shí),源文本窗口會(huì)實(shí)時(shí)高亮顯示當(dāng)前執(zhí)行的代碼行,同時(shí)控制臺(tái)窗口會(huì)輸出執(zhí)行結(jié)果;設(shè)置斷點(diǎn)時(shí),斷點(diǎn)所在的行號(hào)前會(huì)顯示特殊標(biāo)記,方便我們識(shí)別和管理斷點(diǎn)。
在調(diào)試一個(gè)復(fù)雜的 C++ 項(xiàng)目時(shí),我需要在多個(gè)函數(shù)之間來(lái)回切換查看代碼執(zhí)行邏輯,TUI 模式讓我可以直接在源文本窗口中清晰地看到代碼的上下文關(guān)系,配合單步執(zhí)行和斷點(diǎn)設(shè)置,快速定位到了程序中的邏輯錯(cuò)誤。而且,當(dāng)程序暫停時(shí),按下 Ctrl + X + S 快捷鍵后,就可以直接使用 GDB 命令,而無(wú)需每次都回車(chē)確認(rèn),進(jìn)一步提高了調(diào)試效率 。
5.2自定義命令與腳本自動(dòng)化
在日常調(diào)試工作中,我們經(jīng)常會(huì)重復(fù)執(zhí)行一些相同的命令序列,比如每次調(diào)試時(shí)都需要設(shè)置相同的斷點(diǎn)、查看特定變量的值等。為了提高調(diào)試效率,GDB 允許我們將這些常用命令定義成自定義命令或腳本。
自定義命令的定義格式如下:
define command_name
statement1
statement2
...
end
其中,command_name 是自定義命令的名稱(chēng),statement 是具體的 GDB 命令。例如,我們可以定義一個(gè)名為 my_debug 的自定義命令,用于設(shè)置多個(gè)斷點(diǎn)并啟動(dòng)程序:
define my_debug
b main
b function_a
b function_b
r
end
定義好自定義命令后,在 GDB 中直接輸入命令名即可執(zhí)行這些命令。
對(duì)于更復(fù)雜的操作,我們可以編寫(xiě) GDB 腳本。GDB 腳本是一個(gè)包含一系列 GDB 命令的文本文件,其擴(kuò)展名為 .gdb 。例如,我們創(chuàng)建一個(gè)名為 debug_script.gdb 的腳本文件,內(nèi)容如下:
b main
b function_c if i > 10
r
在 GDB 中使用 source 命令加載腳本:
(gdb) source debug_script.gdb
這樣,腳本中的命令就會(huì)依次執(zhí)行。GDB 還提供了豐富的流程控制命令,如 if...else...end、while...end 等,結(jié)合這些命令,我們可以編寫(xiě)功能強(qiáng)大的自動(dòng)化調(diào)試腳本,實(shí)現(xiàn)復(fù)雜的調(diào)試邏輯。在調(diào)試一個(gè)大型數(shù)據(jù)庫(kù)應(yīng)用程序時(shí),我編寫(xiě)了一個(gè)腳本,通過(guò)循環(huán)遍歷數(shù)據(jù)庫(kù)連接池中的連接對(duì)象,檢查每個(gè)連接的狀態(tài)和屬性,快速發(fā)現(xiàn)了連接泄漏和配置錯(cuò)誤的問(wèn)題,大大節(jié)省了調(diào)試時(shí)間 。
5.3配合 IDE 使用
雖然 GDB 本身是一個(gè)強(qiáng)大的命令行調(diào)試工具,但它也可以與一些集成開(kāi)發(fā)環(huán)境(IDE)配合使用,充分發(fā)揮兩者的優(yōu)勢(shì)。以 Eclipse CDT 為例,它提供了直觀(guān)的圖形化界面,方便我們進(jìn)行代碼編輯、項(xiàng)目管理和調(diào)試操作,同時(shí)又集成了 GDB 的強(qiáng)大調(diào)試功能。
在 Eclipse CDT 中使用 GDB 進(jìn)行調(diào)試,首先需要?jiǎng)?chuàng)建一個(gè) C/C++ 項(xiàng)目,并確保項(xiàng)目的編譯設(shè)置中包含調(diào)試信息(通常在項(xiàng)目屬性的 C/C++ Build - Settings 中,選擇 Debug 配置,勾選 Generate debug info 選項(xiàng))。然后,在項(xiàng)目的源代碼中設(shè)置斷點(diǎn),點(diǎn)擊 Eclipse 工具欄上的調(diào)試按鈕,Eclipse 會(huì)自動(dòng)啟動(dòng) GDB,并將其與項(xiàng)目關(guān)聯(lián)起來(lái)。
在調(diào)試過(guò)程中,我們可以在 Eclipse 的調(diào)試視圖中查看變量值、調(diào)用棧信息,進(jìn)行單步執(zhí)行、繼續(xù)執(zhí)行等操作,這些操作都通過(guò) Eclipse 的圖形界面完成,但底層實(shí)際上是由 GDB 來(lái)執(zhí)行的。這種方式既保留了 GDB 的強(qiáng)大功能,又提供了更加便捷和直觀(guān)的調(diào)試體驗(yàn),對(duì)于新手開(kāi)發(fā)者來(lái)說(shuō)尤其友好,能夠快速上手調(diào)試工作。