除錯程式: gdb


對程式設計師而言, 最簡單方便的除錯程式就是列印指令/函數 -- 如果你的程式執行結果與預期不符, 就將中間計算過程一路印出。 但這並不方便。 例如某個迴圈可能在第一百萬零七十三次才出錯, 這當中印出來的 debug 資料可能多到令人頭昏。 這時候你需要一個工具, 讓你可以一邊執行你的程式, 一邊視需要將它停下來觀察當中某些變數的變化。 如果發現問題不在此處, 還可以叫它繼續執行。 執行到一半, 還可以手動修改變數的值, 看看如果這個錯解決了, 下個錯又在何處, 一口氣解決好幾個問題。 這些就是 debugger 的工作。

gdb 是一個命令列模式的 debugger。 如果你寫的程式用的是 C, objective C, C++, Fortran, Pascal, Ada, ... 等等語言, 而且採用的編譯器 來自 gnu, 就可以拿 gdb 來除錯。 以下我們拿 選擇排序 程式的 錯誤示範版 來練習除錯。

首先一如往常, 用 gcc -Wall -o ssort ssort_bug.c 編譯出執行檔 ssort。 然後執行一下 ./ssort 12.3 -17 6.5 結果出現 Segmentation fault。 很好, 這和 win32 上面經常出現的 "請與程式設計師聯絡" 一樣, 是最無濟於事的錯誤訊息。

於是下 gdb ssort 進入 gdb 開始除錯。 在 gdb 的命令列底下打: run 12.3 -17 6.5 餵給 ssort 程式相同的命令列參數。 結果出現類似這樣的訊息:

    Program received signal SIGSEGV, Segmentation fault.
    0x4004ca01 in __strtod_internal () from /lib/libc.so.6

我們到底執行到何處了呢? 下 where, 出現類似這樣的訊息:

    #0  0x4004ca01 in __strtod_internal () from /lib/libc.so.6
    #1  0x40042d93 in atof () from /lib/libc.so.6
    #2  0x080483a9 in main ()
    #3  0x4002f80c in __libc_start_main () from /lib/libc.so.6

這些都是函數的名字, 表示 __libc_start_main 執行到一半, 正在呼叫 main, 而 main 執行到一半, 正在呼叫 atof, atof 又執行到一半, 正在呼叫 __strtod_internal, 結果就掛在這裡了。 那麼印一點程式原始碼來看吧: l 好像沒有我們熟悉的東西。

看來只有 main() 是我們自己寫的程式; 其他都是系統函數, 難怪沒有原始碼。 向上兩層: up 2, 按兩次上箭頭, 把先前敲的 l 指令叫出來再列印一次。 這個快速鍵的功能由 GNU readline 提供, 還有一些簡單方便的快速鍵, 值得一學。

奇怪, 還是沒有原始碼...? 對了, 編譯程式時必須告訴 compiler 將 debug 需要用的的資訊一起編譯進去才對。 不必跳出 gdb, 請開另外一個視窗, 重新編譯: gcc -Wall -g -o ssort ssort_bug.c 這裡的 -g 就是要 debug 的意思。 然後在原來的 gdb 視窗下: run 重跑一遍。 這次不必再給參數, 它自己會記得用上次所給的參數。

這時它會問你是否真的要重新執行。 這是因為程式正 debug 到一半, 你可能已經花了很多功夫, 中斷又執行好幾次, 好不容易才找到這裡來, 如果這麼一重跑, 先前的一切都要泡湯了。 不過我們的狀況很簡單, 所以大方地按 "y"。 請注意它發現你的程式已重新編譯過了, 所以提醒你:

    'ssort' has changed; re-reading symbols.

這次再打 where, 印出的訊息好像更多一點。 up 2 之後, 發現它印出發生問題的那一句話。 我們還是下 l 把上下文的程式碼一起印出來, 看起來比較清楚一點。

如果你不小心又按了一次 Enter 鍵 (馬上試試看吧!), 它會接著往下印十列。 在 gdb 裡面, 為了方便你重複下同一個指令, 有許多時候按 Enter 就表示重複上一個指令。

沒關係, 再下一次 where 提醒自己究竟除錯到何處。 這次請特別注意它印出來的 "#2 ... at ssort_bug.c:12" 我們依樣畫葫蘆, 下 l ssort_bug.c:12。 不需要查手冊也猜得出來: 這表示 "ssort_bug.c 這個檔案的第 12 列"。 因為大程式的原始碼可能包含許多個 .c 檔, 所以要與 gdb 溝通程式第幾列, 應該連檔帶列完整指明才是最保險的方式。 不過我們現在只有一個檔案, 所以其實只打 l 12 也可以。

好了, 我們的程式究竟那裡出錯呢? 把變數印出來看看吧: p i 好像沒什麼問題; 那麼 p argv[i] 呢? 有點奇怪了... 0x0 是空指標, 也就是 NULL。 請將 argv 陣列的每個元素都印出來看。 找到原因了嗎?

回原始碼調整迴圈終止條件之後, 重新編譯, 並在 gdb 底下重新執行 (一樣, 不需要離開 gdb)。 這次程式可以執行, 但是印出來的結果並不正確 -- 根本就沒有排序。 但是究竟是何處出錯呢? 程式已執行完, where 告訴我們沒有東西可看。 我們在程式將要開始執行之前就將它停下來吧: b main 這句話的意思是在進入副程式 main 的入口處設一個 break point 中斷點。 當然下 b ssort_bug.c:7 也可以。 馬上用 i b 查看一下現在已經設定了那些 break points, 用 d b 1 將唯一的 break point 刪除, 再下 b ssort_bug.c:7, 最後再用 i b 檢查。

好, 重新執行一次, 程式停在 main 的門檻, 第 7 列。 下 n 叫它執行一步。 然後一直按 Enter, 同時注意它正在執行那一列, 直到結束為止。

當然單單逐步執行程式是不夠的, 還需要在過程當中下 p 指令, 把可疑變數的值印出來看。 請重新執行一次, 迴圈當中每次計算出 min, 就將它印出來。

待續...


以下為舊資料

假設你要對 a.out 除錯, 可以下指令: gdb a.out 進入 gdb 之後有下列常用指令可用:

  1. 基本指令
    1. quit: 結束
    2. help: 求助 (可加指令名稱)
    3. run: 執行程式 (可加餵給程式的命令列參數)
    4. list: 列印程式本文 (可加列號或函數名稱)
    5. print: 印出運算式的值
  2. 中斷指令
    1. break 列號或函數名稱: 設定中斷點
    2. info break: 看我們已設定了那些中斷點
    3. disp 運算式: 每次中斷就顯示這個運算式
    4. info disp: 看我們已設定了那些顯示式
    5. next: 執行一列程式碼 (可加欲執行的列數)
    6. step: 執行一列程式碼, 但是如果遇到函數呼叫, 要跳進函數裡去一步一步執行, 不要把整個函數呼叫當做一步來執行.
    7. cont: 執行下去, 直到下一個中斷點或程式結束為止
  3. 用於運算式 (例如 print 及 disp 的參數) 中的特殊變數:
    1. $: 前一次的運算式
    2. $$: 兩次前的運算式
    3. $7: 第七個運算式
    4. $$7: 倒數第七個運算式
  4. 與堆疊有關的指令:
    1. where: 顯示目前副程式層層呼叫的狀況
    2. up: 往上一層
    3. down: 往下一層
  5. 其他指令:
    1. [CR]: 重複上一個動作
事實上大部分的指令只要沒有混淆之虞, 都不必完整地打完, 例如 info break 可以簡單地打 i b 就好了.
以上為舊資料

相關連結

更多教學文件:

  1. Guide to Faster, Less Frustrating Debugging 從觀念與方法的角度著眼, 而非從 gdb 指令著眼, 非常棒的除錯教學文件。 提到筆者行之已久但甚少聽人提起的 「二分逼近除錯法」。
  2. Debugging with GDB: 完整的 gdb 線上教學手冊。
  3. Peter's gdb Tutorial: 另一本完整的 gdb 線上教學手冊。
  4. gdb Tutorial: Andrew Gilpin 寫的教學文件, 長短適中, 以一個小型 C++ 程式為例。
  5. RMS's gdb Tutorial: 問答形式的 gdb 文件, 比較像 faq。

gdb 的圖形外衣:

  1. ddd: 蠻熱門的 gdb 圖形外衣。
  2. ibiblio 搜集了很多除錯工具, 包含有點古老的 xxgdb。
  3. gnu 的網站有更多連結。