1. 程式人生 > >gdb除錯(1):單步執行和跟蹤函式呼叫

gdb除錯(1):單步執行和跟蹤函式呼叫

轉發自:http://songjinshan.com/akabook/zh/gdb.html#id1

看下面的程式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#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[1000]; 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

第一個結果正確,第二個結果顯然不正確 [1]

 ,在小學我們就聽說過高斯小時候的故事,從1加到100應該是5050。一段程式碼,第一次執行結果是對的,第二次執行卻不對,這是很常見的一類錯誤現象,這種情況一方面要懷疑程式碼,另一方面更要懷疑資料:第一次和第二次執行的都是同一段程式碼,如果程式碼是錯的,那第一次的結果為什麼能對呢?所以很可能是第二次執行時相關的狀態和資料錯了,錯誤的資料導致了錯誤的結果。在動手除錯之前,讀者先試試只看程式碼能不能看出錯誤原因,只要前面幾章學得紮實就應該能看出來。

[1]如果你編譯執行這個程式的環境和我的環境(Ubuntu 12.04 LTS 32位x86)不同,也許在你的機器上跑不出這個結果,那也沒關係,重要的是學會本章介紹的思想方法。另外你也可以嘗試修改程式,總有辦法得到類似的結果,上例中故意定義了一個很大的陣列 result[1000]
 ,修改陣列的大小就會改變各區域性變數的儲存空間的位置,執行結果就可能會不同。

在編譯時要加上 -g 選項,生成的可執行檔案才能用 gdb 進行原始碼級除錯:

$ gcc -g main.c -o main
$ gdb main
GNU gdb (Ubuntu/Linaro 7.4-2012.02-0ubuntu2) 7.4-2012.02
Copyright (C) 2012 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 "i686-linux-gnu".
For bug reporting instructions, please see:
<http://bugs.launchpad.net/gdb-linaro/>...
Reading symbols from /home/akaedu/main...done.
(gdb)

-g 選項的作用是在可執行檔案中加入原始檔的資訊,即可執行檔案 main 中的第幾條機器指令對應原始檔 main.c 的第幾行,但並不是把整個原始檔嵌入到可執行檔案中,所以在除錯時必須保證 gdb 能找到原始檔 main.c 。 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-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[1000];
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.

可見 gcc 的 -g 選項並不是把原始碼嵌入到可執行檔案中,在除錯時也需要原始檔。現在把原始碼恢復原樣,我們繼續除錯。首先用 start 命令開始執行程式:

$ gdb main
...
(gdb) start
Temporary breakpoint 1 at 0x8048415: file main.c, line 14.
Starting program: /home/akaedu/main

Temporary breakpoint 1, 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
Temporary breakpoint 2 at 0x8048415: file main.c, line 14.
Starting program: /home/akaedu/main

Temporary breakpoint 2, 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 0x08048429 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  0x08048429 in main () at main.c:14
14           result[0] = add_range(1, 10);
(gdb) i locals
result = {0 <repeats 471 times>, 1184572, 0 <repeats 11 times>, -1207961512, -1073746088, 1249268, -1073745624, 1142336,
...

注意到 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
0x08048429 in main () at main.c:14
14           result[0] = add_range(1, 10);
Value returned is $2 = 55

返回值是55,當前正準備執行賦值操作,用 n 命令執行賦值操作後檢視 result 陣列:

(gdb) n
15           result[1] = add_range(1, 100);
(gdb) p result
$3 = {55, 0 <repeats 470 times>, 1184572, 0 <repeats 11 times>, -1207961512, -1073746088, 1249268, -1073745624, 1142336,
...

第一個值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  0x08048441 in main () at main.c:15
(gdb) i locals
i = 11
sum = 55

由於區域性變數 i 和 sum 沒初始化,所以具有不確定的值,又由於兩次呼叫是挨著的, i 和 sum 正好取了上次呼叫時的值,回顧一下我們講過的 驗證區域性變數儲存空間的分配和釋放 那個例子,其實和現在這個例子是一樣的道理,只不過我這次舉的例子設法讓區域性變數 sum 在第一次呼叫時初值為0而第二次呼叫時初值不為0。 i 的初值不確定倒沒關係,在 for 迴圈中首先會把 i 賦值為 low ,但 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
0x08048441 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 命令:

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)執行下一行語句,如果有函式呼叫則進入到函式中