1. 程式人生 > 其它 >瞭解和使用GDB除錯-基礎

瞭解和使用GDB除錯-基礎

目錄

啟動除錯

1. 哪些程式可以被除錯

  • 對於C和C++程式,編譯時加上-g引數,會保留除錯資訊,否則無法使用GDB進行除錯。

2. 如何判斷檔案是否可以除錯

  • 直接使用gdb 檔名執行,如果不可除錯則會有相應提示。
  • readelf檢視段資訊:readelf -S helloWorld|grep debug
  • file檢視strip狀況:file helloworld,如果最後提示為stripped,則說明檔案的符號表資訊和除錯資訊以及被去除。但是如果未 not stripped,也並不意味著一定可以進行gdb除錯。

3. 無參程式啟動除錯

$ gdb helloWorld
(gdb)
(gdb) run

輸入run命令,即可直接執行程式。

4. 帶參程式啟動除錯

  • 方式1:run命令時帶上所需引數即可
$ gdb hello
(gdb)run Argument1
  • 方式2:run命令前,使用set args
$ gdb hello
(gdb) set args Argument1
(gdb) run

5. 除錯core檔案

core檔案介紹:https://www.jianshu.com/p/e38a3f1cf7f7

  • Linux下程式異常退出時,核心在當前工作目錄下生成core檔案,記錄當時的記憶體映像和除錯資訊。可以使用gdb來檢視core檔案。
  • 產生core檔案的前提是編譯時帶上了-g引數,並且core檔案生成沒有收到限制。
  • 關注core檔案的生成開關和大小限制。
  • 關注core檔案的名詞和生成路徑。
$ gdb [exec file] [core file]

6. 除錯已執行程式

通過ps命令以執行的特點程式程序id

$ ps -ef|grep 程序名

或者可以自己編寫測試檔案,利用bg和fg來切換前後臺。

使用attach直接除錯相關程序id的程序,如果提示沒有許可權,可以sudo gdb

$ gdb
(gdb) attach 20829

7. 已執行程式且無除錯資訊

為了節省磁碟空間,已經執行的程式通常沒有除錯資訊。並且不能停止當前程式進行重新編譯除錯,此時可以利用同樣的程式碼,再編譯一個帶除錯資訊的版本。

$ gdb
(gdb) file hello
Reading symbols from hello...done.
(gdb)attach 20829

斷點設定

1. 檢視已設定的斷點

命令info breakpoints 檢視已設定的斷點。

2. 根據行號設定斷點

兩種方式任一:

b 9  #break 可簡寫為b
b test.c:9

3. 根據函式名設定斷點

當初程式呼叫到函式funcName時會斷點

b funcName

4. 根據條件設定斷點

特定行數 + 變數判斷 組成條件設定,如果該條件成立,則形成斷點可進行觀察:

break test.c:23 if b==0

含義為:當b等於0時,程式將在23行斷點。

condition命令有著類似作用:

condition 1 b==0 

含義為:當b等於0時,產生斷點1。

5. 根據規則設定斷點

#用法:rbreak file:regex
rbreak . 
rbreak test.c:. #對test.c中的所有函式設定斷點
rbreak test.c:^print #對以print開頭的函式設定斷點

6. 設定臨時斷點

在某處的斷點只生效一次,則可以設定臨時斷點:

tbreak test.c:l0  #在第10行設定臨時斷點

7. 跳過多次設定斷點

對於某個斷點處,前30次不會發生問題,可以跳過前30次:

ignore 1 30

其中1為通過info breakpoints查詢的斷點序號。

8. 根據表示式值變化來產生斷點

觀察某個特定表示式或者值,當其發生變化時,產生斷點並列印相關內容:

watch a  
# 當產生值變化時,會打印出
Hardware watchpoint 2: a
Old value = 12
New value = 11

rwatch和awatch同樣可以設定觀察點前者是當變數值被讀時斷住,後者是被讀或者被改寫時斷住。

9. 禁用、啟用、刪除斷點

對於暫時不需要使用,但是也不可刪除的斷點,可以選擇暫時禁用

disable  #禁用所有斷點
disable bnum #禁用標號為bnum的斷點
enable  #啟用所有斷點
enable bnum #啟用標號為bnum的斷點
clear   #刪除當前行所有breakpoints
clear function  #刪除函式名為function處的斷點
clear filename:function #刪除檔案filename中函式function處的斷點
clear lineNum #刪除行號為lineNum處的斷點
clear f:lename:lineNum #刪除檔案filename中行號為lineNum處的斷點
delete  #刪除所有breakpoints,watchpoints和catchpoints
delete bnum #刪除斷點號為bnum的斷點

變數檢視

1. 列印基本資料型別:變數、陣列、字串

使用print(可簡寫為p)列印變數內容:

(gdb) p a
$1 = 10
(gdb) p b
$2 = {1, 2, 3, 5}
(gdb) p c
$3 = "hello,shouwang"
(gdb)

可以在前面加上函式名或者檔名來區分同名變數:

(gdb) p 'testGdb.h'::a
$1 = 11
(gdb) p 'main'::b
$2 = {1, 2, 3, 5}
(gdb) 

2. 列印指標指向內容

  • 如果以列印普通變數的形式列印指標,則會打印出指標地址:
(gdb) p d
$1 = (int *) 0x602010
  • 若需要列印指標所指向的內容,需要解引用:
(gdb) p *d
$2 = 0
(gdb) p *d@10
$3 = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
(gdb) 

通過@符號跟上想要列印的長度。

  • $在gdb中為上一個變數
(gdb) p *linkNode
(這裡顯示linkNode節點內容)
(gdb) p *$.next
(這裡顯示linkNode節點下一個節點的內容)
  • 設定gdb變數和使用累加
(gdb) set $index=0
(gdb) p b[$index++]
$11 = 1
(gdb) p b[$index++]
$12 = 2
(gdb) p b[$index++]
$13 = 3

3. 按照特定格式列印變數

  • x 按十六進位制格式顯示變數。
  • d 按十進位制格式顯示變數。
  • u 按十六進位制格式顯示無符號整型。
  • o 按八進位制格式顯示變數。
  • t 按二進位制格式顯示變數。
  • a 按十六進位制格式顯示變數。
  • c 按字元格式顯示變數。
  • f 按浮點數格式顯示變數。
(gdb) p/x c
$19 = {0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x73, 0x68, 0x6f, 0x75, 0x77, 0x61, 
  0x6e, 0x67, 0x0}
(gdb)

4. 檢視記憶體內容和暫存器內容

examine(簡寫為x)可以用來檢視記憶體地址中的值:

x/[n][f][u] addr

其中:

  • n 表示要顯示的記憶體單元數,預設值為1
  • f 表示要列印的格式,前面已經提到了格式控制字元
  • u 要列印的單元長度
    • b 位元組
    • h 半字,即雙位元組
    • w 字,即四位元組
    • g 八位元組
  • addr 記憶體地址
(gdb) x/4tb &e  # &e開始的4塊位元組記憶體,以二進位制列印
0x7fffffffdbd4:    00000000    00000000    00001000    01000001
(gdb) 

命令info registers可以檢視暫存器內容:

(gdb)info registers
rax            0x0    0
rbx            0x0    0
rcx            0x7ffff7dd1b00    140737351850752
rdx            0x0    0
rsi            0x7ffff7dd1b30    140737351850800
rdi            0xffffffff    4294967295
rbp            0x7fffffffdc10    0x7fffffffdc10

5. 斷點時自動列印變數內容

若希望程式在斷點時自動列印某個變數的值,可以使用display命令:

(gdb) display e
1: e = 8.5

想要檢視哪些變數被設定了display:

(gdb)into display

想要清除:

delete display num #num為前面變數前的編號,不帶num時清除所有。

單步除錯

1. 列印當前除錯程式的原始碼

(gdb) list

2. 單步執行程式 next

若當前已經啟動除錯,停在某個斷點處,使用next命令(簡寫為n)可以繼續往下執行下一條語句。若跟上數字number,則表示執行number次:

$ gdb gdbStep   #啟動除錯
(gdb)b 25       #將斷點設定在12行
(gdb)run        #執行程式
Breakpoint 1, main () at gdbStep.c:25
25        int b = 7;
(gdb) n     #單步執行
26        printf("it will calc a + b\n");
(gdb) n 2   #執行兩次
it will calc a + b
28        printf("%d + %d = %d\n",a,b,c);
(gdb) 

3. 單步進入 step

若需要跟著進入函式內部檢視情況,可以使用step命令(簡寫為s),單步跟蹤導函式內部,前提為該函式有除錯資訊和原始碼資訊。

$ gdb gdbStep    #啟動除錯
(gdb) b 25       #在12行設定斷點
Breakpoint 1 at 0x4005d3: file gdbStep.c, line 25.
(gdb) run        #執行程式
Breakpoint 1, main () at gdbStep.c:25
25        int b = 7;
(gdb) s          
26        printf("it will calc a + b\n");
(gdb) s     #單步進入,但是並沒有該函式的原始檔資訊
_IO_puts (str=0x4006b8 "it will calc a + b") at ioputs.c:33
33    ioputs.c: No such file or directory.
(gdb) finish    #繼續完成該函式呼叫
Run till exit from #0  _IO_puts (str=0x4006b8 "it will calc a + b")
    at ioputs.c:33
it will calc a + b
main () at gdbStep.c:27
27        int c = add(a,b);
Value returned is $1 = 19
(gdb) s        #單步進入,現在已經進入到了add函式內部
add (a=13, b=57) at gdbStep.c:6
6        int c = a + b;

s命令會嘗試進入函式,但是如果沒有該函式原始碼,需要跳過該函式執行,可使用finish命令,繼續後面的執行。

  • s命令可以設定選項,選擇是否預設跳過沒有除錯資訊的函式:
(gdb) show step-mode 
Mode of the step operation is off.
(gdb) set step-mode on
(gdb) set step-mode off
  • s命令為每次執行一條程式語句,可以使用stepi(簡寫為si),每次執行一條機器指令。

4. 繼續執行到下一個斷點 continue

使用continue命令(可簡寫為c),會繼續執行當前程式,直到再次遇到斷點。

5. 繼續執行到指定行數位置 until

若我們希望在繼續執行直到特定行數停住,可以使用until命令(簡寫為u):

6. 跳過執行 skip

skip可以在step時跳過一些不想關注的函式或者某個檔案的程式碼:

$ gdb gdbStep
(gdb) b 27
Breakpoint 1 at 0x4005e4: file gdbStep.c, line 27.
(gdb) skip function add    # step時跳過add函式
Function add will be skipped when stepping.
(gdb) info skip   # 檢視step情況
Num     Type           Enb What
1       function       y   add
(gdb) run
Starting program: /home/hyb/workspaces/gdb/gdbStep 
it will calc a + b

Breakpoint 1, main () at gdbStep.c:27
27        int c = add(a,b);
(gdb) s
28        printf("%d + %d = %d\n",a,b,c);
(gdb)skip file gdbStep.c # 跳過檔案內的所有函式

其他相關命令:

  • skip delete [num] 刪除skip
  • skip enable [num] 使能skip
  • skip disable [num] 去使能skip

原始碼檢視

除錯過程中一般需要對照整體或者部分原始碼檢視,在GDB除錯下快速檢視原始碼或者對原始碼進行編輯。

1. 原始碼列印

  • 直接列印原始碼:list命令(簡寫l)
  • 列出指定行附近的原始碼:list命令 + 行號
(gdb) l 9
  • 列出指導函式附件的原始碼:list命令 + 函式名
  • 設定原始碼一次列出的行數,一般列印原始碼預設顯示10行。通過listsize屬性來設定。
(gdb) set listsize 20
(gdb) show listsize
  • 列出指定行之間區域的原始碼:list + 起始行號 + 結束行號
(gdb) l 3,15 # 列出3到15行之間的原始碼
  • 列出指導檔案的原始碼
(gdb) l test.c:1
(gdb) l test.c:printNum1
(gdb) l test.c:1,test.c:3

2. 指定原始碼路徑

檢視原始碼之前,需要先確保程式能夠關聯到原始碼檔案。但是當出現原始碼檔案移動等情況時,無法直接通過lsit命令檢視到原始碼。

  • 場景1:原始碼檔案移動

原始碼檔案 main.c 移動到temp目錄下,此時執行list命令

(gdb) l
1    main.c: No such file or directory.
(gdb) 

通過dir命令重新指定原始碼路徑:

(gdb) dir ./temp
Source directories searched: /home/hyb/workspaces/gdb/sourceCode/./temp:$cdir:$cwd
  • 場景2:更換原始碼目錄

全部原始碼檔案移動到了另一個目錄,可以使用上述場景1中的方式新增原始碼搜尋路徑,也可以使用set substitute-path from to將原來的路徑替換為新的路徑

通過readelf命令可以檢視原來原始碼路徑:

$ readelf main -p .debug_str
  [     0]  long unsigned int
  [    12]  short int
  [    1c]  /home/hyb/workspaces/gdb/sourceCode
  [    40]  main.c
(顯示部分內容)

替換路徑:

(gdb) set substitute-path /home/hyb/workspaces/gdb/sourceCode /home/hyb/workspaces/gdb/sourceCode/temp
(gdb) show substitute-path
List of all source path substitution rules:
  `/home/hyb/workspaces/gdb/sourceCode' -> `/home/hyb/workspaces/gdb/sourceCode/temp'.
(gdb)

可以通過unset substitute-path [path]取消替換。

3. 編輯原始碼

啟動除錯後,若有編輯原始碼的需求,可以直接在gdb模式下進行編輯原始碼。gdb預設使用的編輯器為/bin/ex,可以設定替換編輯器:

$ EDITOR=/usr/bin/vim
$ export EDITOR

gdb除錯模型下進行編輯原始碼,使用edit命令:

(gdb)edit 3  #編輯第三行
(gdb)edit printNum #編輯printNum函式
(gdb)edit test.c:5 #編輯test.c第五行

在vim編輯器下編輯儲存完後,可以直接重新編譯程式:

(gdb)shell gcc -g -o main main.c test.c

在gdb模式下執行shell命令,需要在命令前加上shell。

其他參考資料

命令彙總

https://www.toutiao.com/i6914639382385295886/?tt_from=weixin&utm_campaign=client_share&wxshare_count=1&timestamp=1637332354&app=news_article&utm_source=weixin&utm_medium=toutiao_ios&use_new_style=1&req_id=202111192232330101502202011138A213&share_token=5985B6A6-7323-40D4-897E-3CF9544B6CAD&group_id=6914639382385295886&wid=1637663728310

命令再次列出:

  • file:裝入除錯程式原始檔
  • kill:終止除錯程式
  • list:列印原始碼
  • break:設定斷點
  • run:執行程式
  • quit:推出gdb
  • step:單步進入
  • next:單步執行
  • continue:繼續執行,直到下一個斷點
  • print:列印變數等
  • watch:監視變數值的變化
  • display:每次斷點都會列印一次變數
  • start:開始執行程式,並在main函式的第一條語句前停下來
  • info:gdb相關資訊
  • set var name = value:設定變數的值
  • backtrace:檢視函式呼叫資訊(堆疊)
  • frame:檢視棧幀
  • return:強制函式返回
  • where:列出當前程式執行的位置
  • whatis:檢視變數、函式的型別
  • examine:檢視記憶體內容

除錯指令碼補丁

https://www.toutiao.com/i6836648226666316295/?tt_from=weixin&utm_campaign=client_share&wxshare_count=1&timestamp=1637326374&app=news_article&utm_source=weixin&utm_medium=toutiao_ios&use_new_style=1&req_id=20211119205254010151180208251D4D47&share_token=16A0B9FE-C93F-427F-B735-24C97F5A0096&group_id=6836648226666316295

  • 反彙編
(gdb) disassemble 函式名
  • GDB斷點預置命令

GDB提供了一種功能,對於指定的斷點,GDB允許使用者預設一組操作(通常是除錯命令),當斷點被觸發時,GDB會自動執行這組預設的操作。

先設定一個或者多個斷點;再利用commands命令進行預設操作(id為斷點id):

commands [id...]
  command-list
end
  • Debug過程中通過預置命令來快速修復已知的bug,避免多次編譯的繁瑣。
b *do_stuff
commands
  printf "\n ESI = %d\n",$esi
  set $esi=10
  printf "\n ESI = %d\n",$esi
  continue
end
# do_stuff函式入口處設定斷點,通過修改esi暫存器來修改函式入參傳遞。
  • 將上述可以寫為一個熱補丁檔案,test.fix.1.加入silent命令,遮蔽斷點被觸發時的列印資訊,避免視覺干擾。
# test.fix.1
b *do_stuff
commands
  slient
  set $esi=10
  continue
end
  • GDB重新除錯執行,-x引數載入指令碼檔案
gdb test -x test.fix.1