看下面的程序:
例 10.1. 函數調試實例
#include <stdio.h> int add_range(int low, int high) { int i, sum; for (i = low; i <= high; i++) sum = sum + i; return sum; } int main(void) { int result[100]; result[0] = add_range(1, 10); result[1] = add_range(1, 100); printf("result[0]=%d\nresult[1]=%d\n", result[0], result[1]); return 0; }
add_range
函數從low
加到high
,在main
函數中首先從1加到10,把結果保存下來,然後從1加到100,再把結果保存下來,最後打印的兩個結果是:
result[0]=55 result[1]=5105
第一個結果正確[20],第二個結果顯然不正確,在小學我們就聽說過高斯小時候的故事,從1加到100應該是5050。一段代碼,第一次運行結果是對的,第二次運行卻不對,這是很常見的一類錯誤現象,這種情況不應該懷疑代碼而應該懷疑數據,因為第一次和第二次運行的都是同一段代碼,如果代碼是錯的,那為什麼第一次的結果能對呢?然而第一次和第二次運行時相關的數據卻有可能不同,錯誤的數據會導致錯誤的結果。在動手調試之前,讀者先試試只看代碼能不能看出錯誤原因,只要前面幾章學得紮實就應該能看出來。
在編譯時要加上-g
選項,生成的執行檔才能用gdb
進行源碼級調試:
$ gcc -g main.c -o main $ gdb main GNU gdb 6.8-debian Copyright (C) 2008 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "i486-linux-gnu"... (gdb)
-g
選項的作用是在執行檔中加入原始碼的信息,比如執行檔中第幾條機器指令對應原始碼的第幾行,但並不是把整個源檔案嵌入到執行檔中,所以在調試時必須保證gdb
能找到源檔案。gdb
提供一個類似Shell的命令行環境,上面的(gdb)
就是提示符,在這個提示符下輸入help
可以查看命令的類別:
(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 the program user-defined -- User-defined commands Type "help" followed by a class name for a list of commands in that class. Type "help all" for the list of all commands. Type "help" followed by command name for full documentation. Type "apropos word" to search for commands related to "word". Command name abbreviations are allowed if unambiguous.
也可以進一步查看某一類別中有哪些命令,例如查看files
類別下有哪些命令可用:
(gdb) help files Specifying and examining files. List of commands: add-shared-symbol-files -- Load the symbols from shared objects in the dynamic linker's link map add-symbol-file -- Load symbols from FILE add-symbol-file-from-memory -- Load the symbols out of memory from a dynamically loaded object file cd -- Set working directory to DIR for debugger and program being debugged core-file -- Use FILE as core dump for examining memory and registers directory -- Add directory DIR to beginning of search path for source files edit -- Edit specified file or function exec-file -- Use FILE as program for getting contents of pure memory file -- Use FILE as program to be debugged forward-search -- Search for regular expression (see regex(3)) from last line listed generate-core-file -- Save a core file with the current state of the debugged process list -- List specified function or line ...
現在試試用list
命令從第一行開始列出原始碼:
(gdb) list 1 1 #include <stdio.h> 2 3 int add_range(int low, int high) 4 { 5 int i, sum; 6 for (i = low; i <= high; i++) 7 sum = sum + i; 8 return sum; 9 } 10
一次只列10行,如果要從第11行開始繼續列原始碼可以輸入
(gdb) list
也可以什麼都不輸直接敲回車,gdb
提供了一個很方便的功能,在提示符下直接敲回車表示重複上一條命令。
(gdb) (直接回車) 11 int main(void) 12 { 13 int result[100]; 14 result[0] = add_range(1, 10); 15 result[1] = add_range(1, 100); 16 printf("result[0]=%d\nresult[1]=%d\n", result[0], result[1]); 17 return 0; 18
gdb
的很多常用命令有簡寫形式,例如list
命令可以寫成l
,要列一個函數的原始碼也可以用函數名做參數:
(gdb) l add_range 1 #include <stdio.h> 2 3 int add_range(int low, int high) 4 { 5 int i, sum; 6 for (i = low; i <= high; i++) 7 sum = sum + i; 8 return sum; 9 } 10
現在退出gdb
的環境:
(gdb) quit
我們做一個實驗,把原始碼改名或移到別處再用gdb
調試,這樣就列不出原始碼了:
$ mv main.c mian.c $ gdb main ... (gdb) l 5 main.c: No such file or directory. in main.c
可見gcc
的-g
選項並不是把原始碼嵌入到執行檔中的,在調試時也需要源檔案。現在把原始碼恢復原樣,我們繼續調試。首先用start
命令開始執行程序:
$ gdb main ... (gdb) start Breakpoint 1 at 0x80483ad: file main.c, line 14. Starting program: /home/akaedu/main main () at main.c:14 14 result[0] = add_range(1, 10); (gdb)
gdb
停在main
函數中變數定義之後的第一條語句處等待我們發命令,gdb
列出的這條語句是即將執行的下一條語句。我們可以用next
命令(簡寫為n
)控制這些語句一條一條地執行:
(gdb) n 15 result[1] = add_range(1, 100); (gdb) (直接回車) 16 printf("result[0]=%d\nresult[1]=%d\n", result[0], result[1]); (gdb) (直接回車) result[0]=55 result[1]=5105 17 return 0;
用n
命令依次執行兩行賦值語句和一行打印語句,在執行打印語句時結果立刻打出來了,然後停在return
語句之前等待我們發命令。雖然我們完全控制了程序的執行,但仍然看不出哪裡錯了,因為錯誤不在main
函數中而在add_range
函數中,現在用start
命令重新來過,這次用step
命令(簡寫為s
)鑽進add_range
函數中去跟蹤執行:
(gdb) start The program being debugged has been started already. Start it from the beginning? (y or n) y Breakpoint 2 at 0x80483ad: file main.c, line 14. Starting program: /home/akaedu/main main () at main.c:14 14 result[0] = add_range(1, 10); (gdb) s add_range (low=1, high=10) at main.c:6 6 for (i = low; i <= high; i++)
這次停在了add_range
函數中變數定義之後的第一條語句處。在函數中有幾種查看狀態的辦法,backtrace
命令(簡寫為bt
)可以查看函數調用的棧幀:
(gdb) bt #0 add_range (low=1, high=10) at main.c:6 #1 0x080483c1 in main () at main.c:14
可見當前的add_range
函數是被main
函數調用的,main
傳進來的參數是low=1, high=10
。main
函數的棧幀編號為1,add_range
的棧幀編號為0。現在可以用info
命令(簡寫為i
)查看add_range
函數局部變數的值:
(gdb) i locals i = 0 sum = 0
如果想查看main
函數當前局部變數的值也可以做到,先用frame
命令(簡寫為f
)選擇1號棧幀然後再查看局部變數:
(gdb) f 1 #1 0x080483c1 in main () at main.c:14 14 result[0] = add_range(1, 10); (gdb) i locals result = {0, 0, 0, 0, 0, 0, 134513196, 225011984, -1208685768, -1081160480, ... -1208623680}
注意到result
數組中有很多元素具有雜亂無章的值,我們知道未經初始化的局部變數具有不確定的值。到目前為止一切正常。用s
或n
往下走幾步,然後用print
命令(簡寫為p
)打印出變數sum
的值:
(gdb) s 7 sum = sum + i; (gdb) (直接回車) 6 for (i = low; i <= high; i++) (gdb) (直接回車) 7 sum = sum + i; (gdb) (直接回車) 6 for (i = low; i <= high; i++) (gdb) p sum $1 = 3
第一次循環i
是1,第二次循環i
是2,加起來是3,沒錯。這裡的$1
表示gdb
保存着這些中間結果,$後面的編號會自動增長,在命令中可以用$1
、$2
、$3
等編號代替相應的值。由於我們本來就知道第一次調用的結果是正確的,再往下跟也沒意義了,可以用finish
命令讓程序一直運行到從當前函數返回為止:
(gdb) finish Run till exit from #0 add_range (low=1, high=10) at main.c:6 0x080483c1 in main () at main.c:14 14 result[0] = add_range(1, 10); Value returned is $2 = 55
返回值是55,當前正準備執行賦值操作,用s
命令賦值,然後查看result
數組:
(gdb) s 15 result[1] = add_range(1, 100); (gdb) p result $3 = {55, 0, 0, 0, 0, 0, 134513196, 225011984, -1208685768, -1081160480, ... -1208623680}
第一個值55確實賦給了result
數組的第0個元素。下面用s
命令進入第二次add_range
調用,進入之後首先查看參數和局部變數:
(gdb) s add_range (low=1, high=100) at main.c:6 6 for (i = low; i <= high; i++) (gdb) bt #0 add_range (low=1, high=100) at main.c:6 #1 0x080483db in main () at main.c:15 (gdb) i locals i = 11 sum = 55
由於局部變數i
和sum
沒初始化,所以具有不確定的值,又由於兩次調用是挨着的,i
和sum
正好取了上次調用時的值,原來這跟例 3.7 “驗證局部變數存儲空間的分配和釋放”是一樣的道理,只不過我這次舉的例子設法讓局部變數sum
在第一次調用時初值為0了。i
的初值不是0倒沒關係,在for
循環中會賦值為0的,但sum
如果初值不是0,累加得到的結果就錯了。好了,我們已經找到錯誤原因,可以退出gdb
修改原始碼了。如果我們不想浪費這次調試機會,可以在gdb
中馬上把sum
的初值改為0繼續運行,看看這一處改了之後還有沒有別的Bug:
(gdb) set var sum=0 (gdb) finish Run till exit from #0 add_range (low=1, high=100) at main.c:6 0x080483db in main () at main.c:15 15 result[1] = add_range(1, 100); Value returned is $4 = 5050 (gdb) n 16 printf("result[0]=%d\nresult[1]=%d\n", result[0], result[1]); (gdb) (直接回車) result[0]=55 result[1]=5050 17 return 0;
這樣結果就對了。修改變數的值除了用set
命令之外也可以用print
命令,因為print
命令後面跟的是表達式,而我們知道賦值和函數調用也都是表達式,所以也可以用print
命令修改變數的值或者調用函數:
(gdb) p result[2]=33 $5 = 33 (gdb) p printf("result[2]=%d\n", result[2]) result[2]=33 $6 = 13
我們講過,printf
的返回值表示實際打印的字元數,所以$6
的結果是13。總結一下本節用到的gdb
命令:
表 10.1. gdb基本命令1
命令 | 描述 |
---|---|
backtrace(或bt) | 查看各級函數調用及參數 |
finish | 連續運行到當前函數返回為止,然後停下來等待命令 |
frame(或f) 幀編號 | 選擇棧幀 |
info(或i) locals | 查看當前棧幀局部變數的值 |
list(或l) | 列出原始碼,接着上次的位置往下列,每次列10行 |
list 行號 | 列出從第幾行開始的原始碼 |
list 函數名 | 列出某個函數的原始碼 |
next(或n) | 執行下一行語句 |
print(或p) | 打印表達式的值,通過表達式可以修改變數的值或者調用函數 |
quit(或q) | 退出gdb 調試環境 |
set var | 修改變數的值 |
start | 開始執行程序,停在main 函數第一行語句前面等待命令 |
step(或s) | 執行下一行語句,如果有函數調用則進入到函數中 |
1、用gdb
一步一步跟蹤第 3 節 “遞歸”講的factorial
函數,對照着圖 5.2 “factorial(3)的調用過程”查看各層棧幀的變化情況,練習本節所學的各種gdb
命令。
[20] 這麼說不夠準確,在有些平台和操作系統上第一個結果也未必正確,如果在你機器上運行第一個結果也不正確,首先檢查一下程序有沒有抄錯,如果沒抄錯那就沒關係了,順着我的講解往下看就好了,結果是多少都無關緊要。