GDB:從單線程調試到多線程調試
1. 裸跑GDB
1.1 安裝GDB
sudo apt-get install gdb
1.2 編譯程序
由於需要調試,因此編譯的時候需要添加-g編譯參數:
1.3 GDB調試運行

1.4 常用調試參數
進入上面那個界面以后,說明正常啟動GDB了,目前只是GDB啟動了,程序還沒有跑起來,輸入run可以讓程序跑起來,但是這樣程序就直接執行結束了,沒有被逐行調試,沒什么意義,因此通常需要先打斷點,再啟動程序。下面先列出常用的命令:
| 命令 | 簡寫形式 | 說明 |
|---|---|---|
list |
l |
查看源碼(后面可以接數字,表示查看該行附近的源碼) |
next |
n |
(執行完該行后)跳到下一行,遇到函數直接執行完成 |
step |
s |
(執行完該行后)跳到下一行,遇到函數會進入 |
finish |
運行到函數結束 | |
continue |
c |
繼續運行(如果后面沒有斷點,則一直執行到函數結束) |
break |
b |
打斷點(d 行號,或d函數名) |
info breakpoints |
顯示斷點信息(比如目前有幾個斷點,是否被啟用等等) | |
delete |
d |
刪除斷點(delete 斷點編號:刪除某一斷點,delete:刪除所有斷點) |
print |
p |
打印變量值(p 代碼中變量名:打印代碼中變量的值) |
info locals |
打印當前所在函數局部變量的值 | |
run |
r |
啟動程序(設置完斷點后用該命令啟動程序) |
until |
u |
執行到指定行 |
info |
i |
顯示信息 |
help |
h |
顯示幫助信息(如:help list) |
quit |
q |
退出GDB |
!(shell命令) |
在GDB里執行shell命令,第4.2節會用到 | |
shell (shell命令) |
1.5 簡單示例
用GDB運行程序:
查看源碼:
打斷點后運行:
然后就可以配合n、s和p來一步步調試源碼了。
2. GDB增強實現CGDB
2.1 安裝
原生的GDB由於沒有獨立顯示代碼的窗口,調試比較麻煩,需要不斷l來顯示代碼,CGDB是GDB的加強版,下載鏈接為:http://cgdb.github.io/
按照教程安裝即可,注意configure的時候添加參數:
CXXFLAGS='-std=c++11' ./configure --prefix=/usr/local
因為新版CGDB是用C++11編寫的,不添加前面的參數可能會報錯,其他的按照官網步驟即可。
2.2 配置
使用規則和GDB基本一致,不同的是有兩個窗口,不過默認的是上下分屏,不太好看,這里先配置一下:[谷歌接口報錯]:
1.網絡錯誤或者文本過長。
2.谷歌接口可能對於某些網絡不能用,具體不清楚。可以嘗試掛VPN試試。
3.這個問題我沒辦法修復,請右鍵菜單更換百度、騰訊翻譯接口。
vim ~/.cgdb/cgdbrc
默認沒有這個文件,自己用vim填寫一下:
:set ignorecase
:set ts=4
:set wso=vertical
:set hls
map <F11> :until<cr>
代表的含義依次是:
大小寫不敏感;tab對應4個空格;分屏為左右垂直分屏;搜索高亮;F11快捷鍵,用於跳出循環(默認沒有這個快捷鍵)
2.3 使用
在命令行輸入
cgdb bugging
后進入如下界面:
左側為源碼,右側為CGDB調試界面,命令基本和GDB一致。
按鍵盤esc可以進入源碼界面,用vim的操作方式控制方向;按鍵盤i又重新回到gdb的調試界面。
2.4 快捷鍵
可以先按esc進入源碼界面,然后用快捷鍵控制單步運行,這樣操作方便很多。有如下常用快捷鍵:
- F5 - Send a run command to GDB.(相當於
r) - F6 - Send a continue command to GDB.(相當於
c) - F7 - Send a finish command to GDB.(相當於
f) - F8 - Send a next command to GDB.(相當於
n) - F11 - 跳出循環(自己配置的快捷鍵)
- F10 - Send a step command to GDB.(相當於
s)3. CGDB單線程調試運行MFiX(gdb命令行參數)
可以用CGDB來單步調試mfix代碼。這里使用的是mfix-19.1,常規的運行命令為:如果用CGDB調試,首先:./mfixsolver -f mfix.dat
進入GDB界面:cgdb mfixsolver
由於Fortran代碼沒有提供高亮,因此左側代碼沒有花花綠綠的效果,先湊合用。
然后打斷點,運行mfix。注意需要添加命令行參數:run -f mfix.dat

注意,如果要打印module里的變量,需要帶上module名稱,例如 p module_name::var.
參考:https://stackoverflow.com/questions/10264329/fortran-module-variables-not-accessible-in-debuggers
然后可以用之前設置的快捷鍵單步調試mfix程序。注意主循環邏輯都在RUN_MFIX函數里,因此要進入到這個函數里才能進一步單步調試循環過程。最后通過單步調試可以找到循環迭代的位置,在RUN_MFIX這個subroutine里:
4. CGDB調試多線程簡單案例
多線程(MPI)調試調試一直都是個很麻煩的事情,雖然有專業的多線程調試軟件,但是一般都是收費的。這里提供一種在GDB下的方法。主要是先在函數里人為添加一個死循環,然后用GDB attach到這些進程,然后通過set環境變量的方式使其退出循環,並接管程序,進行單步調試。
4.1 添加死循環
首先有下面一段程序:
#include <stdio.h>
#include <string.h>
#include <mpi.h>
const int MAX_STRING = 100;
int main(void){
char greeting[MAX_STRING];
int comm_sz;
int my_rank;
MPI_Init(NULL, NULL);
MPI_Comm_size(MPI_COMM_WORLD, &comm_sz);
MPI_Comm_rank(MPI_COMM_WORLD, &my_rank);
#ifdef MPI_DEBUG
int gdb_break = 1;
while(gdb_break) {};
#endif
if(my_rank != 0){
sprintf(greeting, "Greetings from process %d of %d!", my_rank, comm_sz);
/* int MPI_Send( void* msg_buf_p, //發送消息的緩沖區(內存首地址) int msg_size, //發送消息的緩沖區大小(數據個數) MPI_Datatype msg_type, //發送數據類型 int dest, //數據目的地,這里是0號進程 int tag, //標簽,非負int。比如同樣的數據類型,有的用來打印有的用來計算,只通過前面四個 參數無法區分 MPI_Comm communicator //通訊子,指定通訊范圍 ); */
MPI_Send(greeting, strlen(greeting)+1, MPI_CHAR, 0, 0, MPI_COMM_WORLD);
}else{
printf("Greetings from process %d of %d!\n", my_rank, comm_sz);
for(int q = 1; q < comm_sz; q++){
/* int MPI_Recv( void* msg_buf_p, //接收消息的緩沖區 int buf_size, //接收消息的緩沖區大小 MPI_Datatype buf_type, //接收消息的類型 int source, //消息從哪里發過來 int tag, //標簽,與發送過來的標簽一致 MPI_Comm communicator, //通訊子,指定通訊范圍 MPI_Status* status_p //通常賦值MPI_STATUS_IGNORE即可 ); */
MPI_Recv(greeting, MAX_STRING, \
MPI_CHAR, q, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
printf("%s\n", greeting);
}
}
MPI_Finalize();
return 0;
}
我在代碼的前面加了這樣一句:
#ifdef MPI_DEBUG
int gdb_break = 1;
while(gdb_break) {};
#endif
這樣,我在編譯的時候添加參數-DMPI_DEBUG,運行的時候就能停在這個循環處:

4.2 運行程序
現在我們啟動這個程序:
現在的程序由於進入死循環,因此停在這里。下面我們另外再打開兩個terminal,並進入GDB。
為了方便說明,上面的運行程序的terminal編號#1,另外打開的兩個terminal分別編號#2, #3。
4.3 啟動CGDB
現在#2和#3都以管理員身份進入CGDB:
sudo cgdb

4.4 找到進程編號
在GDB里調用shell,查看a.out的進程號:
(gdb) !ps aux | grep a.out

ps aux是顯示所有進程信息,|是管道符,把結果傳給grep,篩選出含有a.out內容的部分。
可以看到,進程號971和972對應的是a.out的兩個進程,通常進程號小的是0號進程。
4.5 GDB attach到進程
找到進程號,就可以讓GDB attach過去,然后接管程序的運行。
分別讓#2和#3 attach到971進程和972進程。
4.6 跳出死循環
可以看到,現在這兩個進程都停在了19行while這個地方,現在讓程序跳出循環:
(gdb) set gdb_break = 0

4.7 GDB調試
現在GDB已經接管程序了,可以用n往下執行代碼了:
這是#2(也即0號進程)的執行結果,已經跳出循環。下面多執行幾步:
可以看到進入0號進程的邏輯,並打印輸出。繼續執行:
這里程序進入等待狀態,等待#2發送消息。當#2發送完消息:
#1則收到#2發來的消息,並退出等待,可以繼續往下運行:
繼續執行#1:
#1也即0號進程,打印出#2發來的數據,並再次進入for循環,如果此時還有別的進程發來消息則會繼續,但是這里一共只有兩個進程,因此只會收到1號進程發來的消息。
當兩個進程都全部執行完后,整個程序執行結束:
5. 參考
[1] 實驗樓《GDB 簡明教程》
[2] CSDN 《cgdb的介紹和使用》
[3] GitHub (CGDB編譯報錯解決)
[4] CSDN 《gdb調試 -帶有命令行參數》
[5] 知乎《終端調試哪家強?》
[6] CGDB中文手冊《CGDB配置命令》
[7] segmentfault《MPI並行程序的調試技巧》
