用GDB調試程序
GDB概述
————
GDB是GNU開源組織發布的一個強大的UNIX下的程序調試工具。或許,各位比較喜歡那種圖形界面方式的,像VC、BCB等IDE的調試,但如果你是在UNIX平臺下做軟件,你會發現GDB這個調試工具有比VC、BCB的圖形化調試器更強大的功能。所謂“寸有所長,尺有所短”就是這個道理。
一般來說,GDB主要幫忙你完成下面四個方面的功能:
?
?
?
?
從上面看來,GDB和一般的調試工具沒有什么兩樣,基本上也是完成這些功能,不過在細節上,你會發現GDB這個調試工具的強大,大家可能比較習慣了圖形化的調試工具,但有時候,命令行的調試工具卻有著圖形化工具所不能完成的功能。讓我們一一看來。
一個調試示例
——————
源程序:tst.c
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
編譯生成執行文件:(Linux下)
?
使用GDB調試:
hchen/test> gdbtst?
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 certainconditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.?
This GDB was configured as "i386-suse-linux"...
(gdb)l?
1?
2
3?
4?
5?
6?
7?
8?
9?
10?
(gdb)?
11?
12
13
14?
15?
16?
17?
18?
19?
20?
(gdb) break16?
Breakpoint 1 at 0x8048496: file tst.c, line 16.
(gdb) break func?
Breakpoint 2 at 0x8048456: file tst.c, line 5.
(gdb) info break?
NumType?
1?
2?
(gdb)r?
Starting program: /home/hchen/test/tst
Breakpoint 1, main () attst.c:17?
17?
(gdb)n?
18?
(gdb) n
20?
(gdb) n
18?
(gdb) n
20?
(gdb)c?
Continuing.
result[1-100] =5050?
Breakpoint 2, func (n=250) attst.c:5
5?
(gdb) n
6?
(gdb) pi?
$1 = 134513808
(gdb) n
8?
(gdb) n
6?
(gdb) p sum
$2 = 1
(gdb) n
8?
(gdb) p i
$3 = 2
(gdb) n
6?
(gdb) p sum
$4 = 3
(gdb)bt?
#0?
#1?
#2?
(gdb) finish?
Run till exit from #0?
0x080484e4 in main () at tst.c:24
24?
Value returned is $6 = 31375
(gdb)c?
Continuing.
result[1-250] =31375?
Program exited with code 027.<--------程序退出,調試結束。
(gdb)q?
hchen/test>
好了,有了以上的感性認識,還是讓我們來系統地認識一下gdb吧。
?
使用GDB
————
一般來說GDB主要調試的是C/C++的程序。要調試C/C++的程序,首先在編譯時,我們必須要把調試信息加到可執行文件中。使用編譯器(cc/gcc/g++)的-g 參數可以做到這一點。如:
?
?
如果沒有-g,你將看不見程序的函數名、變量名,所代替的全是運行時的內存地址。當你用-g把調試信息加入之后,并成功編譯目標代碼以后,讓我們來看看如何用gdb來調試他。
啟動GDB的方法有以下幾種:
?
?
?
?
?
?
?
GDB啟動時,可以加上一些GDB的啟動開關,詳細的開關可以用gdb-help查看。我在下面只例舉一些比較常用的參數:
?
?
?
?
?
?
?
?
?
?
?
GDB的命令概貌
———————
啟動gdb后,就你被帶入gdb的調試環境中,就可以使用gdb的命令開始調試程序了,gdb的命令可以使用help命令來查看,如下所示:
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
gdb的命令很多,gdb把之分成許多個種類。help命令只是例出gdb的命令種類,如果要看種類中的命令,可以使用help <class> 命令,如:helpbreakpoints,查看設置斷點的所有命令。也可以直接help<command>來查看命令的幫助。
gdb中,輸入命令時,可以不用打全命令,只用打命令的前幾個字符就可以了,當然,命令的前幾個字符應該要標志著一個唯一的命令,在Linux下,你可以敲擊兩次TAB鍵來補齊命令的全稱,如果有重復的,那么gdb會把其例出來。
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
要退出gdb時,只用發quit或命令簡稱q就行了。
?
GDB中運行UNIX的shell程序
————————————
在gdb環境中,你可以執行UNIX的shell的命令,使用gdb的shell命令來完成:
?
?
還有一個gdb命令是make:
?
?
?
在GDB中運行程序
————————
當以gdb<program>方式啟動gdb后,gdb會在PATH路徑和當前目錄中搜索<program>的源文件。如要確認gdb是否讀到源文件,可使用l或list命令,看看gdb是否能列出源代碼。
在gdb中,運行程序使用r或是run命令。程序的運行,你有可能需要設置下面四方面的事。
1、程序運行參數。
?
?
2、運行環境。
?
?
?
?
3、工作目錄。
?
?
4、程序的輸入輸出。
?
?
?
調試已運行的程序
————————
兩種方法:
1、在UNIX下用ps查看正在運行的程序的PID(進程ID),然后用gdb<program> PID格式掛接正在運行的程序。
2、先用gdb<program>關聯上源代碼,并進行gdb,在gdb中用attach命令來掛接進程的PID。并用detach來取消掛接的進程。
?
暫停 / 恢復程序運行
—————————
調試程序中,暫停程序運行是必須的,GDB可以方便地暫停程序的運行。你可以設置程序的在哪行停住,在什么條件下停住,在收到什么信號時停往等等。以便于你查看運行時的變量,以及運行時的流程。
當進程被gdb停住時,你可以使用info program來查看程序的是否在運行,進程號,被暫停的原因。
在gdb中,我們可以有以下幾種暫停方式:斷點(BreakPoint)、觀察點(WatchPoint)、捕捉點(CatchPoint)、信號(Signals)、線程停止(ThreadStops)。如果要恢復程序運行,可以使用c或是continue命令。
一、設置斷點(BreakPoint)
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
二、設置觀察點(WatchPoint)
?
?
?
?
?
?
?
?
?
?
?
?
?
?
三、設置捕捉點(CatchPoint)
?
?
?
?
?
?
?
?
?
?
?
?
?
使用gdb調試
我們將會使用GNU調試器,gdb,來調試這個程序。這是一個可以免費得到并且可以用于多個Unix平臺的功能強大的調試器。他也是Linux系統上的默認調試器。gdb已經被移植到許多其他平臺上,并且可以用于調試嵌入式實時系統。
啟動gdb
讓我們重新編譯我們的程序用于調試并且啟動gdb。
$ cc -g -o debug3 debug3.c
$ gdb debug3
GNU gdb 5.2.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 certainconditions.
Type “show copying” to see the conditions.
There is absolutely no warranty for GDB. Type “show warranty” fordetails.
This GDB was configured as “i586-suse-linux”...
(gdb)
gdb具有豐富的在線幫助,以及可以使用info程序進行查看或是在Emacs中進行查看的完整手冊。
(gdb) help
List of classes of commands:
aliases — Aliases of other commands
breakpoints — Making program stop at certain points
data — Examining data
files — Specifying and examining files
internals — Maintenance commands
obscure — Obscure features
running — Running the program
stack — Examining the stack
status — Status inquiries
support — Support facilities
tracepoints — Tracing of program execution without stopping theprogram
user-defined — User-defined commands
Type “help” followed by a class name for a list of commands in thatclass.
Type “help” followed by command name for full documentation.
Command name abbreviations are allowed if unambiguous.
(gdb)
gdb本身是一個基于文本的程序,但是他確實了一些有助于重復任務的簡化操作。許多版本具有一個命令行編輯歷史,從而我們可以在命令歷史中進行滾動并且再次執行相同的命令。所有的版本都支持一個"空白命令",敲擊Enter會再次執行上一條命令。當我們使用step或是next命令在一個程序中分步執行特殊有用。
運行一個程序
我們可以使用run命令執行這個程序。我們為run命令所指定的所有命令都作為參數傳遞給程序。在這個例子中,我們并不需要任何參數。
我們在這里假設我們的系統與作者的類似,也產生了內存錯誤的錯誤信息。如果不是,請繼續閱讀。我們就會發現當我們自己的程序生成一個內存錯誤時應怎么辦。如果我們并不沒有得到內存錯誤信息,但是我們在閱讀本書時希望運行這個例子,我們可以拾起第一個內存訪問問題已經被修復的debug4.c。
(gdb) run
Starting program: /home/neil/BLP3/chapter10/debug3
Program received signal SIGSEGV, Segmentation fault.
0x080483c0 in sort (a=0x8049580, n=5) at debug3.c:23
23?
(gdb)
如前面一樣,我們程序并沒有正確運行。當程序失敗時,gdb會向我們顯示原因以及位置。現在我們可以檢測問題背后的原因。
依據于我們的內核,C庫,以及編譯器選項,我們所看到的程序錯誤也許有所不同,例如,也許當數組元素交換時是在25行,而不是數組元素比較時的23行。如果是這種情況,我們也許會看到如下的輸出:
Program received signal SIGSEGV, Segmentation fault.
0x8000613 in sort (a=0x8001764, n=5) at debug3.c:25
25?
我們仍然可以遵循如下的gdb例子會話。
棧追蹤
程序已經在源文件debug3.c的第23行處的sort函數停止。如果我們并沒有使用額外的調試信息來編譯這個程序,我們就不能看到程序在哪里失敗,也不能使用變量名來檢測數據。
我們可以通過使用backstrace命令來查看我們是如何到達這個位置的。
(gdb) backtrace
#0 0x080483c0 in sort (a=0x8049580, n=5) at debug3.c:23
#1 0x0804849b in main () at debug3.c:37
#2 0x400414f2 in __libc_start_main () from /lib/libc.so.6
(gdb)
這個是一個非常簡單的程序,而且追蹤信息很短小,因為我們并沒有在其他的函數內部來調用許多函數。我們可以看到sort是由同一個文件debug3.c中37行處的main來調用的。通常,問題會更為復雜,而我們可以使用backtrace來發現我們到達錯誤位置的路徑。
backtrace命令可以簡寫為bt,而且為了與其他調試器兼容,where命令也具有相同的功能。
檢測變量
當程序停止時由gdb所輸出的信息以及在棧追蹤中的信息向我們顯示了函數能數的值。
sort函數是使用一個參數a來調用的,而其值為0x8049580。這是數組的地址。依據于所使用的編譯器以及操作系統,這個值在不同的操作系統也會不同。
所影響的行號23,是一個數組元素與另一個數組元素進行比較的地方。
if(a[j].key > a[j+1].key) {
我們可以使用調試器來檢測函數參數,局部變量以及全局數據的內容。print命令可以向我們顯示變量以及其他表達式的內容。
(gdb) print j
$1 = 4
在這里我們可以看到局部變量j的值為4。類似這樣由gdb命令所報告的所有值都會保存在偽變量中以備將來使用。在這里變量$1賦值為4以防止我們在以后使用。以后的命令將他們的結果存儲為$2,$3,依次類推。
j的值為4的事實意味著程序試著執行語句
if(a[4].key > a[4+1].key)
我們傳遞給sort的數組,array,只有5個元素,由0到4進行索引。所以這條語句讀取并不存在的array[5]。循環變量j已經讀取一個錯誤的值。
如果我們嘗試這個例子,而我們程序在25行發生錯誤,我們系統只有在交互元素時才會檢測到一個超過數組邊界的讀取,執行
a[j] = a[j+1];
此時將j設置為4,結果為
a[4] = a[4+1];
我們可以使用print通過表達式來查看所傳遞的數組元素。使用gdb,我們幾乎可以使用任何合法的C表達式來輸出變量,數組元素,以及指針的值。
(gdb) print a[3]
$2 = {data = “alex”, ‘\000’ <repeats 4091times>, key = 1}
(gdb)
gdb將命令的結果保存在一個偽變量中,$<number>。上一個結果總是為$,而之前的一個為$$。這可以使得在一個結果可以用在另一個命令中。例如,
(gdb) print j
$3 = 4
(gdb) print a[$-1].key
$4 = 1
列出程序
我們可以使用list命令在gdb內查看程序源代碼。這會打印出當前位置周圍的部分代碼。持續的使用list會輸出更多的代碼。我們也可以為list指定一個行號或是函數名作為一個參數,而gdb就會顯示那個位置的代碼。
(gdb) list
18?
19?
20?
21?
22?
23?
24?
25?
26?
27?
(gdb)
我們可以看到在22行循環設置為當變量j小于n時才會執行。在這個例子中,n為5,所以j的最終值為4,總是小1。4會使得a[4]與a[5]進行比較并且有可能進行交換。這個問題的解決方法就是修正循環的結束條件為j< n-1。
讓我們做出修改,將這個新程序稱之為debug4.c,重新編譯,并再次運行。
for(j = 0; j < n-1; j++) {
$ cc -g -o debug4 debug4.c
$ ./debug4
array[0] = {john, 2}
array[1] = {alex, 1}
array[2] = {bill, 3}
array[3] = {neil, 4}
array[4] = {rick, 5}
程序仍不能正常工作,因為他輸出了一個不正確的排序列表。下面我們使用gdb在程序運行時分步執行。
設置斷點
查找出程序在哪里失敗,我們需要能夠查看程序運行他都做了什么。我們可以通過設置斷點在任何位置停止程序。這會使得程序停止并將控制權返回調試器。我們將能夠監視變量并且允許程序繼續執行。
在sort函數中有兩個循環。外層循環,使用循環變時i,對于數組中的每一個元素運行一次。內層循環將其與列表中的下一個元素進行交換。這具有將最小的元素交換到最上面的效果。在外層循環的每一次執行之后,最大的元素應位置底部。我們可通過在外層循環停止程序進行驗證并且檢測數組狀態。
有許多命令可以用于設置斷點。通過gdb的help breakpoint命令可以列表這些命令:
(gdb) help breakpoint
Making program stop at certain points.
List of commands:
awatch — Set a watchpoint for an expression
break — Set breakpoint at specified line or function
catch — Set catchpoints to catch events
clear — Clear breakpoint at specified line or function
commands — Set commands to be executed when a breakpoint ishit
condition — Specify breakpoint number N to break only if COND istrue
delete — Delete some breakpoints or auto-display expressions
disable — Disable some breakpoints
enable — Enable some breakpoints
hbreak — Set a hardware assisted breakpoint
ignore — Set ignore-count of breakpoint number N to COUNT
rbreak — Set a breakpoint for all functions matching REGEXP
rwatch — Set a read watchpoint for an expression
tbreak — Set a temporary breakpoint
tcatch — Set temporary catchpoints to catch events
thbreak — Set a temporary hardware assisted breakpoint
watch — Set a watchpoint for an expression
Type “help” followed by command name for full documentation.
Command name abbreviations are allowed if unambiguous.
讓我們在20行設置一個斷點并且運行這個程序:
$ gdb debug4
(gdb) break 20
Breakpoint 1 at 0x804835d: file debug4.c, line 20.
(gdb) run
Starting program: /home/neil/BLP3/chapter10/debug4
Breakpoint 1, sort (a=0x8049580, n=5) at debug4.c:20
20?
我們可以輸出數組值并且使用cont可以使得程序繼續執行。這個會使得程序繼續運行直到遇到下一個斷點,在這個例子中,直到他再次執行到20行。在任何時候我們都可以有多個活動斷點。
(gdb) print array[0]
$1 = {data = “bill”, ‘\000’ <repeats 4091times>, key = 3}
要輸出多個連續的項目,我們可以使用@<number>結構使得gdb輸出多個數組元素。要輸出array的所有五個元素,我們可以使用
(gdb) print array[0]@5
$2 = {{data = “bill”, ‘\000’ <repeats 4091times>, key = 3}, {
?
?
?
?
注意,輸出已經進行簡單的處理從而使其更易于閱讀。因為這是第一次循環,數組并沒有發生變量。當我們允許程序繼續執行,我們可以看到當處理執行時array的成功修改:
(gdb) cont
Continuing.
Breakpoint 1, sort (a=0x8049580, n=4) at debug4.c:20
20?
(gdb) print array[0]@5
$3 = {{data = “bill”, ‘\000’ <repeats 4091times>, key = 3}, {
?
?
?
?
(gdb)
我們可以使用display命令來設置gdb當程序在斷點處停止時自動顯示數組:
(gdb) display array[0]@5
1: array[0] @ 5 = {{data = “bill”, ‘\000’ <repeats4091 times>, key = 3}, {
?
?
?
?
而且我們可以修改斷點,從而他只是簡單的顯示我們所請求的數據并且繼續執行,而不是停止程序。在這樣做,我們可以使用commands命令。這會允許我們指定當遇到一個斷點時執行哪些調試器命令。因為我們已經指定了一個display命令,我們只需要設置斷點命令繼續執行。
(gdb) commands
Type commands for when breakpoint 1 is hit, one per line.
End with a line saying just “end”.
> cont
> end
現在我們允許程序繼續,他會運行完成,在每次運行到外層循環時輸出數組的值。
(gdb) cont
Continuing.
Breakpoint 1, sort (a=0x8049684, n=3) at debug4.c:20
20?
1: array[0] @ 5 = {{data = “john”, ‘\000’ <repeats4091 times>, key = 2}, {
?
?
?
?
Breakpoint 1, sort (a=0x8049684, n=2) at debug4.c:20
20?
1: array[0] @ 5 = {{data = “john”, ‘\000’ <repeats4091 times>, key = 2}, {
?
?
?
?
array[0] = {john, 2}
array[1] = {alex, 1}
array[2] = {bill, 3}
array[3] = {neil, 4}
array[4] = {rick, 5}
Program exited with code 044.
(gdb)
gdb報告程序并沒有以通常的退出代碼退出。這是因為程序本身并沒有調用exit也沒有由main返回一個值。在這種情況下,這個退出代碼是無意義的,而一個有意義的退出代碼應由調用exit來提供。
這個程序看起來似乎外層循環次數并不是我們所期望的。我們可以看到循環結束條件所使用的參數值n在每個斷點處減小。這意味著循環并沒有執行足夠的次數。問題就在于30行處n的減小。
n--;
這是一個利用在每一次外層循環結束時array的最大元素都會位于底部的事實來優化程序的嘗試,所以就會有更少的排序。但是,正如我們所看到的,這是與外層循環的接口,并且造成了問題。最簡單的修正方法就是刪除引起問題的行。讓我們通過使用調試器來應用補丁測試這個修正是否有效。
使用調試進行補丁
我們已經看到了我們可以使用調試器來設置斷點與檢測變量的值。通過使用帶動作的斷點,我們可以在修改源代碼與重新編譯之前試驗一個修正,稱之為補丁。在這個例子中,我們需要在30行設置斷點,并且增加變量n。然后,當30行執行,這個值將不會發生變化。
讓我們從頭啟動程序。首先,我們必須刪除我們的斷點與顯示。我們可以使用info命令來查看我們設置了哪些斷點與顯示:
(gdb) info display
Auto-display expressions now in effect:
Num Enb Expression
1:?
(gdb) info break
NumType?
1?
?
?
我們可以禁止這些或是完全刪除他們。如果我們禁止他們,那么我們可以在以后需要他們時重新允許這些設置:
(gdb) disable break 1
(gdb) disable display 1
(gdb) break 30
Breakpoint 2 at 0x8048462: file debug4.c, line 30.
(gdb) commands 2
Type commands for when breakpoint 2 is hit, one per line.
End with a line saying just “end”.
>set variable n = n+1
>cont
>end
(gdb) run
Starting program: /home/neil/BLP3/chapter10/debug4
Breakpoint 2, sort (a=0x8049580, n=5) at debug4.c:30
30?
Breakpoint 2, sort (a=0x8049580, n=5) at debug4.c:30
30?
Breakpoint 2, sort (a=0x8049580, n=5) at debug4.c:30
30?
Breakpoint 2, sort (a=0x8049580, n=5) at debug4.c:30
30?
Breakpoint 2, sort (a=0x8049580, n=5) at debug4.c:30
30?
array[0] = {alex, 1}
array[1] = {john, 2}
array[2] = {bill, 3}
array[3] = {neil, 4}
array[4] = {rick, 5}
Program exited with code 044.
(gdb)
這個程序運行結束并且會輸出正確的結果。現在我們可以進行修正并且繼續使用更多的數據進行測試。
了解更多有關gdb的內容
GNU調試器是一個強大的工具,可以提供大量的有關運行程序內部狀態的信 息。在支持一個名叫硬件斷點(hardwarebreakpoint)的實用程序的系統上,我們可以使用gdb來實時的查看變量的改變。硬件斷點是某些CPU的一個特性;如果出現特定的條件,通常是在指定區域的內存訪問,這些處理器能夠自動停止。相對應的,gdb可以使用watch表達式。這就意味著,出于性能考慮,當一個表達式具有一個特定的值時,gdb可以停止這個程序,而不論計算發生在程序中的哪個位置。
斷點可以使用計數以及條件進行設置,從而他們只在一定的次數之后或是當滿足一個條件時才會被引發。
gdb也能夠將其本身附在已經運行的程序中。這對于我們調試客戶端/服務器系統時是非常有用的,因為我們可以調試一個正在運行的行為不當的服務器進程,而不需要停止與重啟服務器。例如,我們可以使用gcc -O-g選項來編譯我們的程序,從而得到優化與調試的好處。不足之處就是優化也許會重新組織代碼,所以當我們分步執行時,我們也許會發出我們自身的跳轉來達到與原始源代碼相同的效果。
我們也可以使用gdb來調試已經崩潰的程序。Linux與Unix經常會在一個程序失敗時在一個名為core的文件中生成一個核心轉儲信息。這是一個程序內存的圖象并且會包含失敗時全局變量的值。我們可以使用gdb來查看當程序崩潰時程序運行到哪里。查看gdb手冊頁我們可以得到更為詳細的信息。
gdb以GPL許可證發布并且絕大多數的Unix系統都會支持他。我們強烈建議了解gdb。
更多的調試工具
除了強大的調試工具,例如gdb,Linux系統通常還會提供一些其他們的我們可以用于診治調試進程的工具。其中的一些會提供關于一個程序的靜態信息;其他的會提供動態分析。
靜態分析只由程序源代碼提供信息。例如ctags,cxref,與cflow這樣的程序與源代碼一同工作,并且會提供有關函數調用與位置的有用信息。
動態會析會提供有關一個程序在執行過程中如何動作的信息。例如prof與gprof這樣的程序會提供有關執行了哪個函數并且執行多長時間的信息。
下面我們來看一下其中的一些工具及其輸出。并不是所有的這些工具都可以在所有的系統上得到,盡管其中的一些都有自由版本。
Lint:由我們的程序中移除Fluff
原始的Unix系統提供了一個名為lint的實用程序。他實質是一個C編譯的前端,帶有一個測試設計來適應一些常識并且生成警告。他會檢測變量在設計之前在哪里使用以及哪里函數參數沒有被使用,以及其他的一些情況。
更為現代的C編譯器可以編譯性能為代價提供類似的警告。lint本身已經被C標準所超越。因為這個工具是基于早期的C編譯器,他并不能處理所有的ANSI語法。有一些商業版本的lint可以用于Unix,而且在網絡上至少有一個名為splint可以用于Linux。這就是過去所知的LClint,他是MIT一個工程的一部分,來生成用于通常規范的工具。一個類似于lint的工具splint可以提供查看注釋的有用代碼。splint可以在htt://www.splin.org處得到。
下面是一個編輯過的splint例子輸出,這是運行我們在前面調試的例子程序的早期版本中所產生的輸出:
neil@beast:~/BLP3/chapter10> splint -strictdebug0.c
Splint 3.0.1.6 --- 27 Mar 2002
debug0.c:14:22: Old style function declaration
?
?
debug0.c: (in function sort)
debug0.c:20:31: Variable s used before definition
?
?
debug0.c:20:23: Left operand of & is not unsignedvalue (boolean):
?
?
?
?
debug0.c:20:23: Test expression for for not boolean, type unsignedint:
?
?
?
debug0.c:20:23: Operands of & are non-integer(boolean) (in post loop test):
?
?
?
debug0.c:32:14: Path with no return in function declared to returnint
?
?
?
debug0.c:34:13: Function main declared without parameter list
?
?
debug0.c: (in function main)
debug0.c:36:17: Return value (type int) ignored: sort(array,5)
?
?
debug0.c:37:14: Path with no return in function declared to returnint
debug0.c:14:13: Function exported but not used outside debug0:sort
?
Finished checking --- 22 code warnings
$
這個程序報告舊風格的函數定義以及函數返回類型與他們實際返回類型之間的不一致。這些并不會影響程序的操作,但是應該注意。
他還在下面的代碼片段中檢測到兩個實在的bug:
int s;
for(; i < n & s != 0; i++) {
?
splint已經確定在20行使用了變量s,但是并沒有進行初始化,而且操作符&已經被更為通常的&&所替代。在這個例子中,操作符優先級修改了測試的意義并且是程序的一個問題。
所有這些錯誤都在調試開始之前在代碼查看中被修正。盡管這個例子有一個故意演示的目的,但是這些錯誤真實世界的程序中經常會出現的。
函數調用工具
三個實用程序-ctags,cxref與cflow-形成了X/Open規范的部分,所以必須在具有軟件開發功能的Unix分枝系統上提供。
ctags
ctags程序創建函數索引。對于每一個函數,我們都會得到一個他在何處使用的列表,與書的索引類似。
ctags [-a] [-f filename] sourcefile sourcefile ...
ctags -x sourcefile sourcefile ...
默認情況下,ctags在當前目錄下創建一個名為tags的目錄,其中包括在輸入源文件碼中所聲明的每一個函數,如下面的格式
announce app_ui.c /^static void announce(void) /
文件中的每一行由一個函數名,其聲明所在的文件,以及一個可以用在文件中查找到函數定義所用的正則表達式所組成。一些編輯器,例如Emacs可以使用這種類型的文件在源碼中遍歷。
相對應的,通過使用ctags的-x選項,我們可以在標準輸出上產生類似格式的輸出:
find_cat 403 app_ui.c static cdc_entry find_cat(
我們可以通過使用-ffilename選項將輸出重定向到另一個不同的文件中,或是通過指定-a選項將其添加到一個已經存在的文件中。
cxref
cxref程序分析C源代碼并且生成一個交叉引用。他顯示了每一個符號在程序中何處被提到。他使用標記星號的每一個符號定義位置生成一個排序列表,如下所示:
SYMBOL?
?
?
?
?
?
?
?
?
?
calldata?
?
?
?
在作者的機子上,前面的輸入在程序的源碼目錄中使用下面的命令來生成的:
$ cxref *.c *.h
但是實際的語法因為版本的不同而不同。查看我們系統的文檔或是man手冊可以得到更多的信息。
cflow
cflow程序會輸出一個函數調用樹,這是一個顯示函數調用關系的圖表。這對于查看程序結構來了解他是如何操作的以及了解對于一個函數有哪些影響是十分有用的。一些版本的cflow可以同時作用于目標文件與源代碼。查看手冊頁我們可以了解更為詳細的操作。
下面是由一個cflow版本(cflow-2.0)所獲得的例子輸出,這個版本的cflow版本是由MartyLeisner維護的,并且可以網上得到。
1?
2?
3?
4?
5?
6?
7?
8?
9?
10?
11?
從這個輸出中我們可以看到main函數調用show_all_lists,而show_all_lists調用display_list,display_list本身調用printf。
這個版本cflow的一個選項就是-i,這會生成一個反轉的流程圖。對于每一個函數,cflow列出調用他的其他函數。這聽起來有些復雜,但是實際上并不是這樣。下面是一個例子。
19 display_list {prcc.c 1056}
20?
21 exit {}
22?
23?
24?
...
74?
75?
76?
77?
78?
...
99?
100?
例如,這告訴我們調用exit的函數有main,show_all_lists與usage。
使用prof/gprof執行性能測試
當我們試著追蹤一個程序的性能問題時一個十分有用的技術就是執行性能測試(executionprofiling)。通常被特殊的編譯器選項以及輔助程序所支持,一個程序的性能顯示他在哪里花費時間。
prof程序(以及其GNU版本gprof)會由性能測試程序運行時所生成的執行追蹤文件中輸出報告。一個可執行的性能測試是由指定-p選項(對prof)或是-pg選項(對gprof)所生成的:
$ cc -pg -o program program.c
這個程序是使用一個特殊版本的C庫進行鏈接的并且被修改來包含監視代碼。對于不同的系統結果也許不同,但是通常是由安排頻繁被中斷的程序以及記錄執行位置來做到的。監視數據被寫入當前目錄中的一個文件,mon.out(對于gprof為gmon.out)。
$ ./program
$ ls -ls
?
prof/gprof程序讀取這些監視數據并且生成一個報告。查看其手冊頁可以詳細了解其程序選項。下面以gprof輸出作為一個例子:
cumulative?
?
?
?
?
?
?
?
?
?
?
?
?
斷言
在程序的開發過程中,通常使用條件編譯的方法引入調試代碼,例如printf,但是在一個發布的系統中保留這些信息是不實際的。然而,經常的情況是問題出現與不正確的假設相關的程序操作過程中,而不是代碼錯誤。這些都是"不會發生"的事件。例如,一個函數也許是在認為其輸入參數總是在一定范圍下而編寫的。如果給他傳遞一些不正確的數據,也許整個系統就會崩潰。
對于這些情況,系統的內部邏輯在哪里需要驗證,X/Open提供了assert宏,可以用來測試一個假設是否正確,如果不正確則會停止程序。
#include <assert.h>
void assert(int expression)
assert宏會計算表達式的值,如果不為零,則會向標準錯誤上輸出一些診斷信息,并且調用abort來結束程序。
頭文件assert.h依據NDEBUG的定義來定義宏。如果頭文件被處理時定義了NDEBUG,assert實質上被定義為空。這就意味著我們可以通過使用-DNDEBUG在編譯時關閉斷言或是在包含assert.h文件之前包含下面這行:
#define NDEBUG
這種方法的使用是assert的一個問題。如果我們在測試中使用assert,但是卻對生產代碼而關閉,比起我們測試時的代碼,我們的生產代碼就不會太安全。在生產代碼中保留斷言開啟狀態并不是通常的選擇,我們希望我們的代碼向用戶顯示一條不友好的錯誤assertfailed與一個停止的程序嗎?我們也許會認為最好是編寫我們自己的檢測斷言的錯誤追蹤例程,而不必在我們的生產代碼中完全禁止。
我們同時要小心在assert斷言沒有臨界效果。例如,如果我們在一個臨界效果中調用一個函數,如果移除了斷言,在生產代碼中就不會出現這個效果。
試驗--assert
下面的程序assert.c定義了一個必須傳遞正值參數的函數。通過使用一個斷言可以避免不正常參數的可能。
在包含assert.h頭文件和檢測參數是否為正的平方根函數之后,我們可以編寫如下的函數:
#include <stdio.h>
#include <math.h>
#include <assert.h>
double my_sqrt(double x)
{
?
?
}
int main()
{
?
?
?
}
當我們運行這個程序時,我們就會看到當我們傳遞一個非法值時就會違背這個斷言。事實上的斷言失敗的消息格式會因系統的不同而不同。
$ cc -o assert assert.c -lm
$ ./assert
sqrt +2 = 1.41421
assert: assert.c:7: my_sqrt: Assertion `x >= 0.0’failed.
Aborted
$
工作原理
當我們試著使用一個負數來調用函數my_sqrt時,斷言就會失敗。assert宏會提供違背斷言的文件和行號,以及失敗的條件。程序以一個退出陷井結束。這是assert調用abort的結果。
如果我們使用-DNDEBUG選項來編譯這個程序,斷言就會被編譯在外,而當我們由my_sqrt中調用sqrt函數時我們就會得到一個算術錯誤。
$ cc -o assert -DNDEBUG assert.c -lm
$ ./assert
sqrt +2 = 1.41421
Floating point exception
$
一些最近的算術庫版本會返回一個NaN(Not a Number)值來表示一個不可用的結果。
sqrt –2 = nan
內存調試
富含bug而且難于跟蹤調試的一個區域就是動態內存分配。如果我們編譯一個使用malloc與free來分配內存的程序,很重要的一點就是我們要跟蹤我們所分配的內存塊,并且保證不要使用已經釋放的內存塊。
通常,內存是由malloc分配并且賦給一個指針變量的。如果指針變量被修改了,而又沒有其他的指針來指向這個內存塊,他就會變為不可訪問的內存塊。這就是一個內存泄露,而且會使得我們程序尺寸變大。如果我們泄露了大量的內存,那么我們的系統就會變慢并且會最終用盡內存。
如果我們在超出一個分配的內存塊的結束部分(或是在一個內存塊的開始部分)寫入數據,我們很有可能會破壞malloc庫來跟蹤分配所用的數據結構。在這種情況下,在將來的某個時刻,調用malloc,或者甚至是free,就會引起段錯誤,而我們的程序就會崩潰。跟蹤錯誤發生的精確點是非常困難的,因為很可能他在引起崩潰的事件發生以前很一段時間就已經發生了。
不必奇怪的是,有一些工具,商業或是自由的,可以有助于處理這兩種問題類型。例如,有許多不同的malloc與free版本,其中的一些包含額外的代碼在分配與回收上進行檢測嘗試檢測一個內存塊被釋放兩次或是其他一些濫用類型的情況。
ElectricFence
ElectricFence 庫是由BrucePerens開發的,并且在一些Linux發行版本中作為一個可選的組件來提供,例如RedHat,而且已經可以在網絡上獲得。他嘗試使用Linux的虛擬內存工具來保護malloc與free所使用的內存,從而在內存被破壞時終止程序。
試驗--ElectricFence
下面的程序,efence.c,使用malloc分配一個內存塊,然后在超出塊結束處寫入數據。讓我們看一下會發生什么情況。
#include <stdio.h>
#include <stdlib.h>
int main()
{
?
?
?
?
?
}
當我們編譯運行這個程序時,我們并不會看到進一步的行為。然而,似乎malloc所分配的內存區域有一些問題,而我們實際上已經遇到了麻煩。
$ cc -o efence efence.c
$ ./efence
$
然而,如果我們使用ElectricFence庫,libefence.a來鏈接這個程序,我們就會得到一個即時的響應。
$ cc -o efence efence.c -lefence
$ ./efence
?
Segmentation fault
$
在調試器下運行可以定位這個問題:
$ cc -g -o efence efence.c -lefence
$ gdb efence
?
Starting program: /home/neil/BLP3/chapter10/efence
[New Thread 1024 (LWP 1869)]
?
Program received signal SIGSEGV, Segmentation fault.
[Switching to Thread 1024 (LWP 1869)]
0x080484ad in main () at efence.c:10
10?
(gdb)
工作原理
Electric替換malloc并且將函數與計算機處理器的虛擬內存特性相關聯來阻止非法的內存訪問。當這樣的訪問發生時,就會拋出一個段錯誤信息從而可以終止程序。
valgrind
valgrind是一個可以檢測我們已經討論過的許多問題的工具。事實上,他可以檢測數據訪問錯誤與內存泄露。也許他并沒有被包含在我們的Linux發行版本中,但是我們可以在http://developer.kde.org/~sewardj處得到。
程序并不需要使用valgrind重新編譯,而我們甚至可以調用一個正在運行的程序的內存訪問。他很值得一看,他已經用在主要的開發上,包含KDE版本3。
試驗--valgrind
下面的程序,checker.c,分配一些內存,讀取超過那塊內存限制的位置,在其結束處之外寫入數據,然后使其不能訪問。
#include <stdio.h>
#include <stdlib.h>
int main()
{
?
?
?
?
?
?
?
?
?
}
要使用valgrind,我們只需要簡單的運行valgrind命令,傳遞我們希望檢測的選項,其后是使用其參數運行的程序。
當我們使用valgrind來運行我們的程序時,我們可以看到診斷出許多問題:
$ valgrind --leak-check=yes -v ./checker
==3436== valgrind-1.0.4, a memory error detector for x86GNU/Linux.
==3436== Copyright (C) 2000-2002, and GNU GPL’d, by JulianSeward.
==3436== Estimated CPU clock rate is 452 MHz
==3436== For more details, rerun with: -v
==3436==
==3436== Invalid read of size 1
==3436==?
==3436==?
==3436==?
==3436==?
==3436==?
==3436==?
==3436==?
==3436==?
==3436==
==3436== Invalid write of size 1
==3436==?
==3436==?
==3436==?
==3436==?
==3436==?
==3436==?
==3436==?
==3436==?
==3436==
==3436== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0from 0)
==3436== malloc/free: in use at exit: 1024 bytes in 1 blocks.
==3436== malloc/free: 1 allocs, 0 frees, 1024 bytesallocated.
==3436== For counts of detected errors, rerun with: -v
==3436== searching for pointers to 1 not-freed blocks.
==3436== checked 3468724 bytes.
==3436==
==3436== definitely lost: 1024 bytes in 1 blocks.
==3436== possibly lost:?
==3436== still reachable: 0 bytes in 0 blocks.
==3436==
==3436== 1024 bytes in 1 blocks are definitely lost in loss record1 of 1
==3436==?
==3436==?
==3436==?
==3436==?
==3436==
==3436== LEAK SUMMARY:
==3436==?
==3436==?
==3436==?
==3436== Reachable blocks (those to which a pointer was found) arenot shown.
==3436== To see them, rerun with: --show-reachable=yes
==3436== $
這里我們可以看到錯誤的讀取與寫入已經被捕獲,而所關注的內存塊與他們被分配的位置相關聯。我們可以使用調試器在出錯點斷開程序。
valgrind有許多選項,包含特定的錯誤類型表達式與內存泄露檢測。要檢測我們的例子泄露,我們必須使用一個傳遞給valgrind的選項。當程序結束時要檢測內存泄露,我們需要指定 --leak-check=yes。我們可以使用valgrind --help得到一個選項列表。
工作原理
我們的程序在valgrind的控制下執行,這會檢測我們程序所執行的各種動作,并且執行許多檢測,包括內存訪問。如果程序訪問一個已分配的內存塊并且訪問是非法的,valgrind就會輸出一條信息。在程序結束時,一個垃圾收集例程就會運行來檢測是否在存在分配的內存塊沒有被釋放。這些孤兒內存也會被報告。
小結
在這一章,我們了解了一些調試工具與技術。Linux提供了一些強大的工具可以用于由程序中移除缺陷。我們使用gdb來消除程序中的bug,并且了解了如cflow與splint這樣的數據分析工具。最后我們了解了當我們使用動態分配內存時會出現的問題,以及一些用于類似問題診斷的工具,例如ElectricFence與valgrind。
查看運行時數據
———————
?
?
?
?
?
?
?
?
一、表達式
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
二、程序變量
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
三、數組
?
?
?
?
?
?
?
?
?
?
?
?
四、輸出格式
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
五、查看內存
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
六、自動顯示
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
七、設置顯示選項
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
八、歷史記錄
?
?
?
九、GDB環境變量
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
十、查看寄存器
?
?
?
?
?
?
?
?
?
?
?
改變程序的執行
———————
?
?
?
一、修改變量值
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
二、跳轉執行
?
?
?
?
?
?
?
?
?
?
?
?
?
三、產生信號量
?
?
?
?
?
?
四、強制函數返回
?
?
?
?
?
?
?
五、強制調用函數
?
?
?
?
?
在不同語言中使用GDB
——————————
GDB支持下列語言:C, C++, Fortran, PASCAL,Java, Chill, assembly, 和Modula-2。一般說來,GDB會根據你所調試的程序來確定當然的調試語言,比如:發現文件名后綴為“.c”的,GDB會認為是C程序。文件名后綴為“.C, .cc, .cp, .cpp, .cxx, .c++”的,GDB會認為是C++程序。而后綴是“.f,.F”的,GDB會認為是Fortran程序,還有,后綴為如果是“.s, .S”的會認為是匯編語言。
也就是說,GDB會根據你所調試的程序的語言,來設置自己的語言環境,并讓GDB的命令跟著語言環境的改變而改變。比如一些GDB命令需要用到表達式或變量時,這些表達式或變量的語法,完全是根據當前的語言環境而改變的。例如C/C++中對指針的語法是*p,而在Modula-2中則是p^。并且,如果你當前的程序是由幾種不同語言一同編譯成的,那到在調試過程中,GDB也能根據不同的語言自動地切換語言環境。這種跟著語言環境而改變的功能,真是體貼開發人員的一種設計。
下面是幾個相關于GDB語言環境的命令:
?
?
?
?
?
?
?
?
?
如果GDB沒有檢測出當前的程序語言,那么你也可以手動設置當前的程序語言。使用setlanguage命令即可做到。
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?