GDB 101

contributed by < hsuedw >

如果你寫的程式用的是 C, objective C, C++, Fortran, Pascal, Ada, 等等語言, 而且採用的編譯器 來自 gnu, 就可以拿 gdb 來除錯。

進入 gdb 的除錯環境

在命令列的 shell 提示符號下,執行 gdb 就可以進入 gdb 的除錯環境。

$ gdb
GNU gdb (Ubuntu 9.2-0ubuntu1~20.04) 9.2
Copyright (C) 2020 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 "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word".
(gdb)

離開 gdb 的除錯環境

gdb 的提示符號下輸入 qquit 就可以離開 gdb 的除錯環境。

(gdb) q

用一個簡單的程式學習 gdb 的基本操作

在接下來的章節中,以 factorial_buggy.c 學習 gdb 的基本操作

factorial_buggy.c
#include <stdio.h> int main() { // Factorials aren't defined for negative integers. int num; printf("Enter a postitive integer: "); scanf("%d", &num); int factorial; for (int i = 1; i <= num; ++i) factorial = factorial * i; printf("%d! = %d\n", num, factorial); return 0; }

factorial_buggy 會要求使用者輸入一個正整數,然後算出該正整數的階乘。也就是說,如果使用者輸入 4factorial_buggy 應該會印出 4! = 24。將 factorial_buggy.c 編譯並執行後,卻得到下列結果。可見,目前這版程式有問題。

$ gcc factorial_buggy.c -o factorial_buggy
$ ./factorial_buggy
Enter a postitive integer: 4
4! = -837404800

在開始之前有一件事要注意。gdb 只能對可執行檔 (executable file) 進行除錯。它無法對原始碼 (source code) 進行除錯。
還有一件事要注意的是,最好可以在編譯程式碼時,加入 -g 參數。這樣我們在用 gdb 除錯時可以有更完整的資訊

$ gcc factorial_buggy.c -g -o factorial_buggy

$ man gcc
:
-g Produce debugging information in the operating system's native format
(stabs, COFF, XCOFF, or DWARF). GDB can work with this debugging
information.

On most systems that use stabs format, -g enables use of extra debugging
information that only GDB can use; this extra information makes debugging
work better in GDB but probably makes other debuggers crash or refuse to
read the program. If you want to control for certain whether to generate
the extra information, use -gstabs+, -gstabs, -gxcoff+, -gxcoff, or -gvms
(see below).
:

為了方便起見,我們寫了一個簡單的 Makefile 來協助我們編譯程式。

factorial:
        gcc -g -std=c99 -Wall -Werror factorial_buggy.c -o factorial_buggy

開始對程式進行除錯

首先,在 shell 提示符號下執行 gdb ./factorial_buggy 進入 gdb 的執行環境,並開始對 factorial_buggy 進行除錯。

$ gdb ./factorial_buggy
GNU gdb (Ubuntu 9.2-0ubuntu1~20.04) 9.2
Copyright (C) 2020 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 "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./factorial_buggy...
(gdb)

gdb 中執行程式

gdb 的提示符號下執行 rrun 就可以執行我們想要除錯的程式。

(gdb) r
Starting program: /home/edward/workspace/linux-2022/c/factorial_buggy
Enter a postitive integer:

執行 r 指令後,我們想要除錯的程式便開始執行。然後, gdb 會停在 factorial_buggy 所印出的提示訊息 Enter a postitive integer:。接著,在此提示訊息後輸入 4。 果然,4! 不是 24,而是一個奇怪的數字。

(gdb) r
Starting program: /home/edward/workspace/linux-2022/c/factorial_buggy
Enter a postitive integer: 4
4! = -173568
[Inferior 1 (process 197911) exited normally]
(gdb)

做到這裡,factorial_buggy 已經執行結束。我們仍然無法得知到底是哪裡出了問題。所以,我們必須重新執行程式。

設定中斷點 (breakpoint)

breakpoint 的功能是讓 gdb 執行到某一行程式時,可以暫停執行,讓我們可以觀察當時程式的狀態。例如,查看變數內的值,或是記憶體中的內容。
因為我們目前還不知道程式到的是哪裡有問題,所以我們把 breakpoing 先設定在程式的開頭。從程式的開頭開始追蹤程式並除錯。
gdb 提示符號下輸入 break mainb main

(gdb) b main
Breakpoint 1 at 0x555555555189: file factorial_buggy.c, line 4.

至此,我們在程式的一開始設定了一個中斷點 Breakpoint 1 。由 gdb 顯示的訊息可以看出,這個 breakpoint 的位置對應到原始碼的第 4 行。
然後,在 gdb 提示符號下輸入 rrun 重新開始執行 factorial_buggy

gdb) r
Starting program: /home/edward/workspace/linux-2022/c/factorial_buggy

Breakpoint 1, main () at factorial_buggy.c:4
4       {

果然, gdb 停在第 4 行。也就是我們剛剛設定的 Breakpoint 1

gdb 中印出程式碼

這時,我們可以在 gdb 提示符號下輸入 llist 印出程式碼。這時,gdb 會以我們停在的那一行為中心,上下展開一個範圍,並印出這個範圍的程式碼。在執行一次 l 則會印出下一組程式碼。我們可以一直執行 l 直到檔案末尾為止。

Breakpoint 1, main () at factorial_buggy.c:4
4       {
(gdb) l
1       #include <stdio.h>
2
3       int main()
4       {
5           // Factorials aren't defined for negative integers.
6           int num;
7           printf("Enter a postitive integer: ");
8           scanf("%d", &num);
9
10          int factorial;
(gdb) l
11          for (int i = 1; i <= num; ++i)
12              factorial = factorial * i;
13          printf("%d! = %d\n", num, factorial);
14
15          return 0;
16      }

此時若再執行一次 l ,則 gdb 會顯示 out of range 的訊息。

(gdb) l
Line number 17 out of range; factorial_buggy.c has 16 lines.

沒關係,在 l 後面加上行號就可以在印出程式碼了。

(gdb) l 1
1       #include <stdio.h>
2
3       int main()
4       {
5           // Factorials aren't defined for negative integers.
6           int num;
7           printf("Enter a postitive integer: ");
8           scanf("%d", &num);
9
10          int factorial;

單步執行

接下來用 next (可簡寫為 n) 做單步執行。

(gdb) n
7           printf("Enter a postitive integer: ");

可以觀察到,執行 next 後,程式停在第 7 行。但第 7 行還沒有執行。也就是 Enter a postitive integer: 這行提示訊息還沒有被印出來。再執行一次 next,同樣地,程式停在地 8 行,但也還沒執行。雖然此時第 7 行已被執行,但我們還沒有看到提示訊息被印出來。

(gdb) n
8           scanf("%d", &num);

再執行一次 next,提示訊息 Enter a postitive integer: 就被印出來了。這時,factorial_buggy 正等著我們輸入一個正整數。
這時我們輸入 4,然後程式就進入了第 11~12 行的 for 迴圈。然後,我們接連執行 next 幾次之後,for 迴圈結束。程式執行到了第 13 行。算一下 for 迴圈共執行了 4 次。也就是說變數 i 由初值 1 ,每執行一次 for 迴圈,變數 i 遞增 1。一路遞增到變數 i 的值為 5,在第 5 次檢查測試條件時失敗 (5 <= 4 為 false)。然後離開 for 迴圈。所以,for 迴圈的執行看起來沒有問題。

(gdb) n
Enter a postitive integer: 4
11          for (int i = 1; i <= num; ++i)
(gdb) n
12              factorial = factorial * i;
(gdb) n
11          for (int i = 1; i <= num; ++i)
(gdb) n
12              factorial = factorial * i;
(gdb) n
11          for (int i = 1; i <= num; ++i)
(gdb) n
12              factorial = factorial * i;
(gdb) n
11          for (int i = 1; i <= num; ++i)
(gdb) n
12              factorial = factorial * i;
(gdb) n
11          for (int i = 1; i <= num; ++i)
(gdb) n
13          printf("%d! = %d\n", num, factorial);

再執行一次 next。我們可以看到 factorial_buggy 印出了 4! = -173568 。最後,停在第 15 行。

(gdb) n
4! = -173568
15          return 0;

再執行幾次 next。程式又再一次的結束。
可是,我們還是不知道到底錯誤在哪裡?

(gdb) n
16      }
(gdb) n
__libc_start_main (main=0x555555555189 <main>, argc=1, argv=0x7fffffffe3c8,
    init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>,
    stack_end=0x7fffffffe3b8) at ../csu/libc-start.c:342
342     ../csu/libc-start.c: No such file or directory.
(gdb) n
[Inferior 1 (process 198616) exited normally]

觀察程式執行過程中變數的變化

我們再重新執行一次 factorial_buggy 。這次我們要觀察在程式執行的過程中,變數內容的的變化。
這次我們還是一樣想要知道 4! 是多少。所以,我們仍然輸入 4 。然後,程式就進入 for 迴圈。
此時,用 pprint 印出變數 num 的內容。結果如我們所預期的,就是我們剛剛輸入的 4

(gdb) r
Starting program: /home/edward/workspace/linux-2022/c/factorial_buggy

Breakpoint 1, main () at factorial_buggy.c:4
4       {
(gdb) n
7           printf("Enter a postitive integer: ");
(gdb) n
8           scanf("%d", &num);
(gdb) n
Enter a postitive integer: 4
11          for (int i = 1; i <= num; ++i)
(gdb) p num
$1 = 4
(gdb)

factorial_buggy.c 中,我們使用了三個變數,numfactorial 以及 i 。 所以,也把另外兩個印出來看看。

(gdb) n
12              factorial = factorial * i;
(gdb) p factorial
$5 = -7232
(gdb) p i
$6 = 1

這是 for 迴圈的第一個 iteration。很明顯地, factorial 這個變數的內容怪怪的。再看一下程式碼
這次我們注意到了第 10 行。變數 factorial 在第 10 行被定義之後,並沒有被指定初值。接著就進入 for 迴圈,不斷重複運算第 12 行,以計算 4!
至此,我們已經知道問題出在哪裡了。在計算 4! 前,沒有對變數 factorial 指定適當的初始值。所以變數 factorial 被定義後,其內容是 garbage。

(gdb) l
7           printf("Enter a postitive integer: ");
8           scanf("%d", &num);
9
10          int factorial;
11          for (int i = 1; i <= num; ++i)
12              factorial = factorial * i;
13          printf("%d! = %d\n", num, factorial);
14
15          return 0;
16      }
(gdb)

gdb 中修改變數內容

我們可以在 gdb 中直接修改變數內容來確認我們的想法是否正確。
這次在程式第 9 行再設定一個 breakpoint。然後重新執行程式。

(gdb) b factorial_buggy.c:9
Breakpoint 2 at 0x5555555551cd: file factorial_buggy.c, line 11.
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/edward/workspace/linux-2022/c/factorial_buggy

Breakpoint 1, main () at factorial_buggy.c:4
4       {

這次設定 breakpoint 的指令跟上次不大一樣。我們指定了檔名與行號。這樣可以讓 gdb 明確地知道我們想把 breakpoint 設在哪裡。這個用法在程式由多個 source file 編譯而成時很好用。

(gdb) b factorial_buggy.c:9
Breakpoint 2 at 0x5555555551cd: file factorial_buggy.c, line 11.

此時,程式停留在 Breakpoint 1 ,也就是第 4 行。我們可以執行 ccont 直接執行到 Breakpoint 2

(gdb) c
Continuing.
Enter a postitive integer: 4

Breakpoint 2, main () at factorial_buggy.c:11
11          for (int i = 1; i <= num; ++i)
(gdb) n
12              factorial = factorial * i;
(gdb) p num
$1 = 4
(gdb) p factorial
$2 = -7232
(gdb) p i
$3 = 1
(gdb)

此時,變數 factorial 的值為 -7232
最棒的是,我們不需要離開 gdb ,修改程式碼,重新編譯,然後再進入 gdb 進行除錯。我們現在就可以直接用 pprint 指令更改變數 factorial 的初始值。所以,我們執行 p factoria = 1 將變數 factorial 初始化為 1
然後,我們檢查一下變數 factorialnum 以及 i 的值是否分別為 141

(gdb) p factorial = 1
$4 = 1
(gdb) p factorial
$5 = 1
(gdb) p num
$6 = 4
(gdb) p i
$7 = 1
(gdb)

或者,我們可以用 info locals 指令印出目前所有的變數內容。

(gdb) info locals
i = 1
num = 4
factorial = 1
(gdb)

我們在執行一次 iteration,確認是否所有的變數內容都如我們所預期。

(gdb) n
11          for (int i = 1; i <= num; ++i)
(gdb) n
12              factorial = factorial * i;
(gdb) n
11          for (int i = 1; i <= num; ++i)
(gdb) info locals
i = 2
num = 4
factorial = 2
(gdb)

變數 factorial 的值的確變為 2。所以我們可以直接用 ccont 命令一路直行到最後。
而經過修改後的程式也正確地算出 4! = 24。這也驗證了我們當初的想法是正確的。

(gdb) c
Continuing.
4! = 24
[Inferior 1 (process 11946) exited normally]
(gdb)

列出目前的 breakpoint

我們可以用 i binfo break 列出目前所有的 breakpoint

(gdb) i b
Num     Type           Disp Enb Address            What
1       breakpoint     keep n   0x0000555555555189 in main at factorial_buggy.c:4
2       breakpoint     keep n   0x00005555555551cd in main at factorial_buggy.c:10

修改程式碼

請注意! 我們沒有離開 gdb
接下來我們跳到另一個 console ,用編輯器開啟 factorial_buggy.c 。修改第 10 行,在定義變數 factorial 的同時,將它初始化為 1

int factorial = 1;

然後回到我們剛剛用 gdb 進行除錯的 console。在 gdb 的提示符號下執行 make factorial。我們的程式就會被重新編譯了。

(gdb) make factorial
gcc -g -std=c99 -Wall -Werror factorial_buggy.c -o factorial_buggy
(gdb)

接著,在 gdb 的提示符號下執行 disable 關閉所有的 breakpoint。

(gdb) disable

最後,在 gdb 的提示符號下執行 r 確認修改過並重新編譯過的程式是否可以正常運作。

(gdb) r
`/home/edward/workspace/linux_2022/tmp/factorial_buggy' has changed; re-reading symbols.
Starting program: /home/edward/workspace/linux_2022/tmp/factorial_buggy
warning: Probes-based dynamic linker interface failed.
Reverting to original interface.
Enter a postitive integer: 4
4! = 24
[Inferior 1 (process 12049) exited normally]
(gdb)

看起來我們已經把 bug 解決了。

常用 gdb 指令列表

  1. 基本指令
    • quit: 結束
    • help: 求助 (可加指令名稱)
    • run: 執行程式 (可加餵給程式的命令列參數)
    • list: 列印程式本文 (可加列號或函數名稱)
    • print: 印出運算式的值
    • info locals: 印出所有區域變數的內容
  2. 中斷指令
    • break 列號或函數名稱: 設定中斷點
    • info break: 看我們已設定了那些中斷點
    • disp 運算式: 每次中斷就顯示這個運算式
    • info disp: 看我們已設定了那些顯示式
    • next: 執行一列程式碼 (可加欲執行的列數)
    • step: 執行一列程式碼, 但是如果遇到函數呼叫, 要跳進函數裡去一步一步執行, 不要把整個函數呼叫當做一步來執行.
    • cont: 執行下去, 直到下一個中斷點或程式結束為止
  3. 用於運算式 (例如 print 及 disp 的參數) 中的特殊變數:
    • $: 前一次的運算式
    • $$: 兩次前的運算式
    • $7: 第七個運算式
    • $$7: 倒數第七個運算式
  4. 與堆疊有關的指令:
    • where: 顯示目前副程式層層呼叫的狀況
    • up: 往上一層
    • down: 往下一層
  5. 其他指令:
    • 按 Enter 鍵: 重複上一個動作

參考資料