米哈游C++二面:如何使用gdb調試無符號的可執行程序?
在軟件開發的漫漫征程中,調試可謂是開發者手中最為關鍵的武器之一。而調試無符號可執行程序,更是在諸多場景下發揮著不可或缺的重要作用。在軟件開發場景里,無符號可執行程序常常源于一些特殊的編譯選項或者第三方庫的使用 。想象一下,你正在參與一個大型項目,其中部分代碼由第三方提供,當程序出現異常時,由于沒有符號表,你仿佛置身于一片黑暗森林,每一個函數調用、每一個變量變化都變得難以追蹤。但通過調試無符號可執行程序,你可以深入這片黑暗,逐步揭開程序運行的神秘面紗,找到問題的根源所在,讓代碼回歸正軌。
再把目光投向安全分析領域,無符號可執行程序往往與惡意軟件、逆向工程緊密相連。當安全研究人員面對一個無符號的惡意程序時,調試就成為了剖析其行為、了解其攻擊機制的關鍵手段。通過調試,他們能夠觀察程序在運行時的一舉一動,分析它如何竊取信息、如何傳播,進而制定出有效的防御策略,保護系統和用戶的安全。既然調試無符號可執行程序如此重要,那么如何使用 GDB 這一強大的調試工具來完成這項任務呢?接下來,就讓我們一步步走進 GDB 調試無符號可執行程序的奇妙世界。
一、面試問題解讀
問題:如何使用gdb調試不帶調試信息的可執行程序?
解讀思路:使用gdb調試不帶調試信息的可執行程序時,雖然不能直接使用變量名和函數名等符號信息,但仍可進行基本的調試操作。首先,確保安裝了gdb調試工具。然后,啟動gdb并加載可執行程序。在gdb中,可以使用break命令設置斷點(基于地址或函數偏移量),run命令運行程序,next和step命令單步執行,print命令查看內存或寄存器內容。盡管不方便,但通過細心觀察和內存分析,仍然可以追蹤程序執行和定位問題。
以下是步驟和示例:
(1)使用gcc或clang等編譯器,在編譯程序時加上-g選項來生成調試信息。
gcc -g -o program program.c(2)使用gdb來調試你的程序:
gdb program(3)在GDB中,你可以設置斷點、查看變量值、單步執行等。
例如,設置斷點:
(gdb) break 10開始執行:
(gdb) run單步執行:
(gdb) step查看變量值:
(gdb) print variable_name請注意,沒有調試信息,GDB的功能將受到限制,例如無法單步執行、打印變量值等。
二、GDB 基礎入門
2.1 GDB 是什么
GDB,即 GNU Debugger,是 GNU 開源組織發布的一款功能強大的程序調試工具,堪稱調試領域的中流砥柱 。自 1986 年誕生以來,它不斷發展和完善,如今已成為眾多開發者在調試程序時的首選。
- GDB官網:https://www.gnu.org/software/gdb/(https://www.gnu.org/software/gdb/)
- GDB適用的編程語言:Ada / C / C++ / objective-c / Pascal 等。
- GDB的工作方式:本地調試和遠程調試。
GDB 就像是程序世界里的超級偵探,支持 C、C++、Fortran、Ada、Objective-C、Go、D 等多種編程語言。它能夠深入程序內部,通過設置斷點、單步執行、查看變量值、觀察內存等操作,幫助開發者清晰地了解程序的運行狀態,精準定位程序中的錯誤,將隱藏在代碼深處的 Bug 一一揪出。無論是桌面應用程序、服務器端服務,還是嵌入式系統的開發,GDB 都能憑借其強大的功能和靈活的交互方式,為開發者提供無與倫比的調試體驗。 它的出現,極大地提高了軟件開發的效率和質量,讓開發者在與 Bug 的斗爭中更加得心應手。
2.2 GDB 的安裝與啟動
GDB 在不同系統下的安裝方式各有特點。在 Linux 系統中,如果你使用的是基于 Debian 或 Ubuntu 的發行版,安裝 GDB 就像從超市貨架上拿下一件商品一樣簡單,只需在終端中輸入 “sudo apt -y install gdb”,系統就會自動幫你完成安裝。而對于使用 RedHat、CentOS、Fedora 等 RedHat 系列發行版的用戶,在終端中執行 “sudo yum -y install gdb” 命令,就能輕松將 GDB 收入囊中。
在 macOS 系統下,你可以借助 Homebrew 這個強大的包管理器來安裝 GDB。首先確保你已經安裝了 Homebrew,然后在終端中輸入 “brew install gdb”,等待安裝完成即可。不過,由于 macOS 的系統安全性設置,使用 GDB 時可能需要進行一些額外的配置,比如在系統偏好設置中授予 GDB 相關權限 。
對于 Windows 系統,雖然 GDB 并非系統自帶工具,但你可以通過 MinGW 或 Cygwin 來安裝它。以 MinGW 為例,你需要先下載 MinGW 安裝包,在安裝過程中選擇安裝 GDB 組件,安裝完成后,將 MinGW 的安裝路徑添加到系統環境變量中,這樣就能在命令提示符中方便地使用 GDB 了。
當你成功安裝 GDB 后,啟動它的方式也很簡單。最常見的方法是在終端或命令提示符中直接輸入 “gdb”,如果你的系統中同時安裝了多個版本的 GDB,還可以指定版本號來啟動特定版本。如果你想調試某個特定的可執行程序,比如名為 “test” 的程序,那么輸入 “gdb test” 即可啟動 GDB 并加載該程序進行調試 。此外,你還可以在啟動 GDB 時帶上一些參數,以滿足不同的調試需求,比如 “gdb --args test arg1 arg2”,這樣就能在調試 “test” 程序時為其傳遞 “arg1” 和 “arg2” 這兩個參數。
(1)GDB啟動流程
- gdb -v 檢查是否安裝成功,未安裝成功則安裝(必須確保編譯器已經安裝,如 gcc) 。
- 啟動 gdb
- gdb test_file.exe 來啟動 gdb 調試, 即直接指定需要調試的可執行文件名
- 直接輸入 gdb 啟動,進入 gdb 之后采用命令 file test_file.exe 來指定文件名
- 如果目標執行文件要求出入參數(如 argv[] 接收參數),則可以通過三種方式指定參數:
- 在啟動 gdb 時,gdb --args text_file.exe
- 在進入gdb 之后,運行 set args param_1
- 在 進入 gdb 調試以后,run param_1 或者 start para_1
(2)gdb的功能
- 啟動程序,可以按照用戶自定義的要求隨心所欲的運行程序。
- 可讓被調試的程序在用戶所指定的調試斷點處停?。〝帱c可以是條件表達式)。
- 當程序停住時,可以檢查此時程序中所發生的事。比如,可以打印變量的值。
- 動態改變變量程序的執行環境。
(3)gdb的使用
①運行程序
run(r)運行程序,如果要加參數,則是run arg1 arg2 ...②查看源代碼
list(l):查看最近十行源碼
list fun:查看fun函數源代碼
list file:fun:查看flie文件中的fun函數源代碼③設置斷點與觀察斷點
break 行號/fun設置斷點。
break file:行號/fun設置斷點。
break if<condition>:條件成立時程序停住。
info break(縮寫:i b):查看斷點。
watch expr:一旦expr值發生改變,程序停住。
delete n:刪除斷點。④單步調試
continue(c):運行至下一個斷點。
step(s):單步跟蹤,進入函數,類似于VC中的step in。
next(n):單步跟蹤,不進入函數,類似于VC中的step out。
finish:運行程序,知道當前函數完成返回,并打印函數返回時的堆棧地址和返回值及參數值等信息。
until:當厭倦了在一個循環體內單步跟蹤時,這個命令可以運行程序知道退出循環體。⑤查看運行時數據
print(p):查看運行時的變量以及表達式。
ptype:查看類型。
print array:打印數組所有元素。
print *array@len:查看動態內存。len是查看數組array的元素個數。
print x=5:改變運行時數據。2.3常規 GDB 調試與無符號程序調試差異
(1)帶符號調試的流程回顧
在帶符號調試的常規流程中,當我們擁有包含符號表的可執行程序時,仿佛擁有了一份詳細的地圖,每一個細節都清晰可見 。假設我們正在調試一個 C 語言程序,首先使用帶有 “-g” 選項的 gcc 命令進行編譯,如 “gcc -g -o my_program my_program.c”,這樣編譯生成的可執行文件就包含了豐富的符號信息 。
當啟動 GDB 并加載這個可執行程序后,我們可以輕松地通過行號或函數名來設置斷點。比如,我們知道程序中某個關鍵邏輯在 “my_program.c” 文件的第 50 行,那么在 GDB 中輸入 “break my_program.c:50”,就可以精準地在這一行設置斷點。如果我們想要在某個函數,比如 “calculate_result” 函數的入口處設置斷點,只需輸入 “break calculate_result” 即可。
設置好斷點后,使用 “run” 命令運行程序,程序就會在我們設置的斷點處暫停執行。此時,我們可以使用 “print” 命令查看變量的值,比如 “print variable_name”,就能清楚地了解變量在這一時刻的狀態。還可以使用 “next” 命令單步執行下一行代碼,或者使用 “step” 命令進入函數內部,深入探究函數的執行細節 。通過這些操作,我們能夠逐步排查程序中的問題,找到錯誤的根源。
(2)無符號程序調試面臨的挑戰
而當面對無符號可執行程序時,情況就變得復雜許多,就像在沒有地圖的陌生城市中摸索前行。由于缺少符號表,我們無法直接通過行號或函數名來設置斷點。想象一下,同樣是上述的 C 語言程序,如果在編譯時沒有使用 “-g” 選項,生成的無符號可執行程序就如同一個黑匣子,內部的結構和邏輯難以直接窺探 。
我們不能再像之前那樣輸入 “break my_program.c:50” 或 “break calculate_result”來設置斷點,因為 GDB 無法識別這些行號和函數名。在查看變量值時,也會因為沒有符號信息而變得困難重重,無法直接通過變量名來查看其值。這就需要我們采用一些特殊的方法和技巧,來突破這些困境,實現對無符號可執行程序的有效調試 。
2.4 core文件調試
(1)core文件
在程序崩潰時,一般會生成一個文件叫core文件。core文件記錄的是程序崩潰時的內存映像,并加入調試信息,core文件生成過程叫做core dump(核心已轉儲)。系統默認不會生成該文件。
(2)設置生成core文件
ulimit -c:查看core-dump狀態。ulimit -c xxxx:設置core文件的大小。ulimit -c unlimited:core文件無限制大小。
(3)gdb調試core文件
當設置完ulimit -c xxxx后,再次運行程序發生段錯誤,此時就會生成一個core文件,使用gdb core調試core文件,使用bt命令打印棧回溯信息。
三、GDB常用命令
GDB是一款用于調試 C、C++ 等編程語言程序的常用工具,具備設置斷點、單步執行、查看變量等豐富調試功能。以下為你介紹其常用命令:
3.1啟動與退出命令
- gdb <program>:啟動 GDB 并加載名為 <program> 的可執行文件。此外,還能使用 gdb <program> core 調試關聯 core 文件,或通過 gdb <program> <PID> 調試指定進程。
- q:即 quit,用于退出 GDB 調試環境。
3.2查看源代碼
- 1或 list:默認展示 10 行程序源代碼。
- 1<行號>:顯示以指定行號為中心的前后 10 行代碼。
- 1<函數名>:顯示對應函數的源代碼。
3.3打斷點調試
(1)設置斷點:
- a、break + [源代碼行號][源代碼函數名][內存地址]
- b、break ... if condition ...可以是上述任一參數,condition是條件。例如在循環體中可以設置break ... if i = 100 來設置循環次數
(2)刪除斷點
- (gdb) clear location:參數 location 通常為某一行代碼的行號或者某個具體的函數名。當 location 參數為某個函數的函數名時,表示刪除位于該函數入口處的所有斷點。
- (gdb) delete [breakpoints] [num]:breakpoints 參數可有可無,num 參數為指定斷點的編號,其可以是 delete 刪除某一個斷點,而非全部。
(3)禁用斷點
disable [breakpoints] [num...]:breakpoints 參數可有可無;num... 表示可以有多個參數,每個參數都為要禁用斷點的編號。如果指定 num...,disable 命令會禁用指定編號的斷點;反之若不設定 num...,則 disable 會禁用當前程序中所有的斷點。
(4)激活斷點
- enable [breakpoints] [num...]激活用 num... 參數指定的多個斷點,如果不設定 num...,表示激活所有禁用的斷點
- enable [breakpoints] once num… 臨時激活以 num... 為編號的多個斷點,但斷點只能使用 1 次,之后會自動回到禁用狀態
- enable [breakpoints] count num... 臨時激活以 num... 為編號的多個斷點,斷點可以使用 count 次,之后進入禁用狀態
- enable [breakpoints] delete num… 激活 num.. 為編號的多個斷點,但斷點只能使用 1 次,之后會被永久刪除。
break(b): 打的是普通斷點,打斷點有兩種形式
(gdb) break location // b location,location 代表打斷點的位置
(gdb) break ... if cond // b .. if cond,代表如果 cond 條件為true,則在 “...” 處打斷點通過借助 condition 命令為不同類型斷點設置條件表達式,只有當條件表達式成立(值為 True)時,相應的斷點才會觸發從而使程序暫停運行。
tbreak: tbreak 命令可以看到是 break 命令的另一個版本,tbreak 和 break 命令的用法和功能都非常相似,唯一的不同在于,使用 tbreak 命令打的斷點僅會作用 1 次,即使程序暫停之后,該斷點就會自動消失。
rbreak: 和 break 和 tbreak 命令不同,rbreak 命令的作用對象是 C、C++ 程序中的函數,它會在指定函數的開頭位置打斷點。
(gdb) tbreak regex- regex 代表一個正則表達式,會在匹配到的函數的內部的開頭位置打斷點
- tbreak 命令打的斷點和 break 命令打斷點的效果是一樣的,會一直存在,不會自動消失。
watch: 此命令打的是觀察斷點,可以監控某個變量或者表達式的值。只有當被監控變量(表達式)的值發生改變,程序才會停止運行。
(gdb) watch cond- cond 代表的就是要監控的變量或者表達式
- rwatch 命令:只要程序中出現讀取目標變量(表達式)的值的操作,程序就會停止運行;
- awatch 命令:只要程序中出現讀取目標變量(表達式)的值或者改變值的操作,程序就會停止運行。
catch: 捕捉斷點的作用是,監控程序中某一事件的發生,例如程序發生某種異常時、某一動態庫被加載時等等,一旦目標時間發生,則程序停止執行。
(5)觀察斷點:
- a、watch + [變量][表達式] 當變量或表達式值改變時即停住程序。
- b、rwatch + [變量][表達式] 當變量或表達式被讀時,停住程序。
- c、awatch + [變量][表達式] 當變量或表達式被讀或被寫時,停住程序。
(6)設置捕捉點:
catch + event 當event發生時,停住程序。
event可以是下面的內容:
- a、throw 一個C++拋出的異常。(throw為關鍵字)
- b、catch 一個C++捕捉到的異常。(catch為關鍵字)
- c、exec 調用系統調用exec時。(exec為關鍵字,目前此功能只在HP-UX下有用)
- d、fork 調用系統調用fork時。(fork為關鍵字,目前此功能只在HP-UX下有用)
- e、vfork 調用系統調用vfork時。(vfork為關鍵字,目前此功能只在HP-UX下有用)
- f、load 或 load 載入共享庫(動態鏈接庫)時。(load為關鍵字,目前此功能只在HP-UX下有用)
- g、unload 或 unload 卸載共享庫(動態鏈接庫)時。(unload為關鍵字,目前此功能只在HP-UX下有用)
(7)捕獲信號:
handle + [argu] + signals
signals:是Linux/Unix定義的信號,SIGINT表示中斷字符信號,也就是Ctrl+C的信號,SIGBUS表示硬件故障的信號;SIGCHLD表示子進程狀態改變信號;SIGKILL表示終止程序運行的信號,等等。
argu:
- nostop 當被調試的程序收到信號時,GDB不會停住程序的運行,但會打出消息告訴你收到這種信號。
- stop 當被調試的程序收到信號時,GDB會停住你的程序。
- print 當被調試的程序收到信號時,GDB會顯示出一條信息。
- noprint 當被調試的程序收到信號時,GDB不會告訴你收到信號的信息。
- pass or noignore 當被調試的程序收到信號時,GDB不處理信號。這表示,GDB會把這個信號交給被調試程序會處理。
- nopass or ignore 當被調試的程序收到信號時,GDB不會讓被調試程序來處理這個信號。
(8)線程中斷:
break [linespec] thread [threadno] [if ...]- linespec 斷點設置所在的源代碼的行號。如: test.c:12表示文件為test.c中的第12行設置一個斷點。
- threadno 線程的ID。是GDB分配的,通過輸入info threads來查看正在運行中程序的線程信息。
- if ... 設置中斷條件。
查看信息:
①查看數據:
- print variable 查看變量
- print *array@len 查看數組(array是數組指針,len是需要數據長度)
可以通過添加參數來設置輸出格式:
/ 按十六進制格式顯示變量。
/d 按十進制格式顯示變量。
/u 按十六進制格式顯示無符號整型。
/o 按八進制格式顯示變量。
/t 按二進制格式顯示變量。
/a 按十六進制格式顯示變量。
/c 按字符格式顯示變量。
/f 按浮點數格式顯示變量。②查看內存
examine /n f u + 內存地址(指針變量)
- n 表示顯示內存長度
- f 表示輸出格式(見上)
- u 表示字節數制定(b 單字節;h 雙字節;w 四字節;g 八字節;默認為四字節)
如:x /10cw pFilePath (pFilePath為一個字符串指針,指針占4字節)
x 為examine命令的簡寫。③查看棧信息
backtrace [-n][n]
- n 表示只打印棧頂上n層的棧信息。
- -n 表示只打印棧底上n層的棧信息。
- 不加參數,表示打印所有棧信息。
3.4單步調試
run(r)
continue(c)
next(n)命令格式: (gdb) next count:count 表示單步執行多少行代碼,默認為 1 行
其最大的特點是當遇到包含調用函數的語句時,無論函數內部包含多少行代碼,next 指令都會一步執行完。也就是說,對于調用的函數來說,next 命令只會將其視作一行代碼
step(s)
- (gdb) step count:參數 count 表示一次執行的行數,默認為 1 行。
- 通常情況下,step 命令和 next 命令的功能相同,都是單步執行程序。不同之處在于,當 step 命令所執行的代碼行中包含函數時,會進入該函數內部,并在函數第一行代碼處停止執行。
until(u)
(gdb) until:不帶參數的 until 命令,可以使 GDB 調試器快速運行完當前的循環體,并運行至循環體外停止。注意,until 命令并非任何情況下都會發揮這個作用,只有當執行至循環體尾部(最后一行代碼)時,until 命令才會發生此作用;反之,until 命令和 next 命令的功能一樣,只是單步執行程序
(gdb) until location:參數 location 為某一行代碼的行號
查看變量的值
print(p)
- p num_1:參數 num_1 用來代指要查看或者修改的目標變量或者表達式
- 它的功能就是在 GDB 調試程序的過程中,輸出或者修改指定變量或者表達式的值
isplay
- (gdb) display expr
- (gdb) display/fmt expr
- expr 表示要查看的目標變量或表達式;參數 fmt 用于指定輸出變量或表達式的格式
- (gdb) undisplay num...
- (gdb) delete display num...
- 參數 num... 表示目標變量或表達式的編號,編號的個數可以是多個
- (gdb) disable display num...
- 禁用自動顯示列表中處于激活狀態下的變量或表達式
- (gdb) enable display num...
- 也可以激活當前處于禁用狀態的變量或表達式
- 和 print 命令一樣,display 命令也用于調試階段查看某個變量或表達式的值
- 它們的區別是,使用 display 命令查看變量或表達式的值,每當程序暫停執行(例如單步執行)時,GDB 調試器都會自動幫我們打印出來,而 print 命令則不會
GDB handle 命令: 信號處理
→(gdb) handle signal mode其中,signal 參數表示要設定的目標信號,它通常為某個信號的全名(SIGINT)或者簡稱(去除‘SIG’后的部分,如 INT);如果要指定所有信號,可以用 all 表示。
mode 參數用于明確 GDB 處理該目標信息的方式,其值可以是如下幾個:
- ostop:當信號發生時,GDB 不會暫停程序,其可以繼續執行,但會打印出一條提示信息,告訴我們信號已經發生;
- stop:當信號發生時,GDB 會暫停程序執行。
- noprint:當信號發生時,GDB 不會打印出任何提示信息;
- print:當信號發生時,GDB 會打印出必要的提示信息;
- nopass(或者 ignore):GDB 捕獲目標信號的同時,不允許程序自行處理該信號;
- pass(或者 noignore):GDB 調試在捕獲目標信號的同時,也允許程序自動處理該信號。
可以在 gdb 模式下,通過 info signals 或者 info signals <signal_name> (例如 info signals SIGINT) 查看不同 signal 的信息。
GDB frame和backtrace命令:查看棧信息
(gdb) frame spec 該命令可以將 spec 參數指定的棧幀選定為當前棧幀。spec 參數的值,常用的指定方法有 3 種:
- 通過棧幀的編號指定。0 為當前被調用函數對應的棧幀號,最大編號的棧幀對應的函數通常就是 main() 主函數;
- 借助棧幀的地址指定。棧幀地址可以通過 info frame 命令(后續會講)打印出的信息中看到;
- 通過函數的函數名指定。注意,如果是類似遞歸函數,其對應多個棧幀的話,通過此方法指定的是編號最小的那個棧幀。
(gdb) info frame 我們可以查看當前棧幀中存儲的信息
該命令會依次打印出當前棧幀的如下信息:
- 當前棧幀的編號,以及棧幀的地址;
- 當前棧幀對應函數的存儲地址,以及該函數被調用時的代碼存儲的地址
- 當前函數的調用者,對應的棧幀的地址;
- 編寫此棧幀所用的編程語言;
- 函數參數的存儲地址以及值;
- 函數中局部變量的存儲地址;
- 棧幀中存儲的寄存器變量,例如指令寄存器(64位環境中用 rip 表示,32為環境中用 eip 表示)、堆?;羔樇拇嫫鳎?4位環境用 rbp 表示,32位環境用 ebp 表示)等。
除此之外,還可以使用 info args 命令查看當前函數各個參數的值;使用 info locals 命令查看當前函數中各局部變量的值。
(gdb) backtrace [-full] [n] 用于打印當前調試環境中所有棧幀的信息
其中,用 [ ] 括起來的參數為可選項,它們的含義分別為:
n:一個整數值,當為正整數時,表示打印最里層的 n 個棧幀的信息;n 為負整數時,那么表示打印最外層 n 個棧幀的信息;
-full:打印棧幀信息的同時,打印出局部變量的值。
GDB編輯和搜索源碼
GDB edit命令:編輯文件
- (gdb) edit [location]
- (gdb) edit [filename] : [location]
location 表示程序中的位置。這個命令表示激活文件的指定位置,然后進行編輯。
如果遇到報錯 "bash: /bin/ex: 沒有那個文件或目錄", 因為 GDB 的默認編輯器是 ex , 則需要指定編輯器,如 export EDITOR=/usr/bin/vim or export EDITOR=/usr/bin/vi
GDB search命令:搜索文件
search <regexp>
reverse-search <regexp>第一項命令格式表示從當前行的開始向前搜索,后一項表示從當前行開始向后搜索。其中 regexp 就是正則表達式,正則表達式描述了一種字符串匹配的模式,可以用來檢查一個串中是否含有某種子串、將匹配的子串替換或者從某個串中取出符合某個條件的子串。很多的編程語言都支持使用正則表達式。
注:GDB 命令還能利用 help 指令查看更具體的使用說明,比如輸入 help breakpoints 可獲取更多有關斷點設置命令的內容。
四、GDB調試程序用法
一般來說,GDB主要幫忙你完成下面四個方面的功能:
- 1、啟動你的程序,可以按照你的自定義的要求隨心所欲的運行程序
- 2、可讓被調試的程序在你所指定的調置的斷點處停住。(斷點可以是條件表達式)
- 3、當程序被停住時,可以檢查此時你的程序中所發生的事。
- 4、動態的改變你程序的執行環境。
從上面看來,GDB和一般的調試工具沒有什么兩樣,基本上也是完成這些功能,不過在細節上,你會發現GDB這個調試工具的強大,大家可能比較習慣了圖形化的調試工具,但有時候,命令行的調試工具卻有著圖形化工具所不能完成的功能。讓我們一一看來。
一個調試示例:
源程序: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 }編譯生成執行文件:(Linux下)
hchen/test> cc -g tst.c -o tst使用GDB調試:
hchen/test> gdb tst <---------- 啟動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命令相當于list,從第一行開始例出原碼。
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) <-------------------- 直接回車表示,重復上一次命令
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 <-------------------- 設置斷點,在源程序第16行處。
Breakpoint 1 at 0x8048496: file tst.c, line 16.
(gdb) break func <-------------------- 設置斷點,在函數func()入口處。
Breakpoint 2 at 0x8048456: file tst.c, line 5.
(gdb) info break <-------------------- 查看斷點信息。
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 <--------------------- 運行程序,run命令簡寫
Starting program: /home/hchen/test/tst
Breakpoint 1, main () at tst.c:17 <---------- 在斷點處停住。
17 long result = 0;
(gdb) n <--------------------- 單條語句執行,next命令簡寫。
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 <--------------------- 繼續運行程序,continue命令簡寫。
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命令簡寫。
$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 <--------------------- 查看函數堆棧。
#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 <--------------------- 退出函數。
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 <--------------------- 繼續運行。
Continuing.
result[1-250] = 31375 <----------程序輸出。
Program exited with code 027. <--------程序退出,調試結束。
(gdb) q <--------------------- 退出gdb。
hchen/test>好了,有了以上的感性認識,還是讓我們來系統地認識一下gdb吧。
基本gdb命令:
GDB常用命令 格式 含義 簡寫
list List [開始,結束] 列出文件的代碼清單 l
prit Print 變量名 打印變量內容 p
break Break [行號或函數名] 設置斷點 b
continue Continue [開始,結束] 繼續運行 c
info Info 變量名 列出信息 i
next Next 下一行 n
step Step 進入函數(步入) S
display Display 變量名 顯示參數
file File 文件名(可以是絕對路徑和相對路徑) 加載文件
run Run args 運行程序 r五、GDB 調試無符號可執行程序
5.1準備工作
首先,我們以一個簡單的 C 語言程序為例。假設我們有如下代碼,保存在名為example.c的文件中:
#include <stdio.h>
int main() {
int a = 10;
int b = 20;
int sum = a + b;
printf("The sum is: %d\n", sum);
return 0;
}在編譯時,我們不使用-g參數,直接使用gcc example.c -o example命令進行編譯,這樣生成的example就是一個無符號可執行文件 。
5.2反匯編獲取關鍵信息
接下來,使用 GDB 加載這個無符號可執行程序,輸入 “gdb example” 啟動 GDB 。然后,我們要對程序中的關鍵函數進行反匯編,這里以main函數為例,在 GDB 中輸入 “disassemble main” 命令 。
執行該命令后,我們會得到類似如下的反匯編代碼:
Dump of assembler code for function main:
0x0804841d <+0>: push %ebp
0x0804841e <+1>: mov %esp,%ebp
0x08048420 <+3>: and $0xfffffff0,%esp
0x08048423 <+6>: sub $0x20,%esp
0x08048426 <+9>: movl $0xa,0x1c(%esp)
0x0804842e <+17>: movl $0x14,0x18(%esp)
0x08048436 <+25>: mov 0x18(%esp),%eax
0x0804843a <+29>: mov 0x1c(%esp),%edx
0x0804843e <+33>: add %edx,%eax
0x08048440 <+35>: mov %eax,0x14(%esp)
0x08048444 <+39>: mov 0x14(%esp),%eax
0x08048448 <+43>: mov %eax,0x4(%esp)
0x0804844c <+47>: movl $0x8048510,(%esp)
0x08048453 <+54>: call 0x8048300 <printf@plt>
0x08048458 <+59>: mov $0x0,%eax
0x0804845d <+64>: leave
0x0804845e <+65>: ret
End of assembler dump.在這段反匯編代碼中,最左邊的一列是指令的內存地址,比如0x0804841d就是函數開始的地址 。中間一列是指令的機器碼,以十六進制表示 。右邊一列是對應的匯編指令,比如 “push % ebp”,它的作用是將ebp寄存器的值壓入棧中,通常用于保存函數調用前的棧幀信息 。通過分析這些匯編指令,我們可以了解函數的執行流程,比如從哪里開始初始化變量,在哪里進行加法運算,以及在哪里調用printf函數等 。
5.3基于地址設置斷點
根據反匯編得到的地址,我們可以使用 “break * 地址” 命令來設置斷點。例如,如果我們想在printf函數調用前設置斷點,從反匯編代碼中可以看到call 0x8048300 <printf@plt>這一行的地址是0x08048453,那么我們就在 GDB 中輸入 “break *0x08048453” 。
我們還可以在其他關鍵位置設置斷點,比如在加法運算的指令處。假設加法運算的指令地址是0x0804843e,我們就可以輸入 “break *0x0804843e” 來設置斷點 。通過在不同關鍵位置設置斷點,我們可以在程序執行到這些位置時暫停,以便觀察程序的運行狀態 。
5.4調試過程中的常用命令
設置好斷點后,我們就可以使用 “run” 命令(簡寫為 “r”)來運行程序,程序會在第一個斷點處暫停 。當程序暫停在斷點處時,如果我們想繼續執行到下一個斷點,可以使用 “continue” 命令(簡寫為 “c”) 。
雖然在無符號程序中直接查看變量值比較困難,但在某些特定場景下,我們可以通過查看內存值來輔助判斷。比如,我們知道變量sum的地址(假設通過分析匯編代碼和棧幀信息得到),我們可以使用 “x /nfu 地址” 命令來查看內存中的值 。其中,“x” 表示查看內存,“n” 表示要顯示的單元數,“f” 表示顯示格式(如 “x” 表示十六進制,“d” 表示十進制),“u” 表示每個單元的大?。ㄈ?“b” 表示字節,“w” 表示字,“g” 表示雙字) 。例如,“x /1dw 0x 內存地址” 就可以以十進制格式查看指定內存地址處的一個字(4 字節)的值,通過這個值我們可以推測變量sum的值是否正確 。
此外,我們還可以使用 “next” 命令(簡寫為 “n”)單步執行下一條指令,不進入函數內部;使用 “step” 命令(簡寫為 “s”)單步執行下一條指令,會進入函數內部 。這些命令在調試過程中非常實用,可以幫助我們逐步排查程序中的問題 。
5.5調試中的常見問題及解決方法
(1)斷點設置無效
在使用 GDB 調試無符號可執行程序時,斷點設置無效是一個較為常見且棘手的問題 。這可能是由多種原因導致的。
首先,地址錯誤是一個常見原因。在無符號程序中,我們依賴反匯編得到的地址來設置斷點,但如果反匯編過程出現偏差,或者我們對地址的理解和使用有誤,就會導致斷點設置在錯誤的位置,從而無法生效 。例如,在反匯編代碼中,指令地址可能會因為代碼優化、鏈接過程中的重定位等因素而發生變化,如果我們沒有及時更新地址,就會出現斷點無效的情況 。
程序優化也可能導致指令偏移,進而使斷點設置無效 。當程序在編譯時開啟了優化選項,編譯器會對代碼進行一系列優化,如刪除冗余代碼、合并指令、調整指令順序等 。這些優化可能會使我們根據未優化代碼反匯編得到的地址與實際執行的代碼地址不一致,導致斷點無法命中 。比如,原本在某一行代碼處設置的斷點,由于優化后這行代碼被刪除或移動到其他位置,斷點就無法起到作用 。
為了排查和解決斷點設置無效的問題,我們可以采取以下方法 。首先,仔細檢查反匯編代碼,確認地址的準確性 ??梢远啻畏磪R編,對比不同情況下的反匯編結果,確保地址沒有錯誤 。同時,查看程序的編譯選項,了解是否開啟了優化選項,如果開啟了,可以嘗試關閉優化重新編譯程序,然后再次設置斷點進行調試 。
如果斷點仍然無效,可以使用 “info breakpoints” 命令查看斷點的詳細信息,包括斷點的編號、位置、狀態等 。通過這些信息,判斷斷點是否被正確設置,是否存在沖突或其他問題 。此外,還可以嘗試在不同的位置設置斷點,觀察程序的響應情況,逐步縮小問題的范圍 。
(2)查看變量值困難
在無符號程序中,由于缺少符號信息,直接查看變量值變得困難重重 。然而,我們可以利用內存查看命令來間接獲取變量值的相關信息 。
GDB 中的 “x /nfu 地址” 命令就是一個強大的內存查看工具 。其中,“n” 表示要顯示的單元數,“f” 表示顯示格式(如 “x” 表示十六進制,“d” 表示十進制,“u” 表示無符號十進制等),“u” 表示每個單元的大小(如 “b” 表示字節,“w” 表示字,“g” 表示雙字) 。例如,“x /1dw 0x 內存地址” 可以以十進制格式查看指定內存地址處的一個字(4 字節)的值 。
在使用這個命令時,我們需要根據反匯編信息和程序邏輯來推測變量值 。首先,通過分析反匯編代碼,確定變量在內存中的存儲位置 。例如,在匯編代碼中,我們可以看到變量的賦值操作,以及與變量相關的內存讀寫指令,通過這些信息,大致確定變量所在的內存區域 。
然后,結合程序邏輯,判斷變量的類型和可能的值范圍 。比如,如果我們知道某個變量是用于存儲整數的,那么在查看內存值時,就可以根據整數的存儲方式和大小來解讀內存中的數據 。如果變量是一個數組,我們可以根據數組的定義和索引,通過內存查看命令查看數組中特定元素的值 。
在實際操作中,還可以通過設置多個斷點,在程序執行的不同階段查看內存值,觀察變量值的變化情況,從而更好地理解程序的運行邏輯,判斷變量值是否符合預期 。例如,在變量賦值前和賦值后分別設置斷點,查看內存值的變化,就可以驗證變量是否被正確賦值 。
六、GDB進階功能
6.1回溯追蹤(backtrace)
在程序調試過程中,了解函數調用順序及各層調用間的上下文關系至關重要。有時候程序出現錯誤,但我們并不知道錯誤是在哪個函數調用鏈路中產生的,這時候回溯追蹤功能就派上用場了。GDB 提供了backtrace命令,簡寫為bt,用于展示當前的調用棧信息。
當程序運行出現異常或者在斷點處暫停時,輸入bt命令,GDB 會按深度由淺至深列出各個棧幀,每個棧幀包含了函數名、源文件名、行號及參數值等關鍵信息。例如,我們有一個包含多個函數調用的程序:
#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 中調試這個程序,當程序在function_c函數內暫停時,輸入bt命令,輸出結果可能如下:
(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從輸出中可以清晰地看到函數的調用順序:main調用function_a,function_a調用function_b,function_b調用function_c,并且還能看到每個函數調用時的參數值 。這對于我們快速定位問題發生的位置非常有幫助,比如如果function_c中出現了除零錯誤,我們就可以通過回溯追蹤信息,從調用鏈路上查找傳入function_c的參數是如何計算得出的,進而找到問題的根源。
6.2動態內存檢測
內存泄漏、非法訪問等內存問題是程序健壯性的隱形殺手,它們可能會導致程序運行一段時間后出現性能下降甚至崩潰。雖然有像 Valgrind 這樣專門的內存分析工具,但 GDB 自身也具備一定的內存檢測能力,尤其是結合 heap 插件,可以對程序的堆內存使用情況進行初步排查。
首先,我們需要獲取并加載 heap 插件,假設插件文件為gdbheap.py,使用以下命令加載插件:
(gdb) source /path/to/gdbheap.py然后,我們可以將 GDB 附加到正在運行的進程上(假設進程 ID 為<pid>),并使用插件提供的命令來查看堆內存分配情況:
(gdb) attach <pid>
(gdb) monitor heap執行上述命令后,GDB 會顯示堆內存的相關信息,比如內存塊的數量、大小、分配狀態等。通過觀察這些信息,我們可以發現一些潛在的內存問題。例如,如果發現有大量的小內存塊被分配且長時間沒有釋放,可能存在內存泄漏的風險;如果看到內存塊的分配和釋放順序異常,可能存在非法內存訪問的問題。
下面是一個簡單的示例,展示如何使用 GDB 和 heap 插件檢測內存問題:
#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,制造內存泄漏
return 0;
}在程序運行后,使用 GDB 和 heap 插件進行檢測,通過分析插件輸出的堆內存信息,我們就有可能發現ptr2所指向的內存沒有被釋放,從而定位到內存泄漏問題。
6.3條件斷點與觀察點
條件斷點:在一些復雜的程序中,我們可能不希望程序在每個斷點處都暫停,而是希望當滿足特定條件時才暫停程序執行,這時候就可以使用條件斷點。例如,在一個處理數組的程序中,我們懷疑當數組下標i大于數組大小時會出現數組越界問題,我們可以設置如下條件斷點:
(gdb) break array_processing_function if i >= array_size這樣,只有當i大于或等于array_size時,程序才會在array_processing_function處暫停,大大提高了調試效率,避免了在無關斷點處頻繁暫停程序,讓我們能夠更精準地捕捉到問題出現的時刻 。
觀察點:觀察點(Watchpoint)用于監控變量值的變化。當觀察的變量被修改時,GDB 會自動暫停程序,這對于追蹤難以復現的偶發問題尤為有用。比如,在一個多線程程序中,某個全局變量的值被意外修改,但我們不確定是哪個線程在什么情況下修改的,就可以為這個全局變量設置觀察點:
(gdb) watch global_variable當global_variable的值發生改變時,程序會立即暫停,此時我們可以查看當前的線程狀態、調用棧等信息,來確定變量是如何被修改的,從而找到問題的根源。此外,還可以設置讀觀察點(rwatch)和讀寫觀察點(awatch),rwatch在變量被讀取時暫停程序,awatch在變量被讀取或修改時暫停程序,根據具體的調試需求選擇合適的觀察點類型 。
6.4遠程調試技術
在實際開發中,我們經常會遇到需要調試部署在遠程服務器或嵌入式設備上的程序的情況,GDB 支持通過網絡進行遠程調試,這極大地簡化了跨設備調試的復雜性。
遠程調試的基本原理是在遠程設備上運行 GDB 的服務器端(gdbserver),并在本地 GDB 客戶端連接至服務器端。具體操作步驟如下:
⑴在遠程設備上:首先確保遠程設備上安裝了gdbserver,可以通過gdbserver --version命令檢查是否安裝。然后啟動gdbserver,并指定調試的程序和監聽端口,例如:
gdbserver :<port> /path/to/remote_program其中<port>是未被占用的端口號,可以根據實際情況任意指定,/path/to/remote_program是要調試的程序路徑。啟動成功后,gdbserver會監聽指定端口,等待本地 GDB 客戶端連接。
⑵在本地 GDB 客戶端:在本地啟動 GDB,并加載本地保存的與遠程程序相同的可執行文件副本(確保編譯時帶有調試信息),然后使用target remote命令連接到遠程gdbserver:
gdb ./local_program
(gdb) target remote <remote_host>:<port><remote_host>是遠程設備的 IP 地址或主機名,<port>是在遠程設備上啟動gdbserver時指定的端口號。連接成功后,就可以像在本地調試程序一樣,在本地 GDB 客戶端使用各種調試命令,如設置斷點、單步執行、查看變量值等,GDB 會通過網絡與遠程gdbserver通信,實現對遠程程序的調試 。
例如,在開發一款嵌入式系統程序時,我們可以在開發板(遠程設備)上運行gdbserver,在本地 PC 上使用 GDB 客戶端進行調試,通過這種方式,能夠在本地環境中方便地調試運行在遠程嵌入式設備上的程序,提高開發效率 。


























