GDB調試技巧:多線程案例分析(保姆級)
在軟件開發(fā)的復雜世界里,高效的調試工具是解決問題的關鍵利器。今天,我們將深入探討強大的調試工具 ——GDB(GNU Debugger)。GDB 為開發(fā)者提供了一種深入程序內部運行機制、查找錯誤和優(yōu)化性能的有效途徑。讓我們一同開啟 GDB 的調試之旅,解鎖代碼中的奧秘。
一、GDB調試工具
GDB(GNU Debugger)是強大的調試工具,在軟件開發(fā)過程中起著至關重要的作用。它可以幫助開發(fā)者快速定位和解決程序中的問題。
GDB做以下4 件主要的事情來幫助您捕獲程序中的bug:
- 在程序啟動之前指定一些可以影響程序行為的變量或條件
- 在某個指定的地方或條件下暫停程序
- 在程序停止時檢查已經發(fā)生了什么
- 在程序執(zhí)行過程中修改程序中的變量或條件,這樣就可以體驗修復一個 bug 的成果,并繼續(xù)了解其他 bug
啟動 GDB 主要有以下兩種方法:
⑴直接啟動
- gdb:單獨輸入此命令啟動 GDB,啟動后需借助file或者exec-file命令指定要調試的程序。
- gdb test.out:如果有一個名為test.out的可執(zhí)行文件,可以直接使用這個命令啟動 GDB 并加載該程序進行調試。
- gdb test.out core:當程序發(fā)生錯誤并生成core文件時,可以使用這個命令啟動 GDB,以便對錯誤進行分析。
⑵動態(tài)鏈接:gdb test.out pid,這種方式可以將 GDB 鏈接到一個正在運行中的進程中去,其中pid就是進程號,可以使用ps aux命令查看對應程序的進程號。
要準備調試的程序,首先需要用gcc的-g參數生成可執(zhí)行文件。這樣才能在可執(zhí)行文件中加入源代碼信息以便調試,但這并不是將源文件嵌入到可執(zhí)行文件中,所以調試時必須保證 GDB 能找到源文件。例如,編譯程序時可以使用gcc -g main.c -o test.out這樣的命令來生成帶有調試信息的可執(zhí)行文件。
二、GDB調試技巧
⑴條件斷點
條件斷點在調試過程中非常實用。設置條件斷點可以利用break if命令,例如(gdb)break 666 if testsize==100123123。條件斷點的優(yōu)勢在于可以在特定條件滿足時才使程序停止,這對于排查異常情況非常有幫助。比如在一個循環(huán)中,當某個變量達到特定值時才中斷程序,這樣可以更精準地定位問題。
⑵斷點命令
斷點命令不僅可以讓程序在特定位置停止,還可以編寫對到達斷點響應的腳本,實現更復雜的調試功能。例如,可以在斷點處設置一些打印變量值、檢查特定條件等操作,以更好地了解程序的運行狀態(tài)。
⑶轉儲二進制內存
GDB 提供了多種方式查看內存。內置支持的x命令可以查看內存地址中的值,其語法為x/<n/f/u> <addr>,其中n是顯示內存的長度,f表示顯示的格式,u表示從當前地址往后請求的字節(jié)數。例如(gdb) x/16xw 0x7FFFFFFFE0F8可以以十六進制、四字節(jié)為單位顯示從地址0x7FFFFFFFE0F8開始的 16 個單位的內存內容。此外,也可以使用自定義的hexdump命令來查看內存,更加靈活地控制輸出格式。
⑷行內反匯編
使用disassemble/s命令可以查看與函數源代碼對應的指令,這有助于了解程序在 CPU 指令級別上的情況。例如,disas main可以顯示main函數對應的匯編代碼。通過查看匯編代碼,可以更深入地理解程序的執(zhí)行過程,對于分析性能問題、理解底層實現等非常有幫助。
⑸反向調試
反向調試是 GDB 的一個強大功能。它可以讓程序實現上一步上一步的操作,即反向運行。反向調試在一些情況下非常有用,比如調試過程中不小心多執(zhí)行了一次命令,或者想再次查看剛剛程序執(zhí)行的過程。反向調試不適用 IO 操作,并且需要 GDB7.0 以上的版本。相關指令有rc或reverse-continue反向運行程序,直到碰到一個能使程序中斷的事件;rs或reverse-step反向運行程序到上一次被執(zhí)行的源代碼行等。通過查看寄存器值等方式,可以深入了解程序在反向運行過程中的狀態(tài)變化。
三、GDB調試方法
⑴編譯及啟動調試
在編譯代碼時,加上 -g 選項是非常重要的,這可以確保在可執(zhí)行文件中包含調試信息,以便在使用 GDB 進行調試時能夠獲取更多的程序內部狀態(tài)信息。例如,使用 gcc -g main.c -o main.out 這樣的命令編譯代碼,生成的 main.out 可執(zhí)行文件就可以被 GDB 有效地調試。
啟動調試代碼有多種方式。可以直接使用 gdb main.out 來啟動調試一個可執(zhí)行文件,然后在 GDB 環(huán)境中使用 run 命令來運行程序。如果程序在啟動時需要命令行參數,可以在進入 GDB 后使用 run arg1 arg2... 的方式來提供參數并啟動調試。
另外,還可以調試正在運行的程序。首先找到程序的進程號,可以使用 ps aux | grep program_name 或 pidof program_name 來獲取進程號。然后使用 gdb attach pid 或者 gdb -p pid 命令將 GDB 附加到正在運行的程序上進行調試。
⑵調試命令
GDB 有許多強大的調試命令。比如 list 命令可以顯示源代碼,list 會打印當前行后面的代碼,list - 顯示當前行前面的代碼,list lineNumber 打印出行第 lineNumber 行前后的代碼,list FunctionName 打印出行函數 FunctionName 前后的代碼。
break 命令用于設置斷點,可以在指定的行號或函數處設置斷點,如 break <function> 在進入指定函數時停止運行,break <lineNumber> 在指定代碼行打斷點,break filename:lineNumber 在指定文件的特定行設置斷點,break filename:function 在指定文件的函數入口處設置斷點。還可以設置條件斷點,如 break... if <condition>,當條件成立時程序停止運行。
next 命令執(zhí)行下一條語句,如果該語句為函數調用,不會進入函數內部執(zhí)行。step 命令執(zhí)行下一條語句,如果該語句為函數調用,則進入函數執(zhí)行其中的第一條語句。continue 命令繼續(xù)程序的運行,直到遇到下一個斷點。
print 和 display 命令用于打印變量 / 表達式的值,print 只輸出一次,display 跟蹤查看某個變量,每次停下來都顯示它的值。可以以不同格式打印變量,如 p /f variable,其中 f 可以是 x(十六進制格式)、d(十進制格式)、u(十六進制格式顯示無符號整型)等。
watch 命令在程序運行過程中監(jiān)視變量值的變化,如果有變化,馬上停止程序運行,如 watch variable 當變量 variable 有變化時,停止程序運行,還有 rwatch 和 awatch 分別在變量被讀取和被讀或被寫時停止程序運行。
⑶調試段錯誤
調試段錯誤的一種快捷方法是生成 core 文件并使用 GDB 加載分析。首先,可以使用 ulimit -c unlimited 命令將 core 文件生成設置為不限制大小。這樣,當程序發(fā)生段錯誤時,會生成 core 文件。
然后,使用 GDB 加載這個 core 文件進行調試。可以使用 gdb program core 的方式,其中 program 是可執(zhí)行程序名稱,core 是生成的 core 文件。在 GDB 中,可以使用 backtrace 命令查看函數調用棧,找到出錯的位置。還可以使用 frame 命令查看特定棧幀的信息,使用 print 命令打印變量的值,以確定問題所在。例如,如果在調試過程中發(fā)現某個變量的值為空指針,可能是內存分配失敗導致的,可以進一步檢查相關的內存分配代碼。
四、GDB使用其他要點
4.1調試參數列表
GDB 擁有豐富的調試參數,以下是一些常見的命令及其用途:
啟動程序:使用 gdb [可執(zhí)行文件名] 啟動 GDB 并加載要被調試的可執(zhí)行文件。例如 gdb test.out。還可以使用 gdb file [可執(zhí)行文件名] 的方式啟動,如 gdb file test.out。另外,若要調試正在運行的程序,可以使用 gdb attach [進程號] 或 gdb -p [進程號]。
①設置斷點:
- break [行號]:在指定行設置斷點,如 break 10。
- break [函數名]:在函數入口處設置斷點,如 break main。
- break [文件名:行號]:在指定文件的特定行設置斷點,如 break test.c:20。
- break... if [條件]:設置條件斷點,當條件成立時程序停止運行,如 break 666 if testsize==100123123。
- info breakpoints:顯示當前程序的斷點設置情況。
- delete breakpoints [斷點號]:刪除指定斷點,不指定斷點號則刪除所有斷點。
- disable [斷點號]:暫停指定斷點。
- enable [斷點號]:開啟指定斷點。
- clear [行號]:清除指定行的斷點。
②單步執(zhí)行:
- next(簡寫為 n):逐過程調試,執(zhí)行下一行,當遇到函數調用時,會一次性執(zhí)行完該函數,不進入函數體內部。
- step(簡寫為 s):單步調試,執(zhí)行下一行,當遇到函數調用時,會進入函數體內部。
- continue(簡寫為 c):繼續(xù)執(zhí)行程序,直到下一個斷點處或程序結束。
- until:當厭倦在一個循環(huán)體內單步跟蹤時,這個命令可以運行程序直到退出循環(huán)體。until+行號:運行至某行,可用于跳出循環(huán)。
- finish:運行程序,直到當前函數完成返回,并打印函數返回時的堆棧地址和返回值及參數值等信息。
- call [函數(參數)]:調用程序中可見的函數,并傳遞參數,如 call gdb_test(55)。
③查看信息:
- info registers:顯示所有寄存器的內容,可查看特定寄存器,如 info registers rbp 顯示 rbp 寄存器的值,info registers rsp 顯示 rsp 寄存器的值。
- info stack:顯示堆棧信息。
- info args:顯示當前函數的參數列表。
- info locals:顯示當前函數的局部變量列表。
- info function:查詢函數。
- info breakpoints:顯示當前程序的斷點設置情況。
- info watchpoints:列出當前所設置的所有觀察點。
- info line [行號/函數名/文件名:行號/文件名:函數名]:查看源代碼在內存中的地址。
4.2查看內存單元值
在 GDB 中,可以使用 examine 命令(簡寫是 x)來查看內存地址中的值。其格式為 x/<n/f/u> <addr>,其中:n是一個正整數,表示顯示內存的長度,從當前地址向后顯示幾個地址的內容。例如 x/16xb 0x7FFFFFFFE0F8 表示以單字節(jié)為單位顯示從地址 0x7FFFFFFFE0F8 開始的 16 個字節(jié)的內容。
f表示顯示的格式,可取如下值:
- x:按十六進制格式顯示變量。
- d:按十進制格式顯示變量。
- u:按十進制格式顯示無符號整型。
- o:按八進制格式顯示變量。
- t:按二進制格式顯示變量。
- a:按十六進制格式顯示變量。
- i:指令地址格式。
- c:按字符格式顯示變量。
- f:按浮點數格式顯示變量。
u表示一個地址單元的長度,可用以下字符代替:
- b表示單字節(jié)。
- h表示雙字節(jié)。
- w表示四字節(jié)。
- g表示八字節(jié)。
4.3查看源程序
在 GDB 中,可以使用 list(簡寫為 l)命令查看源程序,有以下幾種方式:
- list:顯示當前行后面的源程序,默認每次顯示 10 行,按回車鍵繼續(xù)看余下的。
- list [行號]:將顯示當前文件以 “行號” 為中心的前后 10 行代碼,如 list 12。
- list [函數名]:將顯示 “函數名” 所在函數的源代碼。
4.4棧幀相關
GDB 中有一些與棧幀相關的命令:
- info frame:打印當前棧幀的詳細信息,包括當前函數、參數和局部變量等。例如:(gdb) info frame會顯示諸如 Stack level 0, frame at [地址]: pc = [程序計數器值] in [函數名] ([文件名]:[行號]); saved pc [保存的程序計數器值]等信息。
- up和down:在棧幀之間上下移動。up命令將切換到上一個棧幀,而down命令將切換到下一個棧幀。
- info locals:顯示當前函數的局部變量列表,幫助開發(fā)者了解當前棧幀中的局部變量情況。
五、GDB多線程調試
5.1GDB 多線程調試基礎
圖片
⑴基本命令介紹
在 GDB 多線程調試中,有許多常用命令。例如設置斷點可以使用 (gdb) break function_name,通過這個命令可以在特定的函數處設置斷點,當程序執(zhí)行到該函數時會暫停。刪除斷點則可以使用 (gdb) delete breakpoints。查看線程信息可以使用 (gdb) info threads,這個命令會列出所有可調試的線程信息,包括 GDB 分配的線程 ID、系統級的線程標識符以及線程的棧信息等。切換線程可以使用 (gdb) thread thread_id,通過指定線程 ID 可以快速切換到對應的線程進行調試。此外,設置監(jiān)視點可以使用 (gdb) watch variable_name,用于觀察某個變量的值是否有變化,一旦變化程序會立即暫停。刪除監(jiān)視點則是 (gdb) delete watchpoints。
⑵編譯多線程程序
在進行多線程調試之前,我們需要先編譯多線程程序。通常,我們可以使用 gcc 編譯器來編譯多線程程序。例如,對于以下多線程程序代碼:
#include <stdio.h>
#include <pthread.h>
#define NUM_THREADS 5
void * thread_func(void * thread_id) {
long tid = (long)thread_id;
printf("Hello World! It's me, thread #%ld!", tid);
pthread_exit(NULL);
}
int main() {
pthread_t threads[NUM_THREADS];
int rc;
long t;
for (t = 0; t < NUM_THREADS; t++) {
printf("In main: creating thread %ld", t);
rc = pthread_create(&threads[t], NULL, thread_func, (void *)t);
if (rc) {
printf("ERROR; return code from pthread_create() is %d", rc);
return -1;
}
}
pthread_exit(NULL);
}我們可以將上述代碼保存至一個名為 multithread.c 的文件中,并使用以下命令進行編譯:$ gcc -g -pthread -o multithread multithread.c。其中,-g 選項用于在可執(zhí)行文件中加入調試信息,這樣在使用 GDB 進行調試時可以獲取更多的程序信息;-pthread 選項則用于引入多線程庫,確保程序能夠正確地使用多線程功能。
5.2多線程調試案例分析
⑴簡單多線程程序調試
假設我們有一個如下的簡單多線程程序:
#include <stdio.h>
#include <pthread.h>
void *printNumbers(void *arg) {
int i;
for (i = 0; i < 10; i++) {
printf("Thread: %d\n", i);
}
return NULL;
}
int main() {
pthread_t thread1, thread2;
pthread_create(&thread1, NULL, printNumbers, NULL);
pthread_create(&thread2, NULL, printNumbers, NULL);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
return 0;
}我們可以使用以下步驟進行 GDB 調試:
- 首先,編譯程序:$ gcc -g -pthread -o simple_thread simple_thread.c。
- 然后啟動 GDB:$ gdb simple_thread。
- 在 main 函數處設置斷點:(gdb) break main。
- 運行程序:(gdb) run。程序會停在 main 函數的斷點處。
- 接著,我們可以使用 (gdb) info threads 查看當前的線程信息。可以看到有兩個線程正在運行,一個是主線程,一個是其中一個子線程。
- 使用 (gdb) thread thread_id 切換到子線程,然后進行單步執(zhí)行操作,如 (gdb) next。可以觀察到子線程的執(zhí)行過程。
⑵復雜多線程程序調試
對于更復雜的多線程程序,比如多個線程之間存在交互和同步問題的程序,調試會更加具有挑戰(zhàn)性。
例如,有一個多線程程序,多個線程同時對一個共享資源進行讀寫操作,可能會出現競爭條件和數據不一致的問題。
在這種情況下,我們可以使用 GDB 的以下技巧來處理:
- 使用 (gdb) break function_name 在關鍵的同步函數處設置斷點,如互斥鎖的加鎖和解鎖函數。
- 通過 (gdb) info threads 隨時查看線程狀態(tài),確定哪個線程正在持有共享資源,哪個線程在等待資源。
- 使用 (gdb) thread apply all bt 查看所有線程的調用堆棧,以了解每個線程的執(zhí)行路徑和當前狀態(tài)。
- 設置條件斷點,例如 (gdb) break function_name if condition,當特定條件滿足時才觸發(fā)斷點,以便在復雜的交互場景中更精確地定位問題。
例如,假設我們有一個多線程的銀行賬戶管理程序,多個線程同時進行存款和取款操作,我們可以在存款和取款函數處設置斷點,并根據賬戶余額等條件設置條件斷點,以便在出現異常情況時能夠快速定位問題所在。
5.3多線程調試技巧
⑴線程鎖定與并發(fā)控制
在 GDB 中,可以使用 set scheduler-locking 命令來控制線程的執(zhí)行順序和并發(fā)程度。這個命令有三個值,分別是 on、step 和 off。
- set scheduler-locking on:可以用來鎖定當前線程,只觀察這個線程的運行情況,鎖定這個線程時,其他線程處于暫停狀態(tài)。在當前線程執(zhí)行 next、step、until、finish、return 命令時,其他線程是不會運行的。需要注意的是,在使用這個選項時要確認當前線程是否是我們期望鎖定的線程,如果不是,則可以使用 thread + 線程編號 切換到我們需要的線程,再調用 set scheduler-locking on 鎖定。
- set scheduler-locking step:也用來鎖定當前線程,當且僅當使用 next 或 step 命令做單步調試時會鎖定當前線程,如果使用 until、finish、return 等線程內的調試命令(它們不是單步控制命令),則其他線程還是有機會運行的。與 on 選項的值相比,step 選項的值為單步調試提供了更加精細化的控制,因為在某些場景下,我們希望單步調試時其他線程不要對所屬的當前線程的變量值造成影響。
- set scheduler-locking off:用于釋放鎖定當前線程。
我們還可以使用 show scheduler-locking 命令來顯示線程的 scheduler-locking 狀態(tài)。
⑵命令組合與高效調試
一些常用的 GDB 命令組合可以提高多線程調試的效率。例如:
- info threads + thread thread_id + bt:先使用 info threads 查看當前進程的所有線程信息,然后使用 thread thread_id 切換到特定線程,再使用 bt 查看該線程的函數調用堆棧,以便分析該線程的執(zhí)行邏輯。
- break function_name + condition + run + next/step:先使用 break function_name if condition 在特定函數處設置條件斷點,然后使用 run 運行程序,當條件滿足時程序會停在斷點處,接著使用 next 或 step 進行單步調試。
- thread apply all command:可以讓所有被調試線程執(zhí)行特定的 GDB 命令,例如 thread apply all bt 可以查看所有線程的調用堆棧。
⑶常見問題與解決方案
在多線程調試過程中,可能會遇到以下常見問題:
線程死鎖:如果程序出現死鎖,可以使用 GDB 的以下步驟進行分析。首先,使用 gdb 啟動程序,然后在程序死鎖處按 ctrl+c 暫停程序。接著,使用 info threads 查看當前節(jié)點上線程狀態(tài),使用 thread thread_id 切換線程,使用 bt 查看線程堆棧,并查處死鎖位置。多切換幾個線程,全面分析死鎖的原因。一般來說,首先檢查使用頻率最高的鎖在所有函數出口上是否已解鎖。如果是第一輪出現死鎖,則可檢查鎖配對和可能的程序出口上是否進行了開鎖。如果多輪運行后出現,且基本確認函數出口均解鎖,則需要判斷是否是內存越界,可以使用工具 valgrind 進行內存越界診斷。
無法確定當前調試的線程:可以使用 info threads 命令查看當前可調試的所有線程,每個線程會有一個 GDB 為其分配的 ID,前面有 * 的是當前調試的線程。也可以使用 thread thread_id 切換到特定線程進行確認。
多線程程序調試效率低下:可以使用前面提到的命令組合和線程鎖定功能,有針對性地調試特定線程或在特定條件下進行調試,提高調試效率。同時,可以將程序中的線程數量減少至 1 進行調試,觀察是否正確,然后逐步增加線程數量,調試線程的同步是否正確。























