天天看點

使用 GDB 調試 Linux 軟體

編譯

開始調試之前,必須用程式中的調試資訊編譯要調試的程式。這樣,gdb 才能夠調試所使用的變量、代碼行和函數。如果要進行編譯,請在 gcc(或 g++)下使用額外的 '-g' 選項來編譯程式:

gcc -g eg.c -o eg      

回頁首

運作 gdb

在 shell 中,可以使用 'gdb' 指令并指定程式名作為參數來運作 gdb,例如 'gdb eg';或者在 gdb 中,可以使用 file 指令來裝入要調試的程式,例如 'file eg'。這兩種方式都假設您是在包含程式的目錄中執行指令。裝入程式之後,可以用 gdb 指令 'run' 來啟動程式。

調試會話示例

如果一切正常,程式将執行到結束,此時 gdb 将重新獲得控制。但如果有錯誤将會怎麼樣?這種情況下,gdb 會獲得控制并中斷程式,進而可以讓您檢查所有事物的狀态,如果運氣好的話,可以找出原因。為了引發這種情況,我們将使用一個 示例程式:

代碼示例 eg1.c
#include 
int wib(int no1, int no2)
{
  int result, diff;
  diff = no1 - no2;
  result = no1 / diff;
  return result;
}
int main(int argc, char *argv[])
{
  int value, div, result, i, total;
  value = 10;
  div = 6;
  total = 0;
  for(i = 0; i < 10; i++)
  {
    result = wib(value, div);
    total += result;
    div++;
    value--;
  }
  printf("%d wibed by %d equals %d\n", value, div, total);
  return 0;
}
      

這個程式将運作 10 次 for 循環,使用 'wib()" 函數計算出累積值,最後列印出結果。

在您喜歡的文本編輯器中輸入這個程式(要保持相同的行距),儲存為 'eg1.c',使用 'gcc -g eg1.c -o eg1' 進行編譯,并用 'gdb eg1' 啟動 gdb。使用 'run' 運作程式可能會産生以下消息:

Program received signal SIGFPE, Arithmetic exception.
0x80483ea in wib (no1=8, no2=8) at eg1.c:7
7         result = no1 / diff;
(gdb)
      

gdb 指出在程式第 7 行發生一個算術異常,通常它會列印這一行以及 wib() 函數的自變量值。要檢視第 7 行前後的源代碼,請使用 'list' 指令,它通常會列印 10 行。再次輸入 'list'(或者按回車重複上一條指令)将列出程式的下 10 行。從 gdb 消息中可以看出,第 7 行中的除法運算出了錯,程式在這一行中将變量 "no1" 除以 "diff"。

要檢視變量的值,使用 gdb 'print' 指令并指定變量名。輸入 'print no1' 和 'print diff',可以相應看到 "no1" 和 "diff" 的值,結果如下:

(gdb) print no1
$5 = 8
(gdb) print diff
$2 = 0
      

gdb 指出 "no1" 等于 8,"diff" 等于 0。根據這些值和第 7 行中的語句,我們可以推斷出算術異常是由除數為 0 的除法運算造成的。清單顯示了第 6 行計算的變量 "diff",我們可以列印 "diff" 表達式(使用 'print no1 - no2' 指令),來重新估計這個變量。gdb 告訴我們 wib 函數的這兩個自變量都等于 8,于是我們要檢查調用 wib() 函數的 main() 函數,以檢視這是在什麼時候發生的。在允許程式自然終止的同時,我們使用 'continue' 指令告訴 gdb 繼續執行。

(gdb) continue
Continuing.
Program terminated with signal SIGFPE, Arithmetic exception.
The program no longer exists.
      

使用斷點

為了檢視在 main() 中發生了什麼情況,可以在程式代碼中的某一特定行或函數中設定斷點,這樣 gdb 會在遇到斷點時中斷執行。可以使用指令 'break main' 在進入 main() 函數時設定斷點,或者可以指定其它任何感興趣的函數名來設定斷點。然而,我們隻希望在調用 wib() 函數之前中斷執行。輸入 'list main' 将列印從 main() 函數開始的源碼清單,再次按回車将顯示第 21 行上的 wib() 函數調用。要在那一行上設定斷點,隻需輸入 'break 21'。gdb 将發出以下響應:

(gdb) break 21
Breakpoint 1 at 0x8048428: file eg1.c, line 21.
      

以顯示它已在我們請求的行上設定了 1 号斷點。'run' 指令将從頭重新運作程式,直到 gdb 中斷為止。發生這種情況時,gdb 會生成一條消息,指出它在哪個斷點上中斷,以及程式運作到何處:

Breakpoint 1, main (argc=1, argv=0xbffff954) at eg1.c:21
21          result = wib(value, div);
      

發出 'print value' 和 'print div' 将會顯示在第一次調用 wib() 時,變量分别等于 10 和 6,而 'print i' 将會顯示 0。幸好,gdb 将顯示所有局部變量的值,并使用 'info locals' 指令儲存大量輸入資訊。

從以上的調查中可以看出,當 "value" 和 "div" 相等時就會出現問題,是以輸入 'continue' 繼續執行,直到下一次遇到 1 号斷點。對于這次疊代,'info locals' 顯示了 value=9 和 div=7。

與其再次繼續,還不如使用 'next' 指令單步調試程式,以檢視 "value" 和 "div" 是如何改變的。gdb 将響應:

(gdb) next
22          total += result;
      

再按兩次回車将顯示加法和減法表達式:

(gdb)
23          div++;
(gdb)
24          value--;
      

再按兩次回車将顯示第 21 行,wib() 調用。'info locals' 将顯示目前 "div" 等于 "value",這就意味着将發生問題。如果有興趣,可以使用 'step' 指令(與 'next' 形成對比,'next' 将跳過函數調用)來繼續執行 wib() 函數,以再次檢視除法錯誤,然後使用 'next' 來計算 "result"。

現在已完成了調試,可以使用 'quit' 指令退出 gdb。由于程式仍在運作,這個操作會終止它,gdb 将提示您确認。

更多斷點和觀察點

由于我們想要知道在調用 wib() 函數之前 "value" 什麼時候等于 "div",是以在上一示例中我們在第 21 行中設定斷點。我們必須繼續執行兩次程式才會發生這種情況,但是隻要在斷點上設定一個條件就可以使 gdb 隻在 "value" 與 "div" 真正相等時暫停。要設定條件,可以在定義斷點時指定 "break <line number> if <conditional expression>"。将 eg1 再次裝入 gdb,并輸入:

(gdb) break 21 if value==div
Breakpoint 1 at 0x8048428: file eg1.c, line 21.
      

如果已經在第 21 行中設定了斷點,如 1 号斷點,則可以使用 'condition' 指令來代替在斷點上設定條件:

(gdb) condition 1 value==div
      

使用 'run' 運作 eg1.c 時,如果 "value" 等于 "div",gdb 将中斷,進而避免了在它們相等之前必須手工執行 'continue'。調試 C 程式時,斷點條件可以是任何有效的 C 表達式,一定要是程式所使用語言的任意有效表達式。條件中指定的變量必須在設定了斷點的行中,否則表達式就沒有什麼意義!

使用 'condition' 指令時,如果指定斷點編号但又不指定表達式,可以将斷點設定成無條件斷點,例如,'condition 1' 就将 1 号斷點設定成無條件斷點。

要檢視目前定義了什麼斷點及其條件,請發出指令 'info break':

(gdb) info break
Num Type           Disp Enb Address    What
1   breakpoint     keep y   0x08048428 in main at eg1.c:21
        stop only if value == div
        breakpoint already hit 1 time
      

除了所有條件和已經遇到斷點多少次之外,斷點資訊還在 'Enb' 列中指定了是否啟用該斷點。可以使用指令 'disable <breakpoint number>'、'enable <breakpoint number>' 或 'delete <breakpoint number>' 來禁用、啟用和徹底删除斷點,例如 'disable 1' 将阻止在 1 号斷點處中斷。

如果我們對 "value" 什麼時候變得與 "div" 相等更感興趣,那麼可以使用另一種斷點,稱作監視。當指定表達式的值改變時,監視點将中斷程式執行,但必須在表達式中所使用的變量在作用域中時設定監視點。要擷取作用域中的 "value" 和 "div",可以在 main 函數上設定斷點,然後運作程式,當遇到 main() 斷點時設定監視點。重新啟動 gdb,并裝入 eg1,然後輸入:

(gdb) break main
Breakpoint 1 at 0x8048402: file eg1.c, line 15.
(gdb) run
...
Breakpoint 1, main (argc=1, argv=0xbffff954) at eg1.c:15
15        value = 10;
      

要了解 "div" 何時更改,可以使用 'watch div',但由于要在 "div" 等于 "value" 時中斷,那麼應輸入:

(gdb) watch div==value
Hardware watchpoint 2: div == value
      

如果繼續執行,那麼當表達式 "div==value" 的值從 0(假)變成 1(真)時,gdb 将中斷:

(gdb) continue
Continuing.
Hardware watchpoint 2: div == value
Old value = 0
New value = 1
main (argc=1, argv=0xbffff954) at eg1.c:19
19        for(i = 0; i < 10; i++)
      

'info locals' 指令将驗證 "value" 是否确實等于 "div"(再次聲明,是 8)。

'info watch' 指令将列出已定義的監視點和斷點(此指令等價于 'info break'),而且可以使用與斷點相同的文法來啟用、禁用和删除監視點。

core 檔案

在 gdb 下運作程式可以使俘獲錯誤變得更容易,但在調試器外運作的程式通常會中止而隻留下一個 core 檔案。gdb 可以裝入 core 檔案,并讓您檢查程式中止之前的狀态。

在 gdb 外運作示例程式 eg1 将會導緻核心資訊轉儲:

$ ./eg1
Floating point exception (core dumped)      

要使用 core 檔案啟動 gdb,在 shell 中發出指令 'gdb eg1 core' 或 'gdb eg1 -c core'。gdb 将裝入 core 檔案,eg1 的程式清單,顯示程式是如何終止的,并顯示非常類似于我們剛才在 gdb 下運作程式時看到的消息:

...
Core was generated by `./eg1'.
Program terminated with signal 8, Floating point exception.
...
#0  0x80483ea in wib (no1=8, no2=8) at eg1.c:7
7         result = no1 / diff;
      

此時,可以發出 'info locals'、'print'、'info args' 和 'list' 指令來檢視引起除數為零的值。'info variables' 指令将列印出所有程式變量的值,但這要進行很長時間,因為 gdb 将列印 C 庫和程式代碼中的變量。為了更容易地查明在調用 wib() 的函數中發生了什麼情況,可以使用 gdb 的堆棧指令。

堆棧跟蹤

程式“調用堆棧”是目前函數之前的所有已調用函數的清單(包括目前函數)。每個函數及其變量都被配置設定了一個“幀”,最近調用的函數在 0 号幀中(“底部”幀)。要列印堆棧,發出指令 'bt'('backtrace' [回溯] 的縮寫):

(gdb) bt
#0  0x80483ea in wib (no1=8, no2=8) at eg1.c:7
#1  0x8048435 in main (argc=1, argv=0xbffff9c4) at eg1.c:21
      

此結果顯示了在 main() 的第 21 行中調用了函數 wib()(隻要使用 'list 21' 就能證明這一點),而且 wib() 在 0 号幀中,main() 在 1 号幀中。由于 wib() 在 0 号幀中,那麼它就是執行程式時發生算術錯誤的函數。

實際上,發出 'info locals' 指令時,gdb 會列印出目前幀中的局部變量,預設情況下,這個幀中的函數就是被中斷的函數(0 号幀)。可以使用指令 'frame' 列印目前幀。要檢視 main 函數(在 1 号幀中)中的變量,可以發出 'frame 1' 切換到 1 号幀,然後發出 'info locals' 指令:

(gdb) frame 1
#1  0x8048435 in main (argc=1, argv=0xbffff9c4) at eg1.c:21
21          result = wib(value, div);
(gdb) info locals
value = 8
div = 8
result = 4
i = 2
total = 6
      

此資訊顯示了在第三次執行 "for" 循環時(i 等于 2)發生了錯誤,此時 "value" 等于 "div"。

可以通過如上所示在 'frame' 指令中明确指定号碼,或者使用 'up' 指令在堆棧中上移以及 'down' 指令在堆棧中下移來切換幀。要擷取有關幀的進一步資訊,如它的位址和程式語言,可以使用指令 'info frame'。

gdb 堆棧指令可以在程式執行期間使用,也可以在 core 檔案中使用,是以對于複雜的程式,可以在程式運作時跟蹤它是如何轉到函數的。

連接配接到其它程序

除了調試 core 檔案或程式之外,gdb 還可以連接配接到已經運作的程序(它的程式已經過編譯,并加入了調試資訊),并中斷該程序。隻需用希望 gdb 連接配接的程序辨別替換 core 檔案名就可以執行此操作。以下是一個執行循環并睡眠的 示例程式:

eg2 示例代碼
#include 
int main(int argc, char *argv[])
{
  int i;
  for(i = 0; i < 60; i++)
  {
    sleep(1);
  }
  return 0;
}
      

使用 'gcc -g eg2.c -o eg2' 編譯該程式并使用 './eg2 &' 運作該程式。請留意在啟動該程式時在背景上列印的程序辨別,在本例中是 1283:

./eg2 &
[3] 1283
      

啟動 gdb 并指定程序辨別,在我舉的這個例子中是 'gdb eg2 1283'。gdb 會查找一個叫作 "1283" 的 core 檔案。如果沒有找到,那麼隻要程序 1283 正在運作(在本例中可能在 sleep() 中),gdb 就會連接配接并中斷該程序:

...
/home/seager/gdb/1283: No such file or directory.
Attaching to program: /home/seager/gdb/eg2, Pid 1283
...
0x400a87f1 in __libc_nanosleep () from /lib/libc.so.6
(gdb)
      

此時,可以發出所有常用 gdb 指令。可以使用 'backtrace' 來檢視目前位置與 main() 的相對關系,以及 mian() 的幀号是什麼,然後切換到 main() 所在的幀,檢視已經在 "for" 循環中運作了多少次:

(gdb) backtrace
#0  0x400a87f1 in __libc_nanosleep () from /lib/libc.so.6
#1  0x400a877d in __sleep (seconds=1) at ../sysdeps/unix/sysv/linux/sleep.c:78
#2  0x80483ef in main (argc=1, argv=0xbffff9c4) at eg2.c:7
(gdb) frame 2
#2  0x80483ef in main (argc=1, argv=0xbffff9c4) at eg2.c:7
7           sleep(1);
(gdb) print i
$1 = 50
      

如果已經完成了對程式的修改,可以 'detach' 指令繼續執行程式,或者 'kill' 指令殺死程序。還可以首先使用 'file eg2' 裝入檔案,然後發出 'attach 1283' 指令連接配接到程序辨別 1283 下的 eg2。

其它小技巧

gdb 可以讓您通過使用 shell 指令在不退出調試環境的情況下運作 shell 指令,調用形式是 'shell [commandline]',這有助于在調試時更改源代碼。

最後,在程式運作時,可以使用 'set ' 指令修改變量的值。在 gdb 下再次運作 eg1,使用指令 'break 7 if diff==0' 在第 7 行(将在此處計算結果)設定條件斷點,然後運作程式。當 gdb 中斷執行時,可以将 "diff" 設定成非零值,使程式繼續運作直至結束:

Breakpoint 1, wib (no1=8, no2=8) at eg1.c:7
7         result = no1 / diff;
(gdb) print diff
$1 = 0
(gdb) set diff=1
(gdb) continue
Continuing.
0 wibed by 16 equals 10
Program exited normally.
      

繼續閱讀