---
tags: debug, gdb
---
# GDB 101
contributed by < `hsuedw` >
如果你寫的程式用的是 C, objective C, C++, Fortran, Pascal, Ada, ... 等等語言, 而且採用的編譯器 來自 gnu, 就可以拿 gdb 來除錯。
## 進入 `gdb` 的除錯環境
在命令列的 shell 提示符號下,執行 `gdb` 就可以進入 `gdb` 的除錯環境。
```shell
$ 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` 的提示符號下輸入 `q` 或 `quit` 就可以離開 `gdb` 的除錯環境。
```shell
(gdb) q
```
## 用一個簡單的程式學習 `gdb` 的基本操作
在接下來的章節中,以 `factorial_buggy.c` 學習 `gdb` 的基本操作
:::spoiler {state=open} factorial_buggy.c
```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` 會要求使用者輸入一個正整數,然後算出該正整數的階乘。也就是說,如果使用者輸入 `4`, `factorial_buggy` 應該會印出 `4! = 24`。將 `factorial_buggy.c` 編譯並執行後,卻得到下列結果。可見,目前這版程式有問題。
```shell
$ 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` 來協助我們編譯程式。
```Makefile
factorial:
gcc -g -std=c99 -Wall -Werror factorial_buggy.c -o factorial_buggy
```
### 開始對程式進行除錯
首先,在 shell 提示符號下執行 `gdb ./factorial_buggy` 進入 `gdb` 的執行環境,並開始對 `factorial_buggy` 進行除錯。
```shell
$ 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` 的提示符號下執行 `r` 或 `run` 就可以執行我們想要除錯的程式。
```shell
(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`,而是一個奇怪的數字。
```shell
(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 main` 或 `b main` 。
```shell
(gdb) b main
Breakpoint 1 at 0x555555555189: file factorial_buggy.c, line 4.
```
至此,我們在程式的一開始設定了一個中斷點 `Breakpoint 1` 。由 `gdb` 顯示的訊息可以看出,這個 breakpoint 的位置對應到原始碼的第 4 行。
然後,在 `gdb` 提示符號下輸入 `r` 或 `run` 重新開始執行 `factorial_buggy`。
```shell
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` 提示符號下輸入 `l` 或 `list` 印出程式碼。這時,`gdb` 會以我們停在的那一行為中心,上下展開一個範圍,並印出這個範圍的程式碼。在執行一次 `l` 則會印出下一組程式碼。我們可以一直執行 `l` 直到檔案末尾為止。
```shell
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` 的訊息。
```shell
(gdb) l
Line number 17 out of range; factorial_buggy.c has 16 lines.
```
沒關係,在 `l` 後面加上行號就可以在印出程式碼了。
```shell
(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`) 做單步執行。
```shell
(gdb) n
7 printf("Enter a postitive integer: ");
```
可以觀察到,執行 `next` 後,程式停在第 7 行。但第 7 行還沒有執行。也就是 `Enter a postitive integer: ` 這行提示訊息還沒有被印出來。再執行一次 `next`,同樣地,程式停在地 8 行,但也還沒執行。雖然此時第 7 行已被執行,但我們還沒有看到提示訊息被印出來。
```shell
(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 行。
```shell
(gdb) n
4! = -173568
15 return 0;
```
再執行幾次 `next`。程式又再一次的結束。
可是,我們還是不知道到底錯誤在哪裡?
```shell
(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` 迴圈。
此時,用 `p` 或 `print` 印出變數 `num` 的內容。結果如我們所預期的,就是我們剛剛輸入的 `4`。
```shell
(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` 中,我們使用了三個變數,`num` , `factorial` 以及 `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。
```shell
(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。然後重新執行程式。
```shell
(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 編譯而成時很好用。
```shell
(gdb) b factorial_buggy.c:9
Breakpoint 2 at 0x5555555551cd: file factorial_buggy.c, line 11.
```
此時,程式停留在 `Breakpoint 1` ,也就是第 4 行。我們可以執行 `c` 或 `cont` 直接執行到 `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` 進行除錯。我們現在就可以直接用 `p` 或 `print` 指令更改變數 `factorial` 的初始值。所以,我們執行 `p factoria = 1` 將變數 `factorial` 初始化為 `1`。
然後,我們檢查一下變數 `factorial` , `num` 以及 `i` 的值是否分別為 `1` , `4` 與 `1` 。
```shell
(gdb) p factorial = 1
$4 = 1
(gdb) p factorial
$5 = 1
(gdb) p num
$6 = 4
(gdb) p i
$7 = 1
(gdb)
```
或者,我們可以用 `info locals` 指令印出目前所有的變數內容。
```shell
(gdb) info locals
i = 1
num = 4
factorial = 1
(gdb)
```
我們在執行一次 iteration,確認是否所有的變數內容都如我們所預期。
```shell
(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`。所以我們可以直接用 `c` 或 `cont` 命令一路直行到最後。
而經過修改後的程式也正確地算出 `4! = 24`。這也驗證了我們當初的想法是正確的。
```shell
(gdb) c
Continuing.
4! = 24
[Inferior 1 (process 11946) exited normally]
(gdb)
```
### 列出目前的 breakpoint
我們可以用 `i b` 或 `info 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`。
```c=10
int factorial = 1;
```
然後回到我們剛剛用 `gdb` 進行除錯的 console。在 `gdb` 的提示符號下執行 `make factorial`。我們的程式就會被重新編譯了。
```shell
(gdb) make factorial
gcc -g -std=c99 -Wall -Werror factorial_buggy.c -o factorial_buggy
(gdb)
```
接著,在 `gdb` 的提示符號下執行 `disable` 關閉所有的 breakpoint。
```shell
(gdb) disable
```
最後,在 `gdb` 的提示符號下執行 `r` 確認修改過並重新編譯過的程式是否可以正常運作。
```shell
(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 鍵: 重複上一個動作
## 參考資料
* [introduction to GDB a tutorial - Harvard CS50](https://www.youtube.com/watch?v=sCtY--xRUyI)
* [Beej's Quick Guide to GDB](https://jasonblog.github.io/note/bggdb/index.html)