除錯程式: 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
之後有下列常用指令可用:
-
基本指令
- quit: 結束
- help: 求助 (可加指令名稱)
- run: 執行程式 (可加餵給程式的命令列參數)
- list: 列印程式本文 (可加列號或函數名稱)
- print: 印出運算式的值
-
中斷指令
- break 列號或函數名稱: 設定中斷點
- info break: 看我們已設定了那些中斷點
- disp 運算式: 每次中斷就顯示這個運算式
- info disp: 看我們已設定了那些顯示式
- next: 執行一列程式碼 (可加欲執行的列數)
- step: 執行一列程式碼, 但是如果遇到函數呼叫, 要跳進函數裡去一步一步執行, 不要把整個函數呼叫當做一步來執行.
- cont: 執行下去, 直到下一個中斷點或程式結束為止
-
用於運算式 (例如 print 及 disp 的參數) 中的特殊變數:
- $: 前一次的運算式
- $$: 兩次前的運算式
- $7: 第七個運算式
- $$7: 倒數第七個運算式
-
與堆疊有關的指令:
- where: 顯示目前副程式層層呼叫的狀況
- up: 往上一層
- down: 往下一層
-
其他指令:
- [CR]: 重複上一個動作
info break
可以簡單地打 i b
就好了.
以上為舊資料
相關連結
更多教學文件:
- Guide to Faster, Less Frustrating Debugging 從觀念與方法的角度著眼, 而非從 gdb 指令著眼, 非常棒的除錯教學文件。 提到筆者行之已久但甚少聽人提起的 「二分逼近除錯法」。
- Debugging with GDB: 完整的 gdb 線上教學手冊。
- Peter's gdb Tutorial: 另一本完整的 gdb 線上教學手冊。
- gdb Tutorial: Andrew Gilpin 寫的教學文件, 長短適中, 以一個小型 C++ 程式為例。
- RMS's gdb Tutorial: 問答形式的 gdb 文件, 比較像 faq。
gdb 的圖形外衣:
- 本頁最新版網址: https://frdm.cyut.edu.tw/~ckhung/b/c/gdb.php; 您所看到的版本: February 14 2012 02:32:25.
- 作者: 朝陽科技大學 資訊管理系 洪朝貴
- 寶貝你我的地球, 請 減少列印, 多用背面, 丟棄時做垃圾分類。
- 本文件以 Creative Commons Attribution-ShareAlike License 或以 Free Document License 方式公開授權大眾自由複製/修改/散佈。