iT邦幫忙

1

C語言工具使用,GDB個人學習筆記

gdb

簡介

除錯器(debugger),可以在一個精準受控的環境下執行另一個程式。例如: 單步執行程式,跟蹤程式,查看變數內容,記憶體地址,以及程式中每一條指令指行完畢後CPU暫存器的變化情況,檢視程式呼叫堆疊等等。

gdb,全名為gnu debugger,是在GNU軟體系統中的標準除錯器,介面為互動式的shell,許多類Unix,如:FreeBSD, Linux等作業系統中都能夠使用,支援許多語言,包括C, C++等。

詳細的gdb使用手冊,可以在shell中輸入info gdb查閱。

示範程式

以下為範例程式gdb_example.c

#include <stdio.h>

void swap(int *, int*);

int main(void)
{
    int a = 10;
    int b = 20;
    printf("The old values: a = %d, b = %d.\n", a, b);
    swap(&a, &b);
    printf("The new values: a = %d, b = %d.\n", a, b);
    return 0;
}

void swap(int *p1, int *p2)
{
    int *p = p1;
    p1 = p2;
    p2 = p;
}

符號式除錯器(symbolic debugger)

gdb是一種符號式除錯器(symbolic debugger),所謂的符號式(symbolic),意思是在程式執行的時候,可以使用在源程式中的變數名稱或是函式名稱。

為了顯示和翻譯這一些名稱,除錯器需要程式中的變數型別,函式型別等資訊,以及可執行檔中哪一條指令對應到源程式中哪一行code的訊息,而這一些訊息以符號表(symbol table)的形式出現,當使用gcc加上選項'g'進行編譯和鏈結時,會產生出符號表,這個符號表會被嵌入到可執行檔中。

因此,當我們需要對一個程式使用gdb進行debug,需要在gcc加上'g'的原因,是因為gdb需要可執行檔中的符號表作為變數和函式的判斷依據從而進行操作。

在一個可執行檔中嵌入符號表,並不會對執行檔效能產生影響,只會影響執行檔的檔案大小。

進行除錯

首先,先使用gcc產生出可執行檔,並在可執行檔中嵌入符號表

$ gcc -g gdb_example.c -o gdb_example

接下來我們執行gdb_example.out,會得到以下結果

The old values: a = 10, b = 20.
The new values: a = 10, b = 20.

顯然這不是我們想要的結果,我們呼叫了swap函式,但a和b這兩個變數卻沒有發生交換,我們可以嘗試使用gdb來找尋原因,使用以下指令

$ gdb ./gdb_example

得到以下畫面

最下方的Reading symbols from ./gdb_example表示gdb成功讀取到執行檔內的符號表,gdb可以使用。

上方畫面表示除錯器已經成功執行gdb_example,但在執行gdb_example之前,會等待使用者輸入指令。

gdb在執行每一行指令之前,都會輸出(gdb),以提醒使用者輸入除錯指令,輸入list(或是簡寫成l)可以看到執行檔的前10行程式碼,在輸入一次list可看到後續10行的程式碼

在開始執行程式之前,我們可以設置中斷點(breakpoint),讓程式在某一個地方中斷執行,當除錯器遇到中斷點時,除錯器會中斷正在進行的程式,提供一個檢查目前程式狀態的機會,檢視完畢後,我們可以繼續逐步進行程式,一行一行觀察程式的狀態,或是我們想要追蹤的變數狀態。

如果要設置中斷點,輸入break指令,或是簡寫成b即可達成目的,例如下方的示範,表示在gdb_example.c的第15行設置中斷點

(gdb) b 15

之後我們可以輸入run(或是簡寫成r),開始執行程式


遇見中斷點時,程式會中斷,並等待使用者輸入指令。

我們懷疑swap()函式中可能存在錯誤,使我們無法成功交換兩變數的數值,因此我們希望可以一步步執行swap函式,而實現這個目的,gdb提供了next和step這兩個指令供我們使用;

兩者的差別為next會執行整行程式碼,包含所有的函數呼叫,然後在下一行繼續中斷目前程式的執行

而step則是根據符號表來決定是否進入函式,如果有效,則會呼叫函式,並在函式的第1行的地方中斷程式的執行

我們目前已經進入了函式,並且得到了p1和p2的記憶體地址,分別為
p1 = 0x55555555526d, p2 = 0x7fffffffdf96,輸入s後來到第17行,並執行int * p = p1,這時候我們可以檢查變數的值是否正確,我們可以使用print來檢視變數內容

我們可以確定p1和p2中的數值是正確的,接著我們可以繼續執行程式,使用n(next),除錯器會分別執行18, 19, 20這三行程式碼,此時我們還在swap函式中,尚未離開函式

gdb會顯示目前執行到的行數,以及程式碼的內容,此時我們可以使用print檢視p1和p2的內容

可以看到p1和p2的數值確實發生了交換,但,這時候我們需要確認交換的是不是傳入的a和b,前面我們已經知道了a和b的記憶體地址,這裡我們可以試著查看p1和p2的記憶體地址確認這一件事情

這裡我們發現到了一件事情,理想上經過交換之後,p1應該是代表b,p2應該是要代表a,也因此p1的記憶體地址應該為0x7fffffffdf96,p2應該為0x55555555526d,因此我們確定了一件事情,p1指標和p2指標確實交換了數值,但不是由* p1和* p2記憶體地址去進行交換的,而這就是swap()函式所發生的錯誤,是要交換* p1和* p2所指向的整數,而不是儲存p1和p2變數的記憶體地址。我們可以使用quit指令,結束gdb,接著去修改程式。

修改後的swap如下

void swap(int *p1, int *p2)
{
    int tmp = *p1;
    *p1 = *p2;
    *p2 = temp;
}

執行結果如下:

p.s: 我們在啟動gdb時,會有許多的版權宣告或是其他無關的訊息等等,我們可以透過以下參數讓gdb不顯示這一些訊息並啟動

$ gdb -silent

啟動之後我們可以再利用file指令去啟動我們想要進行除錯的程式

指令選項

gdb的指令選項大多數可以分為長選項和短選項這兩種格式。下面列出幾個常用的選項。
長選項,例如"-tty device",這個選項需要額外的參數才能夠運作,我們可以使用空格或是'='進行分割,例如'-tty=/dev/tty6',選項前可以加上連字符號'-',例如'-quiet'和'--quiet',這兩者的功能是相同的。

舉例:

  1. --version, -v 顯示gdb的版本和版權宣告,然後退出gdb
  2. --quiet, --silent 在不顯示版本和版權訊息的情況下啟動gdb
  3. --help, -h 顯示gdb的指令語法,然後退出

gdb指令

  1. help ( h ):顯示指令簡短說明。例:help breakpoint
  2. file:開啟檔案。等同於 gdb filename
  3. run ( r ):執行程式,或是從頭再執行程式。
  4. kill:中止程式的執行。
  5. backtrace ( bt ):顯示程式呼叫的堆疊(stack)。。會顯示出上層所有的 frame 的簡略資訊。
  6. print ( p ):印出變數內容。例:print i,印出變數 i 的內容。
  7. list ( l ):印出程式碼。若在編譯時沒有加上 -g 參數,list 指令將無作用。
  8. whatis:印出變數的型態。例: whatis i,印出變數 i 的型態。
  9. breakpoint (b, bre, break):設定中斷點
    使用 info breakpoint (info b) 來查看已設定了哪些中斷點。
    在程式被中斷後,使用 info line 來查看正停在哪一行。
  10. continue (c, cont):繼續執行。和 breakpoint 搭配使用。
  11. frame:顯示正在執行的行數、副程式名稱、及其所傳送的參數等等 frame 資訊。
     frame 2:看到 #2,也就是上上一層的 frame 的資訊。
  12. next ( n ):單步執行,但遇到函式時會將呼叫的函式作為一個語句執行。
  13. step ( s ):單步執行。但遇到函式時會進入呼叫的函式執行。
  14. up:直接回到上一層的 frame,並顯示其 stack 資訊,如進入點及傳入的參數等。
  15. up 2:直接回到上三層的 frame,並顯示其 stack 資訊。
  16. down:直接跳到下一層的 frame,並顯示其 stack 資訊。
     必須使用 up 回到上層的 frame 後,才能用 down 回到該層來。
  17. info:顯示一些特定的資訊。如: info break,顯示中斷點,
    info share,顯示共享函式庫資訊。
  18. disable:暫時關閉某個 breakpoint 或 display 之功能。
  19. enable:將被 disable 暫時關閉的功能再啟用。
  20. clear/delete:刪除某個 breakpoint。
  21. attach PID:載入已執行中的程式以進行除錯。其中的 PID 可由 ps 指令取得。
  22. detach PID:釋放已 attach 的程式。
  23. shell:執行 Shell 指令。如:shell ls,呼叫 sh 以執行 ls 指令。
  24. quit:離開 gdb。或是按下 Ctrl+C 也行。
  25. 按下Enter:直接執行上個指令

比較step和next,與backtrace的使用

範例程式

#include <stdio.h>

int func();
int main(void)
{
        printf("Hello World");
}

int func()
{
        printf("in func");
}
  • 使用next進行逐步執行結果

    • 我們使用bt去查看next的呼叫結果
  • 使用step進行逐步執行結果

    • 我們使用bt去追蹤step的呼叫結果

我們發現使用step或是next最終的輸出皆是Hello Worldin func,兩者的差別即是在函式中,如果碰到了printf這種函式,step會直接輸出printf的結果,以這個範例為例,如果是在main函式中碰到printf函式,便會直接印出Hello World。

但如果是step,則會進入到printf函式,並去追蹤printf的呼叫,我們使用bt這個指令發現到這一個現象,step去追蹤printf,發現printf呼叫了_vfprintf_internal,繼續使用step,接著追蹤,發現_vfprintf_internal呼叫了_IO_new_file_xsputn,不斷的追蹤下去

我們在上方有使用到backtrace(bt)指令,去追蹤程式呼叫的堆疊,我們也可以使用frame這個指令去檢視目前呼叫的堆疊,frame後方加上數字表示顯示第幾層堆疊的資訊

info這個指令可以顯示出任何我們想要的資訊,例如輸入info breakpoint可以顯示我們所下的所有中斷點

檢查點(watchpoint)

在gdb中,可以透過設定檢查點(watchpoint)來監測變數的讀寫操作,以及變化。檢查點類似中斷點,但差別在於檢查點並沒有綁定源程式碼中某一行程式碼,如果針對某一個變數設置檢查點,當該變數發生變化時,gdb就會中斷程式,和中斷點的差別為不會在是碰到某一行被中斷點標記的程式碼停下,而是標記的變數發生變化時。

檢查點不僅可以用來觀察變數,也可以用來觀察表達式中的變數值。可以使用watch, rwatch, awatch指令來設定不同的檢查點

  1. watch expression 當expression的值發生改變時,程式中斷進行
  2. rwatch expression 當程式讀取與計算與expression相關的任何對象時,程式中斷進行
  3. awatch expression 當程式讀取或修改與計算expression相關的任何對象時,程式中段進行

檢查點最常見的用途為檢查程式'何時'修改某一個變數,當一個變數改變時,gdb會顯示被檢查的變數舊的數值和新的數值,以及下一行要執行的程式。以下方的程式作為範例

#include <stdio.h>

int main(void)
{
        int a = 10;
        int b = 20;
        int *iPtr = &a;

        ++*iPtr;
        puts("This is the statement following ++*iPtr.");

        printf("a = %d; bb = %d.\n", a, b);
        return 0;
}

我們先將中斷點設置在第9行,之後開始執行程式

我們對變數a設置檢查點,指令為watch a

之後使用continue繼續執行程式,直到a發生了改變,程式中斷

因為第9行的iPtr指向變數a,當第9行執行++*iPtr時,a發生了變化,因此程式中斷,並顯示下一行準備被執行的程式碼。

我們繼續執行這一個例子,我們為b設定一個讀取檢查點,指令為rwatch b,這個檢查點會被包含在中斷點列表中,使用info breakpoints指令可以確認是否在其中

我們可以發現目前有三個中斷點,分別為在源程式碼第9行的中斷點,變數a的檢查點,變數b的檢查點,接著繼續執行程式

發現在程式碼第12行的地方會對變數b進行讀取,因此程式進行中斷,接著繼續執行

當程式離開一個程式區塊(被大括號包住的區塊),該區域內變數的檢查點會被自動刪除

繼續上方這個範例程式碼,我們可以看看對整個表達式設置檢查點會發生甚麼事情,
指令為rwatch a + b

我們讓程式繼續進行,會發現每一次只要a+b的值發生讀取或是計算,程式便會中斷



在第9行嘗試讀取a的值,中斷一次,改變a的值,再中斷一次
第12行a+b被讀取了兩次,分別先讀取a,再讀取b

總共中斷4次

核心檔案(core file)

核心檔案(core file),也被稱作為核心轉存檔案(core dump),當程式在執行的過程中發生了異常的中止或是非法存取記憶體,作業系統會將當時的記憶體狀態記錄下來,這個狀態包括暫存器狀態,程式堆疊等等,然後把這一些資訊儲存成一個檔案,而這個檔案被稱為核心轉存檔案(core dump),這個檔案可以有效的協助我們對程式進行除錯。

舉例來說,如果我們非法存取記憶體時,程式會回報Segmentation fault這行字,但這樣的資訊是無法幫助我們除錯的,我們必須知道是哪一行程式碼觸發了這個錯誤,因此,我們必須使用core file來除錯。

通過gdb來分析核心檔案時,與一般的除錯不同,因為程式已經被中斷,我們無法通過bt, next, step這些指令來除錯,假設我們執行一個已經經過'-g'選項編譯過的檔案,執行core_dump這個程式
以下為core_dump.c

#include <stdio.h>

int main(void)
{
    int *b;
    
    
    scanf("%d", b);
    return 0;
}

使用為初始化的指標,造成Segmentation fault,我們執行core_dump的執行檔看看

./core_dump


產生了錯誤,此時在該執行檔所在的資料夾會產生一個名稱為core的檔案,這個檔案即為剛剛發生Segmentation fault所產生的,也就是核心轉存檔案,我們使用gdb來檢視他

gdb ./core_dump core

執行畫面如下

裏頭告訴我們Core是由執行core_dump這個執行檔所產生,下面則有一些堆疊呼叫的資訊,我們得知在呼叫_vfscanf_internal時發生了錯誤,但不知道是由哪一個函式呼叫了__vfscanf_internal,我們可以使用bt來檢視這個程式的函式堆疊

我們可以看到在程式第8行呼叫了__isoc99_scanf這個函式,而這個函式在第30行的地方呼叫了__vfscanf_internal這個函式,結果產生了Segmentation fault,__isoc99_scanf和vfscanf_internal皆為C的標準函式庫,因此發生錯誤的地方是在程式碼第8行scanf的地方,因為存取為初始化的指標而發生的錯誤,我們可以使用print b把b給印出來便會發現。

有了core file這種除錯技巧,當我們遇到Segmentation fault這種狀況時,便可以得到有用的資訊並進行除錯了。

p.s 如果程式進行完畢後沒有產生core file,我們需要執行以下指令

ulimit -c unlimited

讓系統產生出core file。

新手發文,請多指教~

參考資料

https://b8807053.pixnet.net/blog/post/336154079-%5B%E8%BD%89%E8%B2%BC%5Dgdb-%E4%BB%8B%E7%B4%B9
https://www.cnblogs.com/J1ac/p/9113669.html
https://www.cyut.edu.tw/~ckhung/b/c/gdb.php
C語言核心技術第二版


尚未有邦友留言

立即登入留言