1. 程式人生 > 實用技巧 >GDB除錯多程序程式

GDB除錯多程序程式

GDB除錯多程序程式

GDB偵錯程式不只可以除錯多執行緒程式,還可以除錯多程序程式。對於 C 和 C++ 程式而言,多程序的實現往往藉助的是<unistd.h>標頭檔案中的 fork() 函式或者 vfork() 函式。舉個例子:

#include <stdio.h>
#include <unistd.h>
int main()
{
    pid_t pid = fork();
    if(pid == 0)
    {
        printf("this is child,pid = %d\n",getpid());
    }
    else
{ printf("this is parent,pid = %d\n",getpid()); } return 0; }

可以看到,程式中包含 2 個程序,分別為父程序(又稱主程序)和使用 fork() 函式分離出的子程序。
事實上在多數 Linux 發行版系統中,GDB 並沒有對多程序程式提供友好的除錯功能。無論程式中呼叫了多少次 fork() 函式(或者 vfork() 函式),從父程序中分離出多少個子程序,GDB 預設只調試父程序,而不除錯子程序。

GDB attach命令除錯程序

首先,無論父程序還是子程序,都可以藉助 attach 命令啟動 GDB 除錯它。attach 命令用於除錯正在執行的程序,要知道對於每個執行的程序,作業系統都會為其配備一個獨一無二的 ID 號。在得知目標子程序 ID 號的前提下,就可以藉助 attach 命令來啟動 GDB 對其進行除錯。
這裡還需要解決一個問題,很多場景中子程序的執行時間都是一瞬而逝的,這意味著,我們可能還未查到它的程序 ID 號,該程序就已經執行完了,何談藉助 attach 命令對其除錯呢?對於 C、C++ 多程序程式,解決該問題最簡單直接的方法是,在目標程序所執行程式碼的開頭位置,新增一段延時執行的程式碼。
例如,將上面程式中if(pid==0)

判斷語句整體做如下修改:

if(pid == 0)
{
    int num =10;
    while(num==10){
        sleep(10);
    }
    printf("this is child,pid = %d\n",getpid());
}

可以看到,通過新增第 3~6 行程式碼,該程序執行時會直接進入死迴圈。這樣做的好處有 2 個,其一是幫助 attach 命令成功捕捉到要除錯的程序;其二是使用 GDB 除錯該程序時,程序中真正的程式碼部分尚未得到執行,使得我們可以從頭開始對程序中的程式碼進行除錯。

程序都已經進行死迴圈了,後續程式碼還可以進行除錯嗎?當然可以,以上面示例中給出的死迴圈,我們只需用 print 命令臨時修改 num 變數的值,即可使程式跳出迴圈,從而執行後續程式碼。

就以除錯修改後的 myfork.c 程式(已將其編譯為 myfork.exe 可執行檔案)為例:

[root@bogon demo]# gdb myfork.exe -q
Reading symbols from ~/demo/myfork.exe...done.
(gdb) r
Starting program: ~/demo/myfork.exe
Detaching after fork from child process 5316.  <-- 子程序的 ID 號為 5316
this is parent,pid = 5313               <-- 父程序執行完畢

Program exited normally.
(gdb) attach 5316                          <-- 跳轉除錯 ID 號為 5316 的子程序
......
(gdb) n                                           <-- 程式正在執行,所有直接使用 next 命令就可以進行單步除錯
Single stepping until exit from function __nanosleep_nocancel,
which has no line number information.
0x00000037ee2acb50 in sleep () from /lib64/libc.so.6
(gdb) n
Single stepping until exit from function sleep,
which has no line number information.
main () at myfork.c:10
10  while(num==10){
(gdb) p num=1
$1 = 1
(gdb) n                                           <-- 跳出迴圈
13         printf("this is child,pid = %d\n",getpid());
(gdb) c
Continuing.
this is child,pid = 5316

Program exited normally.
(gdb)

對於子程序 ID 號的獲取,除了依靠 GDB 偵錯程式打印出的資訊,也可以使用 pidof 命令手動獲取。

GDB顯式指定要除錯的程序

前面提到,GDB 除錯多程序程式時預設只調試父程序。對於核心版本為 2.5.46 甚至更高的 Linux 發行版系統來說,可以通過修改 follow-fork-mode 或者 detach-on-fork 選項的值來調整這一預設設定。

GDB follow-fork-mode選項

確切地說,對於使用 fork() 或者 vfork() 函式構建的多程序程式,藉助 follow-fork-mode 選項可以設定 GDB 除錯父程序還是子程序。該選項的使用語法格式為:

(gdb) set follow-fork-mode mode

引數 mode 的可選值有 2 個:

  • parent:選項的預設值,表示 GDB 偵錯程式預設只調試父程序;
  • child:和 parent 完全相反,它使的 GDB 只調試子程序。且當程式中包含多個子程序時,我們可以逐一對它們進行除錯。

舉個例子:

(gdb) show follow-fork-mode
Debugger response to a program call of fork or vfork is "parent".
(gdb) set follow-fork-mode child                        <-- 除錯子程序
(gdb) r 
Starting program: ~/demo/myfork.exe
[New process 5376]
this is parent,pid = 5375                  <-- 父程序執行完成

Program received signal SIGTSTP, Stopped (user).
[Switching to process 5376]             <-- 自動進入子程序
0x00000037ee2accc0 in __nanosleep_nocancel () from /lib64/libc.so.6
(gdb) n
Single stepping until exit from function __nanosleep_nocancel,
which has no line number information.
0x00000037ee2acb50 in sleep () from /lib64/libc.so.6
(gdb) n
Single stepping until exit from function sleep,
which has no line number information.
main () at myfork.c:10
10  while(num==10){
(gdb) p num=1
$2 = 1
(gdb) c
Continuing.
this is child,pid = 5376

通過執行如下命令,我們可以輕鬆瞭解到當前除錯環境中 follow-fork-mode 選項的值:

(gdb) show follow-fork-mode
Debugger response to a program call of fork or vfork is "child".

GDB detach-on-fork選項

注意,藉助 follow-fork-mode 選項,我們只能選擇除錯子程序還是父程序,且一經選定,除錯過程中將無法改變。如果既想除錯父程序,又想隨時切換並除錯某個子程序,就需要藉助 detach-on-fork 選項。
detach-on-fork 選項的語法格式如下:

(gdb) set detach-on-fork mode

其中,mode 引數的可選值有 2 個:

  • on:預設值,表明 GDB 只調試一個程序,可以是父程序,或者某個子程序;
  • off:程式中出現的每個程序都會被 GDB 記錄,我們可以隨時切換到任意一個程序進行除錯。

和 detach-on-fork 搭配使用的,還有如表 1 所示的幾個命令。

表 1 GDB多程序除錯常用命令
命令語法格式功 能
(gdb)show detach-on-fork 檢視當前除錯環境中 detach-on-fork 選項的值。
(gdb) info inferiors 檢視當前除錯環境中有多少個程序。其中,程序 id 號前帶有 * 號的為當前正在除錯的程序。
(gdb) inferiors id 切換到指定 ID 編號的程序對其進行除錯。
(gdb) detach inferior id 斷開 GDB 與指定 id 編號程序之間的聯絡,使該程序可以獨立執行。不過,該程序仍存在 info inferiors 列印的列表中,其 Describution 列為 <null>,並且藉助 run 仍可以重新啟用。
(gdb) kill inferior id 斷開 GDB 與指定 id 編號程序之間的聯絡,並中斷該程序的執行。不過,該程序仍存在 info inferiors 列印的列表中,其 Describution 列為 <null>,並且藉助 run 仍可以重新啟用。
remove-inferior id 徹底刪除指令 id 編號的程序(從 info inferiors 列印的列表中消除),不過在執行此操作之前,需先使用 detach inferior id 或者 kill inferior id 命令將該程序與 GDB 分離,同時確認其不是當前程序。

這裡仍以除錯 myfork.c 程式為例,不過為了讓讀者清楚地感受 detach-on-fork 選項的功能,這裡需要對 else 語句塊的程式碼進行如下修改:

else
{
    int mnum=5;
    while(mnum==5){
        sleep(1);
    }
    printf("this is parent,pid = %d\n",getpid());
}

也就是說,myfork.c 程式中,父程序和子程序中各擁有一個死迴圈。在此基礎上,進行如下除錯:

(gdb) set detach-on-fork off             <-- 令 GDB 可除錯多個程序
(gdb) b 6
Breakpoint 1 at 0x11b5: file myfork.c, line 6.
(gdb) r
Starting program: ~/demo/myfork.exe

Breakpoint 1, main () at myfork.c:6
6     pid_t pid = fork();
(gdb) n
[New inferior 2 (process 5163)]           <-- 新增一個子程序,ID 號為 5163
Reading symbols from ~/demo/myfork.exe...
Reading symbols from /usr/lib/debug/lib/x86_64-linux-gnu/libc-2.31.so...
7     if(pid == 0)
(gdb) n                                                <-- 由於 GDB 預設除錯父程序,因此進入 else 語句
17     int mnum=5;
(gdb) info inferiors     <-- 檢視當前除錯環境中的程序數,當前有 2 個程序,1 號程序為當前正在除錯的程序
  Num  Description       Executable       
* 1    process 5159      ~/demo/myfork.exe
  2    process 5163       ~/demo/myfork.exe
(gdb) inferior 2                                   <-- 進入 id 號為 2 的子程序
[Switching to inferior 2 [process 5163] (~/demo/myfork.exe)]
[Switching to thread 2.1 (process 5163)]
(gdb) n
53 in ../sysdeps/unix/sysv/linux/arch-fork.h
(gdb) n
__libc_fork () at ../sysdeps/nptl/fork.c:78
78 ../sysdeps/nptl/fork.c: No such file or directory.
(gdb) n
......                                                       <-- 執行多個 next 命令
(gdb) n
main () at myfork.c:7                           <-- 正式單步除錯子程序  
7     if(pid == 0)
(gdb) n
9         int num =10;
(gdb)

可以看到,通過設定 detach-on-fork 選項值為 off,再配合使用 info inferiors 等命令,即可隨意切換到當前環境中的各個程序,並對它們進行除錯。