GDB 調試工具詳解:逐步剖析程序運行過程
GDB(GNU 調試器)是一個功能強大的開源調試工具,用於幫助程序員分析和調試 C、C++ 等編程語言的代碼。它可以在運行過程中檢查程序狀態,設置斷點以停止程序執行並觀察變量值、內存狀態等,並提供一系列命令和功能來輔助調試過程。
GDB 可以與不同編譯器和操作系統配合使用,支持多種調試特性,如單步執行、條件斷點、查看堆棧信息、監視變量值、內存泄漏檢測等。通過 GDB,開發者能夠深入理解程序運行時的細節,找出錯誤並進行優化。
除了基本的命令行界面外,還有許多圖形化界面和集成開發環境(IDE)提供對 GDB 的支持,使得調試過程更加直觀和方便。無論是新手還是經驗豐富的開發人員,掌握 GDB 都能提高代碼調試能力和效率。
一、什麼是 GDB
gdb 是 GNU debugger 的縮寫,是編程調試工具。
-
GDB 官網:https://www.gnu.org/software/gdb/(https://www.gnu.org/software/gdb/)
-
GDB 適用的編程語言: Ada / C / C++ / objective-c / Pascal 等。
-
GDB 的工作方式: 本地調試和遠程調試。
目前 release 的最新版本爲 8.0,GDB 可以運行在 Linux 和 Windows 操作系統上。
1.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
1.2 gdb 的功能
-
啓動程序,可以按照用戶自定義的要求隨心所欲的運行程序。
-
可讓被調試的程序在用戶所指定的調試斷點處停住(斷點可以是條件表達式)。
-
當程序停住時,可以檢查此時程序中所發生的事。比如,可以打印變量的值。
-
動態改變變量程序的執行環境。
1.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:改變運行時數據。
1.4 程序錯誤
-
編譯錯:編寫程序的時候沒有符合語言規範導致編譯錯誤。比如:語法錯誤。
-
運行時錯誤:編譯器檢查不出這種錯誤,但在運行時候可能會導致程序崩潰。比如:內存地址非法訪問。
-
邏輯錯誤:編譯和運行都很順利,但是程序沒有幹我們期望乾的事情。
1.5 gdb 調試段錯誤
什麼是段錯誤?段錯誤是由於訪問非法地址而產生的錯誤。
-
訪問系統數據區,尤其是往系統保護的內存地址寫數據。比如:訪問地址爲 0 的地址。
-
內存越界(數組越界,變量類型不一致等)訪問到不屬於當前程序的內存區域。
gdb 調試段錯誤,可以直接運行程序,當程序運行崩潰後,gdb 會打印運行的信息,比如:收到了 SIGSEGV 信號,然後可以使用bt
命令,打印棧回溯信息,然後根據程序發生錯誤的代碼,修改程序。
1.6 core 文件調試
1.6.1 core 文件
在程序崩潰時,一般會生成一個文件叫core
文件。core 文件記錄的是程序崩潰時的內存映像,並加入調試信息,core 文件生成過程叫做core dump(核心已轉儲)
。系統默認不會生成該文件。
1.6.2 設置生成 core 文件
-
ulimit -c
:查看 core-dump 狀態。 -
ulimit -c xxxx
:設置 core 文件的大小。 -
ulimit -c unlimited
:core 文件無限制大小。
6.3 gdb 調試 core 文件
當設置完ulimit -c xxxx
後,再次運行程序發生段錯誤,此時就會生成一個core
文件,使用gdb core
調試 core 文件,使用bt
命令打印棧回溯信息。
二、GDB 常用命令
-
以下以 test_file.c 作爲源程序例子的名字,test_file.exe 作爲可執行文件例子的名字, 以 param_1 作爲參數的例子的名字。
-
(gdb) 表示是在 gdb 調試模式下運行
-
一般常用的方法有兩種,即打斷點調試 和單步調試。
-
list(l): 列出源代碼
-
quit(q): 退出 gdb 調試模式
-
進入 gdb 之後,輸入 help 可以查看所有命令的使用說明
2.1 查看源碼
list [函數名][行數]
2.2 打斷點調試
(1)設置斷點:
-
a、break + [源代碼行號][源代碼函數名][內存地址]
-
b、break ... if condition ... 可以是上述任一參數,condition 是條件。例如在循環體中可以設置 break ... if i = 100 來設置循環次數
刪除斷點
(gdb) clear location:參數 location 通常爲某一行代碼的行號或者某個具體的函數名。當 location 參數爲某個函數的函數名時,表示刪除位於該函數入口處的所有斷點。
(gdb) delete [breakpoints] [num]:breakpoints 參數可有可無,num 參數爲指定斷點的編號,其可以是 delete 刪除某一個斷點,而非全部。
禁用斷點
disable [breakpoints] [num...]:breakpoints 參數可有可無;num... 表示可以有多個參數,每個參數都爲要禁用斷點的編號。如果指定 num...,disable 命令會禁用指定編號的斷點;反之若不設定 num...,則 disable 會禁用當前程序中所有的斷點。
激活斷點
-
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: 捕捉斷點的作用是,監控程序中某一事件的發生,例如程序發生某種異常時、某一動態庫被加載時等等,一旦目標時間發生,則程序停止執行。
(2)觀察斷點:
-
a、watch + [變量][表達式] 當變量或表達式值改變時即停住程序。
-
b、rwatch + [變量][表達式] 當變量或表達式被讀時,停住程序。
-
c、awatch + [變量][表達式] 當變量或表達式被讀或被寫時,停住程序。
(3)設置捕捉點:
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 下有用)
(4)捕獲信號:
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 不會讓被調試程序來處理這個信號。
(5)線程中斷:
break [linespec] thread [threadno] [if ...]
linespec 斷點設置所在的源代碼的行號。如: test.c:12 表示文件爲 test.c 中的第 12 行設置一個斷點。
threadno 線程的 ID。是 GDB 分配的,通過輸入 info threads 來查看正在運行中程序的線程信息。
if ... 設置中斷條件。
查看信息:
(1)查看數據:
print variable 查看變量
print *array@len 查看數組(array是數組指針,len是需要數據長度)
可以通過添加參數來設置輸出格式:
/ 按十六進制格式顯示變量。
/d 按十進制格式顯示變量。
/u 按十六進制格式顯示無符號整型。
/o 按八進制格式顯示變量。
/t 按二進制格式顯示變量。
/a 按十六進制格式顯示變量。
/c 按字符格式顯示變量。
/f 按浮點數格式顯示變量。
(2)查看內存
examine /n f u + 內存地址(指針變量)
-
n 表示顯示內存長度
-
f 表示輸出格式(見上)
-
u 表示字節數制定(b 單字節;h 雙字節;w 四字節;g 八字節;默認爲四字節)
如:x /10cw pFilePath (pFilePath爲一個字符串指針,指針佔4字節)
x 爲examine命令的簡寫。
(3)查看棧信息
backtrace [-n][n]
-
n 表示只打印棧頂上 n 層的棧信息。
-
-n 表示只打印棧底上 n 層的棧信息。
-
不加參數,表示打印所有棧信息。
2.3 單步調試
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 表示)、堆棧基指針寄存器(64 位環境用 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
-
reverse-search
-
第一項命令格式表示從當前行的開始向前搜索,後一項表示從當前行開始向後搜索。其中 regexp 就是正則表達式,正則表達式描述了一種字符串匹配的模式,可以用來檢查一個串中是否含有某種子串、將匹配的子串替換或者從某個串中取出符合某個條件的子串。很多的編程語言都支持使用正則表達式。
三、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 實戰
下面是一個使用了上述命令的實戰例子:
[root@www.linuxidc.com bufbomb]# gdb bufbomb
GNU gdb (GDB) Red Hat Enterprise Linux (7.2-75.el6)
Copyright (C) 2010 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-RedHat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /root/Temp/bufbomb/bufbomb...done.
(gdb) b getbuf
Breakpoint 1 at 0x8048ad6
(gdb) run -t cdai
Starting program: /root/Temp/bufbomb/bufbomb -t cdai
Team: cdai
Cookie: 0x5e5ee04e
Breakpoint 1, 0x08048ad6 in getbuf ()
Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.149.el6_6.4.i686
(gdb) bt
#0 0x08048ad6 in getbuf ()
#1 0x08048db2 in test ()
#2 0x08049085 in launch ()
#3 0x08049257 in main ()
(gdb) info frame 0
Stack frame at 0xffffb540:
eip = 0x8048ad6 in getbuf; saved eip 0x8048db2
called by frame at 0xffffb560
Arglist at 0xffffb538, args:
Locals at 0xffffb538, Previous frame's sp is 0xffffb540
Saved registers:
ebp at 0xffffb538, eip at 0xffffb53c
(gdb) info registers
eax 0xc 12
ecx 0xffffb548 -19128
edx 0xc8c340 13157184
ebx 0x0 0
esp 0xffffb510 0xffffb510
ebp 0xffffb538 0xffffb538
esi 0x804b018 134524952
edi 0xffffffff -1
eip 0x8048ad6 0x8048ad6 <getbuf+6>
eflags 0x282 [ SF IF ]
cs 0x23 35
ss 0x2b 43
ds 0x2b 43
es 0x2b 43
fs 0x0 0
gs 0x63 99
(gdb) x/10x $sp
0xffffb510: 0xf7ffc6b0 0x00000001 0x00000001 0xffffb564
0xffffb520: 0x08048448 0x0804a12c 0xffffb548 0x00c8aff4
0xffffb530: 0x0804b018 0xffffffff
(gdb) si
0x08048ad9 in getbuf ()
(gdb) si
0x08048adc in getbuf ()
(gdb) si
0x080489c0 in Gets ()
(gdb) n
Single stepping until exit from function Gets,
which has no line number information.
Type string:123
0x08048ae1 in getbuf ()
(gdb) si
0x08048ae2 in getbuf ()
(gdb) c
Continuing.
Dud: getbuf returned 0x1
Better luck next time
Program exited normally.
(gdb) quit
4.1 逆向調試
GDB 7.0 後加入了 Reversal Debugging 功能。具體來說,比如我在 getbuf() 和 main() 上設置了斷點,當啓動程序時會停在 main() 函數的斷點上。此時敲入 record 後 continue 到下一斷點 getbuf(),GDB 就會記錄從 main() 到 getbuf() 的運行時信息。現在用 rn 就可以逆向地從 getbuf() 調試到 main()。就像《X 戰警:逆轉未來》裏一樣,挺神奇吧!
這種方式適合從 bug 處反向去找引起 bug 的代碼,實用性因情況而異。當然,它也是有侷限性的。像程序假如有 I/O 輸出等外部條件改變時,GDB 是沒法 “逆轉” 的。
[root@www.linuxidc.com bufbomb]# gdb bufbomb
GNU gdb (GDB) Red Hat Enterprise Linux (7.2-75.el6)
Copyright (C) 2010 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /root/Temp/bufbomb/bufbomb...done.
(gdb) b getbuf
Breakpoint 1 at 0x8048ad6
(gdb) b main
Breakpoint 2 at 0x80490c6
(gdb) run -t cdai
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /root/Temp/bufbomb/bufbomb -t cdai
Breakpoint 2, 0x080490c6 in main ()
(gdb) record
(gdb) c
Continuing.
Team: cdai
Cookie: 0x5e5ee04e
Breakpoint 1, 0x08048ad6 in getbuf ()
(gdb) rn
Single stepping until exit from function getbuf,
which has no line number information.
0x08048dad in test ()
(gdb) rn
Single stepping until exit from function test,
which has no line number information.
0x08049080 in launch ()
(gdb) rn
Single stepping until exit from function launch,
which has no line number information.
0x08049252 in main ()
4.2 VSCode+GDB+Qemu 調試 ARM64 linux 內核
linux kernel 是一個非常複雜的系統,初學者會很難入門。如果有一個方便的調試環境,學習效率至少能有 5-10 倍的提升。
爲了學習 linux 內核,通常有這兩個需要:
-
可以擺脫硬件,方便的編譯和運行 linux
-
可以使用圖形化的工具來調試 linux
筆者使用 VSCode+GDB+Qemu 完成了這兩個需求:
-
qemu 作爲虛擬機,用來啓動 linux。
-
VSCode+GDB 作爲調試工具,用來圖形化地 DEBUG。
最終效果大致如下:
qemu 運行界面:
vscode 調試界面:
下面將一步一步介紹如何搭建上述環境。本文所有操作都在 Vmware Ubuntu16 虛擬機上進行。
安裝編譯工具鏈
由於 Ubuntu 是 X86 架構,爲了編譯 arm64 的文件,需要安裝交叉編譯工具鏈
sudo apt-get install gcc-aarch64-linux-gnu
sudo apt-get install libncurses5-dev build-essential git bison flex libssl-dev
製作根文件系統
linux 的啓動需要配合根文件系統,這裏我們利用 busybox 來製作一個簡單的根文件系統
編譯 busybox
wget https://busybox.net/downloads/busybox-1.33.1.tar.bz2
tar -xjf busybox-1.33.1.tar.bz2
cd busybox-1.33.1
打開靜態庫編譯選項
make menuconfig
Settings --->
[*] Build static binary (no shared libs)
指定編譯工具
export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-
編譯
make
make install
編譯完成,在 busybox 目錄下生成_install 目錄
定製文件系統
爲了 init 進程能正常啓動, 需要再額外進行一些配置
根目錄添加 etc、dev 和 lib 目錄
# bryant @ ubuntu in ~/Downloads/busybox-1.33.1/_install [1:02:17]
$ mkdir etc dev lib
# bryant @ ubuntu in ~/Downloads/busybox-1.33.1/_install [1:02:17]
$ ls
bin dev etc lib linuxrc sbin usr
在 etc 分別創建文件:
# bryant @ ubuntu in ~/Downloads/busybox-1.33.1/_install/etc [1:06:13]
$ cat profile
#!/bin/sh
export HOSTNAME=bryant
export USER=root
export HOME=/home
export PS1="[$USER@$HOSTNAME \W]\# "
PATH=/bin:/sbin:/usr/bin:/usr/sbin
LD_LIBRARY_PATH=/lib:/usr/lib:$LD_LIBRARY_PATH
export PATH LD_LIBRARY_PATH
# bryant @ ubuntu in ~/Downloads/busybox-1.33.1/_install/etc [1:06:16]
$ cat inittab
::sysinit:/etc/init.d/rcS
::respawn:-/bin/sh
::askfirst:-/bin/sh
::ctrlaltdel:/bin/umount -a -r
# bryant @ ubuntu in ~/Downloads/busybox-1.33.1/_install/etc [1:06:19]
$ cat fstab
#device mount-point type options dump fsck order
proc /proc proc defaults 0 0
tmpfs /tmp tmpfs defaults 0 0
sysfs /sys sysfs defaults 0 0
tmpfs /dev tmpfs defaults 0 0
debugfs /sys/kernel/debug debugfs defaults 0 0
kmod_mount /mnt 9p trans=virtio 0 0
# bryant @ ubuntu in ~/Downloads/busybox-1.33.1/_install/etc [1:06:26]
$ ls init.d
rcS
# bryant @ ubuntu in ~/Downloads/busybox-1.33.1/_install/etc [1:06:30]
$ cat init.d/rcS
mkdir -p /sys
mkdir -p /tmp
mkdir -p /proc
mkdir -p /mnt
/bin/mount -a
mkdir -p /dev/pts
mount -t devpts devpts /dev/pts
echo /sbin/mdev > /proc/sys/kernel/hotplug
mdev -s
這裏對這幾個文件做一點說明:
busybox 作爲 linuxrc 啓動後, 會讀取 / etc/profile, 這裏面設置了一些環境變量和 shell 的屬性
根據 / etc/fstab 提供的掛載信息, 進行文件系統的掛載
busybox 會從 /etc/inittab 中讀取 sysinit 並執行, 這裏 sysinit 指向了 / etc/init.d/rcS
/etc/init.d/rcS 中 ,mdev -s 這條命令很重要, 它會掃描 / sys 目錄,查找字符設備和塊設備,並在 / dev 下 mknod
dev 目錄:
# bryant @ ubuntu in ~/Downloads/busybox-1.33.1/_install/dev [1:17:36]
$ sudo mknod console c 5 1
這一步很重要, 沒有 console 這個文件, 用戶態的輸出沒法打印到串口上
lib 目錄:拷貝 lib 庫,支持動態編譯的應用程序運行:
# bryant @ ubuntu in ~/Downloads/busybox-1.33.1/_install/lib [1:18:43]
$ cp /usr/aarch64-linux-gnu/lib/*.so* -a .
編譯內核
配置內核
linux 內核源碼可以在 github 上直接下載。
根據 arch/arm64/configs/defconfig 文件生成. config
make defconfig ARCH=arm64
將下面的配置加入. config 文件中
CONFIG_DEBUG_INFO=y
CONFIG_INITRAMFS_SOURCE="./root"
CONFIG_INITRAMFS_ROOT_UID=0
CONFIG_INITRAMFS_ROOT_GID=0
CONFIG_DEBUG_INFO 是爲了方便調試
CONFIG_INITRAMFS_SOURCE 是指定 kernel ramdisk 的位置,這樣指定之後 ramdisk 會直接被編譯到 kernel 鏡像中。
我們將之前製作好的根文件系統 cp 到 root 目錄下:
# bryant @ ubuntu in ~/Downloads/linux-arm64 on git:main x [1:26:56]
$ cp -r ../busybox-1.33.1/_install root
執行編譯
make ARCH=arm64 Image -j8 CROSS_COMPILE=aarch64-linux-gnu-
這裏指定 target 爲 Image 會只編譯 kernel, 不會編譯 modules, 這樣會增加編譯速度
啓動 qemu
下載 qemu
需要注意的,qemu 最好源碼編譯, 用 apt-get 直接安裝的 qemu 可能版本過低,導致無法啓動 arm64 內核。筆者是使用 4.2.1 版本的 qemu
apt-get install build-essential zlib1g-dev pkg-config libglib2.0-dev binutils-dev libboost-all-dev autoconf libtool libssl-dev libpixman-1-dev libpython-dev python-pip python-capstone virtualenv
wget https://download.qemu.org/qemu-4.2.1.tar.xz
tar xvJf qemu-4.2.1.tar.xz
cd qemu-4.2.1
./configure --target-list=x86_64-softmmu,x86_64-linux-user,arm-softmmu,arm-linux-user,aarch64-softmmu,aarch64-linux-user --enable-kvm
make
sudo make install
編譯完成之後,qemu 在 /usr/local/bin 目錄下
$ /usr/local/bin/qemu-system-aarch64 --version
QEMU emulator version 4.2.1
Copyright (c) 2003-2019 Fabrice Bellard and the QEMU Project developers
啓動 linux 內核
/usr/local/bin/qemu-system-aarch64 -m 512M -smp 4 -cpu cortex-a57 -machine virt -kernel
這裏對於參數做一些解釋:
-
-m 512M
內存爲 512M -
-smp 4
4 核 -
-cpu cortex-a57
cpu 爲 cortex-a57 -
-kernel
kernel 鏡像文件 -
-append
傳給 kernel 的 cmdline 參數。其中 rdinit 指定了 init 進程;nokaslr 禁止內核起始地址隨機化,這個很重要, 否則 GDB 調試可能有問題;console=ttyAMA0 指定了串口,沒有這一步就看不到 linux 的輸出; -
-nographic
禁止圖形輸出 -
-s
監聽 gdb 端口, gdb 程序可以通過 1234 這個端口連上來。
這裏說明一下 console=ttyAMA0 是怎麼生效的。
查看 linux 源碼可知 ttyAMA0 對應的是AMBA_PL011
這個驅動:
config SERIAL_AMBA_PL011_CONSOLE
bool "Support for console on AMBA serial port"
depends on SERIAL_AMBA_PL011=y
select SERIAL_CORE_CONSOLE
select SERIAL_EARLYCON
help
Say Y here if you wish to use an AMBA PrimeCell UART as the system
console (the system console is the device which receives all kernel
messages and warnings and which allows logins in single user mode).
Even if you say Y here, the currently visible framebuffer console
(/dev/tty0) will still be used as the system console by default, but
you can alter that using a kernel command line option such as
"console=ttyAMA0". (Try "man bootparam" or see the documentation of
your boot loader (lilo or loadlin) about how to pass options to the
kernel at boot time.)
AMBA_PL011 是 arm 的一個標準串口設備, qemu 的輸出就是模擬的這個串口。
在 qemu 的源碼文件中,也可以看到 PL011 的相關文件:
# bryant @ ubuntu in ~/Downloads/qemu-4.2.1 [1:46:54]
$ find . -name "*pl011*"
./hw/char/pl011.c
成功啓動 Linux 後, 串口打印如下:
[ 3.401567] usbcore: registered new interface driver usbhid
[ 3.404445] usbhid: USB HID core driver
[ 3.425030] NET: Registered protocol family 17
[ 3.429743] 9pnet: Installing 9P2000 support
[ 3.435439] Key type dns_resolver registered
[ 3.440299] registered taskstats version 1
[ 3.443685] Loading compiled-in X.509 certificates
[ 3.461041] input: gpio-keys as /devices/platform/gpio-keys/input/input0
[ 3.473163] ALSA device list:
[ 3.474432] No soundcards found.
[ 3.485283] uart-pl011 9000000.pl011: no DMA platform data
[ 3.541376] Freeing unused kernel memory: 10752K
[ 3.545897] Run /linuxrc as init process
[ 3.548390] with arguments:
[ 3.550279] /linuxrc
[ 3.551073] nokaslr
[ 3.552216] with environment:
[ 3.554396] HOME=/
[ 3.555898] TERM=linux
[ 3.985835] 9pnet_virtio: no channels available for device kmod_mount
mount: mounting kmod_mount on /mnt failed: No such file or directory
/etc/init.d/rcS: line 8: can't create /proc/sys/kernel/hotplug: nonexistent directory
Please press Enter to activate this console.
[root@bryant ]#
[root@bryant ]#
VSCode+GDB
vscode 中集成了 GDB 功能,我們可以用它來圖形化的調試 linux kernel
首先我們添加 vscode 的 gdb 配置文件 (.vscode/launch.json):
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "kernel debug",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/vmlinux",
"cwd": "${workspaceFolder}",
"MIMode": "gdb",
"miDebuggerPath":"/usr/bin/gdb-multiarch",
"miDebuggerServerAddress": "localhost:1234"
}
]
}
這裏對幾個重點參數做一些說明:
-
program
: 調試的符號文件 -
miDebuggerPath
:gdb 的路徑, 這裏需要注意的是,由於我們是 arm64 內核,因此需要用 gdb-multiarch 來進行調試 -
miDebuggerServerAddress
:對端地址,qemu 會默認使用 1234 這個端口
配置完成之後,可以直接啓動 GDB, 連接上 linux kernel
在 vscode 中,可以設置斷點,進行單步調試
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/eI6l4Ye0F-4BWeBqsLgSvw