除錯器(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;
}
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',這兩者的功能是相同的。
舉例:
help ( h )
:顯示指令簡短說明。例:help breakpointfile
:開啟檔案。等同於 gdb filenamerun
( r ):執行程式,或是從頭再執行程式。kill
:中止程式的執行。backtrace ( bt )
:顯示程式呼叫的堆疊(stack)。。會顯示出上層所有的 frame 的簡略資訊。print ( p )
:印出變數內容。例:print i,印出變數 i 的內容。list ( l )
:印出程式碼。若在編譯時沒有加上 -g 參數,list 指令將無作用。whatis
:印出變數的型態。例: whatis i,印出變數 i 的型態。breakpoint (b, bre, break)
:設定中斷點continue (c, cont)
:繼續執行。和 breakpoint 搭配使用。frame
:顯示正在執行的行數、副程式名稱、及其所傳送的參數等等 frame 資訊。next ( n )
:單步執行,但遇到函式時會將呼叫的函式作為一個語句執行。step ( s )
:單步執行。但遇到函式時會進入呼叫的函式執行。up
:直接回到上一層的 frame,並顯示其 stack 資訊,如進入點及傳入的參數等。up 2
:直接回到上三層的 frame,並顯示其 stack 資訊。down
:直接跳到下一層的 frame,並顯示其 stack 資訊。info
:顯示一些特定的資訊。如: info break,顯示中斷點,disable
:暫時關閉某個 breakpoint 或 display 之功能。enable
:將被 disable 暫時關閉的功能再啟用。clear/delete
:刪除某個 breakpoint。attach PID
:載入已執行中的程式以進行除錯。其中的 PID 可由 ps 指令取得。detach PID
:釋放已 attach 的程式。shell
:執行 Shell 指令。如:shell ls,呼叫 sh 以執行 ls 指令。quit
:離開 gdb。或是按下 Ctrl+C 也行。Enter
:直接執行上個指令範例程式
#include <stdio.h>
int func();
int main(void)
{
printf("Hello World");
}
int func()
{
printf("in func");
}
使用next進行逐步執行結果
使用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可以顯示我們所下的所有中斷點
在gdb中,可以透過設定檢查點(watchpoint)來監測變數的讀寫操作,以及變化。檢查點類似中斷點,但差別在於檢查點並沒有綁定源程式碼中某一行程式碼,如果針對某一個變數設置檢查點,當該變數發生變化時,gdb就會中斷程式,和中斷點的差別為不會在是碰到某一行被中斷點標記的程式碼停下,而是標記的變數發生變化時。
檢查點不僅可以用來觀察變數,也可以用來觀察表達式中的變數值。可以使用watch, rwatch, awatch指令來設定不同的檢查點
檢查點最常見的用途為檢查程式'何時'修改某一個變數,當一個變數改變時,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 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語言核心技術第二版