天天看點

掌握GDB調試工具,輕松排除bug

作者:嵌入式Linux核心

一、什麼是GDB

gdb是GNU debugger的縮寫,是程式設計調試工具。

  • GDB官網: https://www.gnu.org/software/gdb/​​
  • GDB适用的程式設計語言: Ada / C / C++ / objective-c / Pascal 等。
  • GDB的工作方式: 本地調試和遠端調試。

目前release的最新版本為8.0,GDB可以運作在Linux 和Windows 作業系統上。

1.1安裝與啟動GDB

  1. gdb -v 檢查是否安裝成功,未安裝成功則安裝(必須確定編譯器已經安裝,如 gcc) 。
  2. 啟動 gdb
    1. gdb test_file.exe 來啟動 gdb 調試, 即直接指定需要調試的可執行檔案名
    2. 直接輸入 gdb 啟動,進入 gdb 之後采用指令 file test_file.exe 來指定檔案名
    3. 如果目标執行檔案要求出入參數(如 argv[] 接收參數),則可以通過三種方式指定參數:
      1. 在啟動 gdb 時,gdb --args text_file.exe
      2. 在進入gdb 之後,運作 set args param_1
      3. 在 進入 gdb 調試以後,run param_1 或者 start para_1

1.2gdb的功能

  • 啟動程式,可以按照使用者自定義的要求随心所欲的運作程式。
  • 可讓被調試的程式在使用者所指定的調試斷點處停住(斷點可以是條件表達式)。
  • 當程式停住時,可以檢查此時程式中所發生的事。比如,可以列印變量的值。
  • 動态改變變量程式的執行環境。

1.3gdb的使用

運作程式

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.5gdb調試段錯誤

什麼是段錯誤?段錯誤是由于通路非法位址而産生的錯誤。

  • 通路系統資料區,尤其是往系統保護的記憶體位址寫資料。比如:通路位址為0的位址。
  • 記憶體越界(數組越界,變量類型不一緻等)通路到不屬于目前程式的記憶體區域。

gdb調試段錯誤,可以直接運作程式,當程式運作崩潰後,gdb會列印運作的資訊,比如:收到了SIGSEGV信号,然後可以使用bt指令,列印棧回溯資訊,然後根據程式發生錯誤的代碼,修改程式。

1.6.core檔案調試

6.1 core檔案

在程式崩潰時,一般會生成一個檔案叫core檔案。core檔案記錄的是程式崩潰時的記憶體映像,并加入調試資訊,core檔案生成過程叫做core dump(核心已轉儲)。系統預設不會生成該檔案。

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調試工具,輕松排除bug

[核心資料領取,](https://docs.qq.com/doc/DTmFTc29xUGdNSnZ2)

[Linux核心源碼學習位址。](https://ke.qq.com/course/4032547?flowToken=1044435)

二、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 會禁用目前程式中所有的斷點。

激活斷點

  1. enable [breakpoints] [num...]激活用 num... 參數指定的多個斷點,如果不設定 num...,表示激活所有禁用的斷點
  2. enable [breakpoints] once num… 臨時激活以 num... 為編号的多個斷點,但斷點隻能使用 1 次,之後會自動回到禁用狀态
  3. enable [breakpoints] count num... 臨時激活以 num... 為編号的多個斷點,斷點可以使用 count 次,之後進入禁用狀态
  4. enable [breakpoints] delete num… 激活 num.. 為編号的多個斷點,但斷點隻能使用 1 次,之後會被永久删除。

break(b): 打的是普通斷點,打斷點有兩種形式

(gdb) break location // b location,location 代表打斷點的位置

掌握GDB調試工具,輕松排除bug

(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調試工具,輕松排除bug
  • (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 種:

  1. 通過棧幀的編号指定。0 為目前被調用函數對應的棧幀号,最大編号的棧幀對應的函數通常就是 main() 主函數;
  2. 借助棧幀的位址指定。棧幀位址可以通過 info frame 指令(後續會講)列印出的資訊中看到;
  3. 通過函數的函數名指定。注意,如果是類似遞歸函數,其對應多個棧幀的話,通過此方法指定的是編号最小的那個棧幀。

(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 <regexp>
  • reverse-search <regexp>
    • 第一項指令格式表示從目前行的開始向前搜尋,後一項表示從目前行開始向後搜尋。其中 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實戰

下面是一個使用了上述指令的實戰例子:

[[email protected] 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是沒法“逆轉”的。

[[email protected] 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.2VSCode+GDB+Qemu調試ARM64 linux核心

linux kernel是一個非常複雜的系統,初學者會很難入門。如果有一個友善的調試環境,學習效率至少能有5-10倍的提升。

為了學習linux核心,通常有這兩個需要:

  1. 可以擺脫硬體,友善的編譯和運作linux
  2. 可以使用圖形化的工具來調試linux

筆者使用VSCode+GDB+Qemu完成了這兩個需求:

  • qemu作為虛拟機,用來啟動linux。
  • VSCode+GDB作為調試工具,用來圖形化地DEBUG。

最終效果大緻如下:

qemu運作界面:

掌握GDB調試工具,輕松排除bug

vscode調試界面:

掌握GDB調試工具,輕松排除bug

下面将一步一步介紹如何搭建上述環境。本文所有操作都在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           

這裡對這幾個檔案做一點說明:

  1. busybox 作為linuxrc啟動後, 會讀取/etc/profile, 這裡面設定了一些環境變量和shell的屬性
  2. 根據/etc/fstab提供的挂載資訊, 進行檔案系統的挂載
  3. busybox 會從 /etc/inittab中讀取sysinit并執行, 這裡sysinit指向了/etc/init.d/rcS
  4. /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-a57cpu 為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

掌握GDB調試工具,輕松排除bug

在vscode中,可以設定斷點,進行單步調試

掌握GDB調試工具,輕松排除bug
掌握GDB調試工具,輕松排除bug

繼續閱讀