1 處於TUI模式的GDB
為了以TUI模式運行GDB,可以在調用GDB時在命令行上指定-tui選項,或者處於非TUI模式時在GDB中使用Ctrl+X+A組合鍵。如果當前處於TUI模式,后一種命令方式就會使你離開TUI模式。
在TUI模式中,GDB窗口划分為兩個子窗口——一個用於輸入GDB命令,而另一個用於查看源代碼。
例如:
源代碼為ins.c
#include <stdio.h> int x[10], y[10], num_inputs, num_y = 0; void get_args(int ac,char **av){ int i; num_inputs = ac - 1; for(i = 0;i < num_inputs;++i) x[i] = atoi(av[i + 1]); } void scoot_over(int jj){ int k; for(k = num_y;k > jj;--k) y[k] = y[k - 1]; } void insert(int new_y){ int j; // if(num_y==0){ y[0] = new_y; return; } for(j = 0;j < num_y;++j){ if(new_y < y[j]){ scoot_over(j); y[j] = new_y; return; } } y[num_y]=new_y; } void process_data(){ for(num_y = 0;num_y < num_inputs;++num_y) insert(x[num_y]); } void print_results(){ int i; for(i = 0;i < num_inputs;++i) printf("%d\n",y[i]); } int main(int argc,char **argv){ get_args(argc,argv); process_data(); print_results(); }
編譯后:
gcc -g3 -Wall -o insert_sort ins.c
注意,在GCC中可以用-g選項讓編譯器將符號表(即對應於程序的變量和代碼行的內存地址列表)保存在生成的可執行文件(這里是insert_sort)中。這是一個絕對必要的步驟,這樣才能在調試會話過程中引用源代碼中的變量名和行號。
使用GDB調試insert_sort
如果正在使用GDB但沒有使用TUI模式,則位於下方的子窗口確切地顯示你將看到的內容。此處,該子窗口顯示如下內容。
1)發出一條break命令,在當前源文件第12行處設置斷點。
2)執行run命令運行程序,並且向該程序傳遞命令行參數12、5、6.在此之后,調試器在指定的斷點處停止執行。GDB會提醒用戶斷點位於ins.c的第12行,並且通知該源代碼行的機器代碼駐留在內存地址0xbffff484中。
3)發出next命令,步進到下一個代碼行,即第13行。
2 主要的調試操作
退出GDB:quit或者Ctrl+d
執行程序:run
2.1 單步調試源代碼
安排程序的執行在某個地方暫停,以便檢查變量的值,從而得到關於程序錯誤所在位置的線索。
- 斷點
調試工具會在指定斷點處暫停程序的執行。在GDB中是通過break命令及其行號完成的。
普通斷點和條件斷點
(gdb) break 30
Breakpoint 1 at 0x80483fc: file ins.c,line 30.
(gdb) condition 1 num_y==1
第一個命令在第30行放置一個斷點。這里的第二個命令condition 1 num_y==1使得該斷點稱為有條件的斷點:只有當滿足條件num_y==1時,GDB才會暫停程序的執行。
注意,與接受行號(或函數名)的break命令不同,condition接受斷點號。總是可以用命令info break來查詢要查找的斷點的編號。
用break if可以將break和condition命令組合成一個步驟,如下所示:
(gdb) break 30 if num_y==1
- 單步調試
前面提到過,在GDB中next命令會讓GDB執行下一行,然后暫停。step命令的作用與此類型,只是函數調用時step命令會進入函數,而next導致程序執行的暫停出現在下次調用函數時。
- 恢復操作
在GDB中,continue命令通知調試器恢復執行並繼續,直到遇到斷點為止。
- 臨時斷點
在GDB中,tbreak命令與break相似,但是這一命令設置的斷點的有效期限只到首次到達指定行時為止。
2.2 檢查變量
(gdb) print j
$1=1
對GDB的這一查詢的輸出表明j的值為1.$1標簽意味着這是你要求GDB輸出的第一個值。($1、$2、$3等表示的值統稱為調試會話的值歷史。)
2.3 在GDB中設置監視點以應對變量值的改變
監視點結合了斷點和變量檢查的概念。最基本形式的監視點通知調試器,每當指定變量的值發生變化時都暫停程序的執行。
(gdb) watch z
當運行程序時,每當z的值發生變化,GDB都會暫停執行。
更好的方法是,可以基於條件表達式來設置監視點。例如,查找執行期間z 的值大於28的第一個位置
(gdb) watch(z>28)
2.4 上下移動調用棧
在函數調用期間,與調用關聯的運行時信息存儲在稱為棧幀的內存區域中。幀中包含函數的局部變量的值、其形參,以及調用該函數的記錄。每次發生函數調用時,都會創建一個新幀,並將其推導一個系統維護的棧上;棧最上方的幀表示當前正在執行的函數,當函數退出時,這個幀被彈出棧,並且被釋放。
在GDB中可用用如下命令查看以前的幀:
(gdb) frame 1
當執行GDB的frame命令時,當前正在執行的函數的幀被編號為0,其父幀(即該函數的調用者的棧幀)被編號為1,父幀的父幀被編號為2,以此類推。GDB的up命令將你帶到調用棧中的下一個父幀(例如,從幀0到幀1),down則引向相反方向。
顯示整個棧:backtrace
瀏覽以前的GDB命令:上一個Ctrl+P、下一個Ctrl+N
3 聯機幫助
在GDB中,可以通過help命令訪問文檔。例如:
(gdb) help breakpoints
4 啟動文件的使用
在重新編譯代碼時,最好不要退出GDB。這樣,你的斷點和建立的其他各種動作都會保留。要是退出GDB,就不得不再次重復鍵入這些內存。
然而,在完成調試前可能需要退出GDB。如果你要離開一段時間,而且不能保持登錄在計算機中,則需要退出GDB。為了不丟失它們,可以將斷點和設置的其他命令放在一個GDB啟動文件中,然后每次啟動GDB時都會自動加載它們。
GDB的啟動文件默認名為.gdbinit。
在調用GDB時可以指定啟動文件。例如,
$ gdb -command=z x
表示要在可執行文件x上運行GDB,首先要從文件z中讀取命令。
5 gdb暫停機制
有3種方式可以通知GDB暫停程序的執行。
1)斷點:通知GDB在程序中的特定位置暫停執行。
2)監視點:通知GDB當特定內存位置的值發生變化時暫停執行
3)捕獲點:通知GDB當特定事件發生時暫停執行。
GDB中使用delete命令刪除斷點:
(gdb) help delete
5.1 斷點概述
GDB中關於斷點“位置”的含義非常靈活,它可以指各種源代碼行、代碼地址、源代碼文件中的行號或者函數的入口等。
例如:
break 35
這里指GDB執行到第34行,但是第35行還沒有執行。斷點顯示的是將要執行的代碼行。
5.2 跟蹤斷點
程序員創建的每個斷點(包括斷點、監視點和捕獲點)都被標識為從1開始的唯一整數標識符。這個標識符用來執行該斷點上的各種操作。
5.2.1 GDB中的斷點列表
當創建斷點時,GDB會告知你分配給該斷點的編號。例如,
(gdb) break main
Breakpoint 1 at 0x8048569: file ins.c, line 52.
被分配的編號是1.如果忘記了分配給哪個斷點的編號是什么可以使用info breakpoints命令來提示。
(gdb) info breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y 0x08048569 in main at ins.c:52
2 breakpoint keep y 0x0804847e in insert at ins.c:25
3 breakpoint keep y 0x08048491 in insert at ins.c:30
如果想要刪除斷點,可以通過delete命令以及斷點標識符,例如
(gdb) delete 1 2 3
5.3 設置斷點
5.3.1 在GDB中設置斷點
1)break function
在函數function()的入口處設置斷點。
(gdb) break main
在main函數的入口處設置斷點
2)break line_number
在當前活動源代碼文件的line_number處設置斷點。
(gdb) break 35
在當前顯示的源文件的35行設置了一個斷點。
3)break filename:line_number
在源代碼文件filename的line_number處設置斷點。如果filename不在當前工作目錄中,則可以給出相對路徑名或者完全路徑名來幫助GDB查找該文件,例如:
(gdb) break source/bed.c:35
4)break filename:function
在文件filename中的函數function()的入口處設置斷點。重載函數或者使用同名靜態函數的程序可能需要使用這種形式,例如:
(gdb) break bed.c:parseArguments
正如我們看到的,當設置一個斷點時,該斷點的有效性會持續到刪除、禁用或者退出GDB時。然而,臨時斷點時首次到達后就會被自動刪除的斷點。臨時斷點使用tbreak命令設置。
C++允許重載函數,使用break function會在所有具有相同名稱的函數上設置斷點。如果要在函數的某個特定實例上設置斷點,需要沒有歧義,則使用源文件中的行號。
int main(void) { int i; i = 3; return 0; }
如果我們嘗試在函數main入口處設置斷點,斷點實際會被設置在第4行。因為GDB會認為第三行的機器碼對我們的調試目的來說沒有用處。
5.4 多文件中的斷點設置
例如:
main.c
#include<stdio.h> void swap(int *a,int *b); int main() { int i=3; int j=5; printf("i:%d,j:%d\n",i,j); swap(&i,&j); printf("i:%d,j:%d\n",i,j); return 0; }
swap.c
void swap(int *a,int *b) { int c=*a; *a=*b; *b=c; }
在main上設置斷點:
(gdb) break main
Breakpoint 1 at 0x80483cd: file main.c, line 6.
在swap上設置斷點的方法:
(gdb) break swapper.c:1
Breakpoint 2 at 0x804843a: file swapper.c, line 1.
(gdb) break swapper.c:swap
Note: breakpoint 2 also set at pc 0x804843a.
Breakpoint 3 at 0x804843a: file swapper.c, line 3.
(gdb) break swap
Note: breakpoints 2 and 3 also set at pc 0x804843a.
Breakpoint 4 at 0x804843a: file swapper.c, line 3.
每個GDB都有一個焦點,可以將它看作當前“活動”文件。這意味着除非對命令做了限定,否則都是在具有GDB的焦點的文件上執行命令。默認情況下,具有GDB的初始焦點的文件是包含main()函數的文件,但是當發生如下任一動作時,焦點會轉移到不同的文件上。
1)向不同的源文件應用list命令
2)進入位於不同的源代碼文件中的代碼
3)當在不同的源代碼文件中執行代碼時GDB遇到斷點
例如:
(gdb) break 6
Note: breakpoint 1 also set at pc 0x80483cd.
Breakpoint 5 at 0x80483cd: file main.c, line 6.
當前焦點是main.c,所以在main.c中設置。
(gdb) list swap
(gdb) break 6
Breakpoint 6 at 0x8048454: file swapper.c, line 6.
現在的焦點是swapper.c。
(gdb) info breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y 0x080483cd in main at main.c:6
2 breakpoint keep y 0x0804843a in swap at swapper.c:1
3 breakpoint keep y 0x0804843a in swap at swapper.c:3
4 breakpoint keep y 0x0804843a in swap at swapper.c:3
5 breakpoint keep y 0x080483cd in main at main.c:6
6 breakpoint keep y 0x08048454 in swap at swapper.c:6
5.5 斷點的持久性
如果在修改和重新編譯代碼時沒有退出GDB,那么在下次執行GDB的run命令時,GDB會感知到代碼已修改,並自動重新加載新版本。
5.6 刪除和禁用斷點
在調試會話期間,有時會發現有的斷點不再使用。如果確認不再需要斷點,可以刪除它。也許你不想刪除它,而是打算將它虛置起來,這稱為禁用斷點。如果以后再次需要,可以重新啟用斷點。
2.6.1 在GDB中刪除斷點
如果確認不再需要當前斷點,那么可以刪除該斷點。
delete命令用來基於標識符刪除斷點,clear命令使用和創建斷點的語法刪除相同。
1)delete breakpointer_list
刪除斷點使用數值標識符。斷點可以是一個數字,比如delete 2 刪除第2個斷點;也可以是數字列表,不然delete 2 4 刪除第二個和第四個斷點。
2)delete
刪除所有斷點。
3)clear
清除GDB將執行的下一個指令處的斷點。這種方法適用於要刪除GDB已經到達的斷點額情況。
4)clear function、clear filename:function、clear line_number和clear filename:line_number
2.6.2 在GDB中禁用斷點
每個斷點都可以禁用和啟用。只有遇到啟用的斷點時,才會暫停程序的執行;它會忽略禁用的斷點。
為什么要禁用斷點呢?在調試會話期間,會遇到大量斷點。對於經常重復的循環結構或函數,這種情況使得調試極不方便。如果要保留斷點以便以后使用,暫時又不希望GDB停止執行,可以禁用它們,在以后需要時再啟用。
使用disable breakpoint-list命令禁用斷點,使用enable breakpoint-list命令啟用斷點。
例如,
(gdb) disable 3
將禁用第三個斷點
(gdb) enable 1 5
將啟用第一個和第五個斷點。
不帶任何參數地執行disable命令將禁用所有現有斷點。類似的,不帶任何參數的enable命令將啟用所有斷點。
還有一個enable once命令,在得到下次引起GDB暫停執行后被禁用。語法為:
enable once breakpoint-list
例如,enable once 3 會使得斷點3 在下次導致GDB停止程序的執行后被禁用。這個命令與tbreak命令非常類似,但是當遇到斷點時,它是禁用斷點,而不是刪除斷點。
2.6.3 瀏覽斷點屬性
info breakpoints命令(簡寫 i b)來獲得設置的所有斷點的清單,以及它們的屬性。
例如:
(gdb) info breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y 0x080483cd in main at main.c:6
2 breakpoint keep y 0x0804843a in swap at swapper.c:1
3 breakpoint keep y 0x0804843a in swap at swapper.c:3
4 breakpoint keep y 0x0804843a in swap at swapper.c:3
5 breakpoint keep y 0x080483cd in main at main.c:6
6 breakpoint keep y 0x08048454 in swap at swapper.c:6
7 hw watchpoint keep y counter
讓我們分析info breakpoints的這一輸出:
1)標識符(num):斷點的唯一標識符
2)類型(type):這個字段指出該斷點是斷點、監視點還是捕獲點
3)部署(disp):每個斷點都有一個部署,指示斷點下次引起GDB暫停程序的執行后該斷點上會發生什么事情。
保持(keep),下次到達斷點后不改變斷點
刪除(del),下次到達斷點后刪除斷點,臨時斷點(tbreak設置)
禁用(dis),下次到達后會禁用斷點,使用enable once命令設置的
4)啟用狀態(enb):這個字段說明斷點當前是啟用還是禁用的
5)地址(Address):這是內存中設置斷點的位置。
6)位置(what):what字段顯示了斷點所在的位置的行號和文件名
6 恢復執行
恢復執行的方法有3類。第一類是使用step和next“單步”調試程序,僅執行代碼的下一行然后再次暫停。第二類由使用continue組成,使GDB無條件地恢復程序的執行,直到遇到另一個斷點或程序結束。最后一類方法涉及條件:用finish或until命令恢復。在這種情況下,GDB會恢復執行;程序繼續運行直到遇到某個預先確定的條件(比如,到達函數的末尾),到達另一個斷點,或者程序完成。
6.1 使用step和next單步調試
一旦GDB在斷點處停止,可以使用next(簡寫n)和step(簡寫s)命令來單步調試代碼。
這兩個命令的不同之處在於它們如何處理函數調用:next執行函數,不會在其中暫停,然后在調用之后的第一條語句處暫停。而step在函數中的第一個語句處暫停。step命令會進入調用的函數,這稱為單步進入函數。而next永遠不會離開main()。這是兩個命令的主要區別。next將函數調用看做一行代碼,並在一個操作中執行整個函數,這稱為單步越過函數。
然而,似乎next越過調用的函數主體,但是它並未真的單步“越過”任何內容。GDB安靜地執行調用函數的每一行,不向我們展示細節。
6.2 使用continue恢復程序執行
第二種恢復執行的方法是使用continue命令,簡寫為c。這個命令使GDB恢復程序的執行,直到觸發斷點或者程序結束。
continue命令可以接受一個可選的整數參數n。這個數字要求GDB忽略下面n個斷點。例如,continue 3讓GDB恢復程序執行,並忽略接下來的3個斷點。
6.3 使用finish恢復程序執行
一旦觸發了斷點,就使用next和step命令逐行執行程序。有時這是一個痛苦的過程。
有時使用step進入的調用的函數,查看了幾個變量的信息,如果沒有興趣單步調試其余部分,想返回到單步進入被調用函數之前GDB所在的調用函數。然而,如果要做的只是跳過函數的其余部分,那么再設置一個無關斷點並使用continue似乎比較浪費。這是可以使用finish命令。
finish命令(簡寫為fin)指示GDB恢復執行,直到恰好在當前棧幀完成之后為止。也就是說,這意味着如果你在一個不是main()的函數中,finish命令會導致GDB恢復執行,直到恰好在函數返回之后為止。
雖然可以鍵入next 3 而不是finish,但是后者更容易。
finish的另一個常見用途是當不小心單步進入原本希望單步越過的函數時(換言之,當需要使用next時使用了step)。在這種情況下,使用finish可以講你正好放回到使用next會位於的位置。
如果在一個遞歸函數中,finish只會將你帶到遞歸的上一層。
6.4 使用until恢復程序執行
finish命令在不進一步在函數中暫停(除了中間斷點)的情況想完成當前函數的執行。類似地,until命令(簡寫為u)通常用來在不進一步在循環中暫停(除了循環中的中間斷點)的情況下完成正在執行的循環。
當i很大是,使用next需要多次。而使用until會執行循環的其余部分,讓GDB在循環后面的第一行代碼處暫停。當然,如果GDB在離開循環前遇到一個斷點,它就會在那里暫停。
7 條件斷點
只要啟用了斷點,調試器就總是在該斷點處停止。然而,有時有必要告訴調試器只有當符合某種添條件時才在斷點處停止。
7.1 設置條件斷點
break break-args if (condition)
其中brea-args是可以傳遞給break以指定斷點位置的任何參數。括着condition的圓括號是可選的。
例如:
break main if argc>1
例如,在循環中,滿足一定次數之后發生中斷:
break if (i==7000)
條件中斷中的condition可以包含如下形式,但是必須是布爾值:
可以對正常斷點設置條件以將它轉變為條件斷點。例如,如果設置了斷點3為無條件斷點,但是希望添加添加i==3,只有鍵入:
(gdb) cond 3 i==3
如果以后要刪除條件,但是保持該斷點,只要鍵入:
(gdb) cond 3
8 斷點命令列表
當GDB遇到斷點時,幾乎總是要查看某個變量。如果反復遇到同一個斷點,將反復查看相同的變量。讓GDB在每次到達某個斷點時自動執行一組命令,從而自動完成這一過程。
事實上,使用“斷點命令列表”就可以做這件事。
使用commands命令設置命令列表。
其中breakpoint-number是要將命令添加到其上的斷點的標識符,commands是用行分隔的任何有效GDB命令列表。逐條輸入命令,然后鍵入end表示輸入命令完畢。從那以后,每當GDB在這個斷點處中斷時,它都會執行輸入的任何命令。
例如:
fibonacci.c
#include<stdio.h> int fibonacci(int n); int main(void) { printf("Fibonacci(3) is %d\n",fibonacci(3)); return 0; } int fibonacci(int n) { if(n<=0||n==1) return 1; else return fibonacci(n-1)+fibonacci(n-2); }
gdb調試:
如果覺得輸出太冗長了,可以使用silent命令使GDB更安靜地觸發斷點。
現在輸出結果不錯,但是每次要鍵入continue,可以修改如下:
也可以使用define定義宏來代替:
9 監視點
監視點是一種特殊類型的斷點,它類似於正常斷點,是要求GDB暫停程序執行的指令。監視點是指示GDB每當某個表達式改變了值就暫停執行的指令。
(gdb) watch i
它會使得每當i改變值時GDB就暫停。
9.1 設置監視點
當變量var存在且在作用域中時,可以通過使用如下命令來設置監視點。
watch var
該命令會導致每當var改變值時GDB都中斷。
例如:
#include<stdio.h> int i=0; int main() { i=3; printf("i is %d.\n",i); i=5; printf("i is %d.\n",i); return 0; }
我們每當i大於4時得到通知。因此在main()的入口處放一個斷點,以便讓i在作用域中,並設置一個監視點以指出i何時大於4.不能在i上設置監視點,因為在程序運行之前,i不存在。因此必須現在main()上設置斷點,然后在i上設置監視點。
既然i已經在作用域中了,現在設置監視點並通知GDB繼續執行程序。
10 顯示數值中的值
比如聲明數組:
int x[25];
方法是通過鍵入:
(gdb) p x
但是,如果是動態創建的數組會是什么樣呢?比如:
int *x
...
x=(int *)malloc(25*sizeof(int));
如果要在GDB中輸出數組,就不能輸入:
(gdb) p x
可以簡單打印數組地址。或者鍵入:
(gdb) p *x
這樣只會輸出數組的一個元素——x[0]。仍然可以像在命令 p x[5]中那樣輸入單個元素,但是不能簡單地在x上使用print命令輸出整個數組。
1)在GDB的解決方案
在GDB中,可以通過創建一個人工數組來解決這個問題。如下:
#include<stdio.h> #include<stdlib.h> int *x; void main() { x=(int*)malloc(25*sizeof(int)); x[3]=12; }
然后執行:
我們可以看到,一般形式為:
*pointer@number_of_elements
GDB還允許在適當的時候使用類型強制轉換,比如:
(gdb) p (int [25])*x
$2={0,0,0,12,0 <repeats 21 times>}