Author:gnuhpc
WebSite:blog.csdn.net/gnuhpc
1.讓程序停下來的三種模式
- 斷點(breakpoint):讓程序在特定的地點停止執行。
- 觀察點(watchpoint):讓程序在特定的內存地址(或者是一個涉及多個地址的表達式)的值發生變化時停止執行。注意,你不能給一個尚沒有在棧幀中的表達式或變量設定觀察點,換句話說,常常在程序停下來後纔去設置觀察點。在設定觀察點後,棧幀中不存在所監控的變量時,觀察點自動刪除。
- 捕捉點(catchpoint):讓程序在發生特定事件時停止執行。
注:
- GDB文檔中統稱這三種程序暫停手段爲breakpoint,例如在GDB的delete命令的幫助手冊中就是這麼描述的,它實際上指代的是這三種暫停手段,本文中以breakpoints統稱三種模式,以中文進行分別稱呼。
- GDB執行程序到斷點(成爲斷點被hit)時,它並沒有執行斷點指向的那一行,而是將要指向斷點指向的那一行。
- GDB是以機器指令爲單位進行執行的,並非是以程序代碼行來進行的,這個可能會帶來一些困惑,下文有例子詳述。
2.GDB breakpoints的查看
命令:i b = info breakpoints。返回列表每一列的含義如下:
- Identifier :breakpoints的唯一標識。
- Type :該breakpoints屬於上述三種模式中的哪一個(breakpoint, watchpoint, catchpoint)
- Disposition:該breakpoints下次被hit以後的狀態(keep,del,dis分別對應保留、刪除、不使能)
- Enable Status:該breakpoints是否使能。
- Address:該breakpoints物理地址。
- Location :若屬於斷點則指的是斷點在哪個文件的第幾行,若是觀察點則指的是被觀察的變量
3.GDB 程序控制的設置
- 斷點設置:
- 設置普通斷點:break function/line_number/filename:line_number/filename:function. 該斷點在被刪除或不使能前一直有效。
- 設置臨時斷點:tbreak function/line_number/filename:line_number/filename:function. 該斷點在被hit一次後自動刪除。
- 設置一次性斷點:enable once breakpoint-list,這個與臨時斷點的不同是該斷點會在被hit一次後不使能,而不是刪除。
- 設置正則表達式斷點:rbreak regexp 注意該正則表達式是grep型的正則,不是perl或shell的正則語法。
- 設置條件斷點:break break-args if (condition) ,例如break main if argc > 1。
- 這個與觀察點不同的是,觀察點只要所觀察的表達式或變量的值有變化程序就停下,而條件斷點必須滿足所指條件。條件斷點在調試循環的時候非常有用,例如break if (i == 70000) 。
- 在已經添加的斷點上加上條件使用cond id condition,例如:cond 3 i == 3;想去掉條件轉化爲普通斷點則直接使用cond id,例如,cond 3。
- 注意,這裏的條件外的括號有沒有都行,條件中可以使用<, <=, ==, !=, >, >=, &&, ||,&, |, ^, >>, <<,+, -, x, /, %等運算符,也可以使用方法,例如:break test.c:myfunc if ! check_variable_sanity(i),這裏的方法返回值一定要是int,否則該條件就會被誤讀。
- 刪除斷點:
- delete breakpoint_list 列表中爲斷點的ID,以空格隔開
- delete 刪除全部斷點
- clear 刪除下一個GDB將要執行的指令處的斷點
- clear function/filename:function/line_number/filename:line_number 刪除特定地點的斷點
- 使能斷點:enable breakpoint-list
- 不使能斷點:disable breakpoint-list
- 跳過斷點:ignore id numbers 表示跳過id表示的斷點numbers次。
- 注意:
- 若設置斷點是以函數名進行的話,C++中函數的重載會帶來麻煩,該同名函數會都被設置上斷點。請使用如下格式在C++中進行函數斷點的設置:TestClass::testFunc(int)
- 設置的斷點可能並非是你想放置的那一行。例如:
1: int main(void)
2: {
3: int i;
4: i=3;
5: return 0;
6: }
我們不使用編譯器優化進行編譯,然後加載到GDB中,如下:
$ gcc -g3 -Wall -Wextra -o test1 test1.c
$ gdb test1
(gdb) break main
Breakpoint 1 at 0x6: file test1.c, line 4.
我們發現顯然#4並非是main函數的入口,這是因爲這一行是該函數第一行雖然產生了機器碼,但是GDB並不認爲這對調試有幫助,於是它就將斷點設置在第一行對調試有幫助的代碼上。我們使用編譯器優化再進行編譯,情況會更加令人困惑,如下:
$ gcc -O9 -g3 -Wall -Wextra -o test1 test1.c
$ gdb test1
(gdb) break main
Breakpoint 1 at 0x3: file test1.c, line 6.GCC發現i變量一直就沒有使用,所以通過優化直接忽略了,於是程序第一行產生機器碼的代碼恰好是main函數的最後一行。
因此,建議在不影響的情況下,程序調試時將編譯器優化選項關閉。
- 同一行有多個斷點時,程序只會停下一次,實際上GDB使用的是其中ID最小的那個。
- 在多文件調試中,常常希望GDB在並非當前文件的部分設置斷點,而GDB默認關注的是含有main函數的文件,此時你可以使用list functionname、或者單步調試等方式進入另一個文件的源代碼中進行設置,此時你設置的行號就是針對這個文件的源代碼了。
- 當你利用代碼行進行斷點設置時,重新編譯程序並在GDB中reload後,斷點可能因爲你代碼行數的變化而發生相對位置變化(GDB指向的行數),這樣的情況下使用DDD直接對原斷點進行拖動是最方便的方法,它不會影響該斷點的狀態和條件,只會改變它所指的位置,從而省去了del一個斷點後再在新位置添加設置新斷點的麻煩。
- 在DDD中還可以Redo和Undo對斷點的操作。
- 設置寫觀察點:watch i ; watch (i | j > 12) && i > 24 && strlen(name) > 6,這是兩種觀察點設置的方式(變量或表達式)。寫觀察點在該變量的值被程序修改後立刻中止。注意,很多平臺都有硬件支持的觀察點,默認GDB是優先使用的就是這些,若暫時不可用,GDB會使用VM技術實現觀察點,這樣的好處是硬件的速度較快。
- 設置讀觀察點:rwatch。
- 設置讀寫觀察點:awatch
- 舉例:下列簡單程序,可以首先在main函數入口設置斷點,然後在該斷點被hit時設置觀察點。
1: #include <stdio.h>
2:
3: int main(int argc, char **argv)
4: {
5: int x = 30;
6: int y = 10;
7:
8: x = y;
9:
10: return 0;
11: }
這是個非常簡單的程序,在main函數入口處斷點被hit後我們可以設置rwatch x進行變量監視。
- 單步執行:
- 單步跳過:n = next跳過調用方法的細節,將該行視爲一行代碼進行執行。next 3表示連續執行三次next。
- 單步進入:s = step 進入調用方法的細節。
- 執行到下一斷點:c = continue,程序繼續執行直到hit下一個斷點。
- 執行到下一棧幀:fin = finish,程序繼續執行直到當前棧幀完成。這個常常被用來完成所謂step out的工作,在你不小心按到了step時(你本意其實是想單步跳過),你就可以使用finish跳出該方法。當然,如果你進入了一個迭代函數中的多層以內,可能一個臨時斷點+continue或者until會更加有用,後者見下文。
- 執行到具有更高內存地址的機器指令:u = until (後邊可以跟上funtionname/linenumber),應用的場景見下邊的代碼,在我們進入了這個循環後我們想跳出來執行循環後的代碼,此時我們當然可以在循環後的第一行代碼設置臨時斷點,然後continue到那,但這個操作會比較麻煩,最好的方式是使用until,該命令使程序繼續運行知道遇到一個具有更高內存地址的機器指令時停止,而在循環被編譯成機器指令時,會將循環條件放在循環體的最底部,所以利用until正好跳出循環進入下一機器指令(P.S. 你可以使用GCC的-s查看生成的機器指令以便更好的理解這個命令的運行方式):
1: ...previous code...
2: int i = 9999;
3: while (i--) {
4: printf("i is %d/n", i);
5: ... lots of code ...
6: }
7: ...future code...
- 這是GDB7以後新加入的功能,如果你在調試的時候發現自己已經錯過了想調試的地方,這個操作可以使你不必重新開始調試而直接返回已經執行過的代碼進行調試。我們使用下邊一個非常簡單的程序對這個新版本的功能加以說明:
1: #include <stdio.h>
2: void foo() {
3: printf("inside foo()");
4: int x = 6;
5: x += 2;
6: }
7:
8: int main() {
9: int x = 0;
10: x = x+2;
11: foo();
12: printf("x = %d/n", x);
13: x = 4;
14: return(0);
15: }
16:
我們編譯一下然後在main函數處設置斷點,然後使用record命令開始記錄,這是使用反向調試必須的開始步驟,最後進行兩步單步調試,打印x的值:(gdb) b main
Breakpoint 1 at 0x804840d: file test.c, line 9.
(gdb) record
Process record: the program is not being run.
(gdb) r
Starting program: /home/gnuhpc/testBreakpoint 1, main () at test.c:9
9 int x = 0;
(gdb) record
(gdb) n
10 x = x+2;
(gdb)
11 foo();
(gdb) print x
$1 = 2
此時x=2,現在我們反向一步單步調試,打印x的值:(gdb) reverse-next
10 x = x+2;
(gdb) p x
$2 = 0
這正是我們想要的。對於斷點,反向調試也支持類似正向調試時的continue語句,我們在15行設置斷點,然後continue到這個斷點:(gdb) b 15
Breakpoint 2 at 0x8048441: file test.c, line 15.
(gdb) c
Continuing.
inside foo()x = 2Breakpoint 2, main () at test.c:15
15 }
此時我們在foo函數處加上斷點,然後反向continue:(gdb) b foo
Breakpoint 3 at 0x80483ea: file test.c, line 3.
(gdb) reverse-continue
Continuing.Breakpoint 3, foo () at test.c:3
3 printf("inside foo()");
程序回到foo入口處。網上有文獻反向調試指出必須使用軟件觀察點,事實並非如此:
(gdb) watch x
Hardware watchpoint 4: x
(gdb) reverse-continue
Continuing.
Hardware watchpoint 4: x
Old value = 6
New value = 134513384
foo () at test.c:4
4 int x = 6;
(gdb) n
Hardware watchpoint 4: x
Old value = 134513384
New value = 6
foo () at test.c:5
5 x += 2;
- 由於篇幅有限我們在此提供手冊上的
- 在程序中止執行時,用戶可能會進行一系列操作,GDB提供了這個操作的自動化,類似於批處理腳本一樣將需要操作的命令進行批量執行。語法爲:
commands breakpoint-id
...
commands
...
end - 例如我們調試斐波那契數列的程序:
1: #include <stdio.h>
2: int fibonacci(int n);
3: int main(void)
4: {
5: printf("Fibonacci(3) is %d./n", fibonacci(3));
6: return 0;
7: }
8: int fibonacci(int n)
9: {
10: if(n<=0||n==1)
11: return 1;
12: else
13: return fibonacci(n-1) + fibonacci(n-2);
14: }
由於這是一個遞歸函數,我們爲了追尋遞歸,要查看程序以什麼順序調用fibonacci()傳入的值,當然你可以使用printf進行查看,只是這個方法看起來很土。我們如下進行調試:(gdb) break fibonacci然後設置命令列表:(gdb) command 1
Type commands for when breakpoint 1 is hit, one per line.
End with a line saying just "end".
>silent
>printf "fibonacci was passed %d./n", n
>continue
>end
(gdb) run
Starting program: fibonacci
fibonacci was passed 3.
fibonacci was passed 2.
fibonacci was passed 1.
fibonacci was passed 0.
fibonacci was passed 1.
Fibonacci(3) is 3.
Program exited normally.
(gdb)
- 這裏就能很清晰地看到函數被調用的情況了。當然,你可以將上述命令列表寫成一個宏(最多支持10個傳入參數,我們使用了兩個):
(gdb) define print_and_go
Redefine command "print_and_go"? (y or n) y
Type commands for definition of "print_and_go".
End with a line saying just "end".
>printf $arg0, $arg1
>continue
>end
使用的時候非常方便:
(gdb) commands 1
Type commands for when breakpoint 1 is hit, one per line.
End with a line saying just "end".
>silent
>print_and_go "fibonacci() was passed %d/n" n
>end
附註:後文將介紹catchpoint的用法和實例,在此略過。
參考文獻:
《Art of Debugging》
《Linux® Debugging and Performance Tuning: Tips and Techniques》
Author:gnuhpc
WebSite:blog.csdn.net/gnuhpc