瞭解和使用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。
其他參考資料
命令彙總
命令再次列出:
- 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:檢視記憶體內容
除錯指令碼補丁
- 反彙編
(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