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;
}
程序的存储路径为 ~/demo/myfork.c。可以看到,程序中包含两个进程,分别为父进程(又称主进程)和使用 fork() 函数分离出的子进程。
事实上在多数 Linux 发行版系统中,GDB 并没有对多进程程序提供友好的调试功能。无论程序中调用了多少次 fork() 函数(或者 vfork() 函数),从父进程中分离出多少个子进程,GDB 默认只调试父进程,而不调试子进程。
那么问题就出现了,如何使用 GDB 调试多进程程序中的子进程呢?
首先,无论父进程还是子进程,都可以借助 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@haicoder 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 调试多进程程序时默认只调试父进程。对于内核版本为 2.5.46 甚至更高的 Linux 发行版系统来说,可以通过修改 follow-fork-mode 或者 detach-on-fork 选项的值来调整这一默认设置。
确切地说,对于使用 fork() 或者 vfork() 函数构建的多进程程序,借助 follow-fork-mode 选项可以设定 GDB 调试父进程还是子进程。该选项的使用语法格式为:
(gdb) set follow-fork-mode mode
参数 mode 的可选值有 2 个:
举个例子:
(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".
注意,借助 follow-fork-mode 选项,我们只能选择调试子进程还是父进程,且一经选定,调试过程中将无法改变。如果既想调试父进程,又想随时切换并调试某个子进程,就需要借助 detach-on-fork 选项。
detach-on-fork 选项的语法格式如下:
(gdb) set detach-on-fork mode
其中,mode 参数的可选值有 2 个:
和 detach-on-fork 搭配使用的,还有如下表所示的几个命令。
命令语法格式 | 功 能 |
---|---|
(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 列为 |
(gdb) kill inferior id | 断开 GDB 与指定 id 编号进程之间的联系,并中断该进程的执行。不过,该进程仍存在 info inferiors 打印的列表中,其 Describution 列为 |
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 等命令,即可随意切换到当前环境中的各个进程,并对它们进行调试。