Skip to content

LV091-gdb-多进程调试

GDB调试器不只可以调试多线程程序,还可以调试多进程程序。

一、多进程程序

对于 C 和 C++ 程序而言,多进程的实现往往借助的是<unistd.h>头文件中的 fork() 函数或者 vfork() 函数,一个简单的多进程程序如下:

c
#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 调试多进程程序中的子进程呢?

二、GDB attach 命令调试进程

1. 语法格式

无论父进程还是子进程,都可以借助 attach 命令启动 GDB 调试它。attach 命令用于调试正在运行的进程,要知道对于每个运行的进程,操作系统都会为其配备一个独一无二的 ID 号。在得知目标子进程 ID 号的前提下,就可以借助 attach 命令来启动 GDB 对其进行调试。

shell
(gdb) attach pid

pid就是要调试的进程的进程ID。对于子进程 ID 号的获取,除了依靠 GDB 调试器打印出的信息,也可以使用 pidof 命令手动获取。

2. YAMA ptrace_scope 安全机制

后面调试测试的时候发现个问题,我是在ubuntu中尝试的,报了下面的错误:

shell
(gdb) attach 30191
Attaching to program: /home/sumu/workspace/c-learning/02-c-basic/21-debug/a.out, process 30191
Could not attach to process.  If your uid matches the uid of the target
process, check the setting of /proc/sys/kernel/yama/ptrace_scope, or try
again as the root user.  For more details, see /etc/sysctl.d/10-ptrace.conf
ptrace: 不允许的操作.

系统的安全模块(YAMA)通过一个 ptrace_scope 内核参数来控制 ptrace 系统调用的行为,其默认值通常为 1,具体含义如下:

描述影响的调试场景
0允许任意进程间跟踪 (ptrace)。最宽松attach 任意非子进程都可工作。
1只允许跟踪子进程 (默认值)。受限gdb ./a.out 可以,但 gdb attach 子进程PID 不行。
2只允许管理员 (root) 进行跟踪。严格:仅 sudo gdb 可用。
3完全禁止任何进程跟踪。最严格:任何调试器都无法 attach

这个是为了安全考虑,系统限制了一个进程(如 GDB)去“检查”或“操控”另一个非其子进程的进程。我们看到的子进程是由父进程创建的,但它在创建后便独立了。所以犹豫这个安全模块的存在,我们无法直接调试子进程,可以调试前临时修改,重启后失效,相对安全。

shell
# 在当前会话临时将限制放宽到级别 0
sudo sysctl -w kernel.yama.ptrace_scope=0

3. 使用示例

3.1 测试程序

这里还需要解决一个问题,很多场景中子进程的执行时间都是一瞬而逝的,这意味着,我们可能还未查到它的进程 ID 号,该进程就已经执行完了,何谈借助 attach 命令对其调试呢?对于 C、C++ 多进程程序,解决该问题最简单直接的方法是,在目标进程所执行代码的开头位置,添加一段延时执行的代码。

shell
#include <stdio.h>
#include <unistd.h>

int main(int argc, const char *argv[]) {
    pid_t pid = fork();
    if (pid == 0) {
        int num = 10;
        while (num == 10) {
            sleep(10);
        }
        printf("this is child,pid = %d\n", getpid());
    }
    else {
        printf("this is parent,pid = %d\n", getpid());
    }
    return 0;
}

该进程执行时会直接进入死循环。这样做的好处有 2 个,其一是帮助 attach 命令成功捕捉到要调试的进程;其二是使用 GDB 调试该进程时,进程中真正的代码部分尚未得到执行,使得我们可以从头开始对进程中的代码进行调试。

Tips:进程都已经进行死循环了,后续代码还可以进行调试吗?当然可以,以上面示例中给出的死循环,我们只需用 print 命令临时修改 num 变量的值,即可使程序跳出循环,从而执行后续代码。

我们在 ubuntu 中测试,所以直接用下面的命令编译:

shell
cd ~/workspace/c-learning/02-c-basic/21-debug
gcc 039-gdb-multi-process.c -g

3.2 调试示例

shell
 sumu@virtual-machine:~/workspace/c-learning/02-c-basic/21-debug [main  +1 ~0 -0 !]
$ gdb ./a.out -q
Reading symbols from ./a.out...
(gdb) r   # <=== 1. 运行程序
Starting program: /home/sumu/workspace/c-learning/02-c-basic/21-debug/a.out 
[Detaching after fork from child process 30790] # <=== 子进程创建,pid为30790
this is parent,pid = 30786
[Inferior 1 (process 30786) exited normally] # <=== 父进程执行完毕退出了
(gdb) attach 30790  # <=== 2. 跳转调试ID为30790的进程
Attaching to program: /home/sumu/workspace/c-learning/02-c-basic/21-debug/a.out, process 30790
Reading symbols from /lib/x86_64-linux-gnu/libc.so.6...
Reading symbols from /usr/lib/debug/.build-id/57/92732f783158c66fb4f3756458ca24e46e827d.debug...
Reading symbols from /lib64/ld-linux-x86-64.so.2...
Reading symbols from /usr/lib/debug/.build-id/db/3ae442c4308e6250049fb6159c302cf4274fa2.debug...
0x00007ffff7e9d1b4 in __GI___clock_nanosleep (clock_id=<optimized out>, clock_id@entry=0, 
    flags=flags@entry=0, req=req@entry=0x7fffffffccd0, rem=rem@entry=0x7fffffffccd0)
    at ../sysdeps/unix/sysv/linux/clock_nanosleep.c:78 # <=== 内部是一个死循环,会一直停在sleep函数
78      ../sysdeps/unix/sysv/linux/clock_nanosleep.c: 没有那个文件或目录.
(gdb) bt  # <=== 3. 可以看一下函数栈帧的情况
#0  0x00007ffff7e9d1b4 in __GI___clock_nanosleep (clock_id=<optimized out>, clock_id@entry=0, 
    flags=flags@entry=0, req=req@entry=0x7fffffffccd0, rem=rem@entry=0x7fffffffccd0)
    at ../sysdeps/unix/sysv/linux/clock_nanosleep.c:78
#1  0x00007ffff7ea2ec7 in __GI___nanosleep (requested_time=requested_time@entry=0x7fffffffccd0, 
    remaining=remaining@entry=0x7fffffffccd0) at nanosleep.c:27
#2  0x00007ffff7ea2dfe in __sleep (seconds=0) at ../sysdeps/posix/sleep.c:55
#3  0x00005555555551dd in main (argc=1, argv=0x7fffffffce28) at 039-gdb-multi_process.c:9
(gdb) frame 3 # <=== 4.将编号为3的栈帧设置为当前栈帧,这样其实就回到了源代码中
#3  0x00005555555551dd in main (argc=1, argv=0x7fffffffce28) at 039-gdb-multi_process.c:9
9                   sleep(10);
(gdb) p num   # <=== 5. 打印num的值
$1 = 10
(gdb) p num=1 # <=== 6. 修改num的值为1
$2 = 1
(gdb) c       # <=== 7. 继续执行
Continuing.
this is child,pid = 30790
[Inferior 1 (process 30790) exited normally] # <=== 子进程退出,因为前面修改了num的值为1,不满足循环条件,就跳出了循环
(gdb)

三、显式指定要调试的进程

1. follow-fork-mode

GDB 调试多进程程序时默认只调试父进程。对于内核版本为 2.5.46 甚至更高的 Linux 发行版系统来说,可以通过修改 follow-fork-mode 或者 detach-on-fork 选项的值来调整这一默认设置。

确切地说,对于使用 fork() 或者 vfork() 函数构建的多进程程序,借助 follow-fork-mode 选项可以设定 GDB 调试父进程还是子进程。该选项的使用语法格式为:

shell
(gdb) set follow-fork-mode mode

参数 mode 的可选值有 2 个:

  • parent:选项的默认值,表示 GDB 调试器默认只调试父进程;
  • child:和 parent 完全相反,它使的 GDB 只调试子进程。且当程序中包含多个子进程时,我们可以逐一对它们进行调试。

2. show follow-fork-mode

通过执行如下命令,我们可以轻松了解到当前调试环境中 follow-fork-mode 选项的值:

shell
(gdb) show follow-fork-mode

3. detach-on-fork

借助 follow-fork-mode 选项,我们只能选择调试子进程还是父进程,且一经选定,调试过程中将无法改变。如果既想调试父进程,又想随时切换并调试某个子进程,就需要借助 detach-on-fork 选项。detach-on-fork 选项的语法格式如下:

shell
(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 仍可以重新启用。
(gdb) remove-inferior id彻底删除指令 id 编号的进程(从 info inferiors 打印的列表中消除),不过在执行此操作之前,需先使用 detach inferior id 或者 kill inferior id 命令将该进程与 GDB 分离,同时确认其不是当前进程。
### 4. 使用示例

4.1 自动进入子进程

4.1.1 测试程序
shell
#include <stdio.h>
#include <unistd.h>

int main(int argc, const char *argv[]) {
    pid_t pid = fork();
    if (pid == 0) {
        int num = 10;
        while (num == 10) {
            sleep(10);
        }
        printf("this is child,pid = %d\n", getpid());
    }
    else {
        printf("this is parent,pid = %d\n", getpid());
    }
    return 0;
}

该进程执行时会直接进入死循环。这样做的好处有 2 个,其一是帮助 attach 命令成功捕捉到要调试的进程;其二是使用 GDB 调试该进程时,进程中真正的代码部分尚未得到执行,使得我们可以从头开始对进程中的代码进行调试。

Tips:进程都已经进行死循环了,后续代码还可以进行调试吗?当然可以,以上面示例中给出的死循环,我们只需用 print 命令临时修改 num 变量的值,即可使程序跳出循环,从而执行后续代码。

我们在 ubuntu 中测试,所以直接用下面的命令编译:

shell
cd ~/workspace/c-learning/02-c-basic/21-debug
gcc 039-gdb-multi-process.c -g
4.1.2 调试示例
shell
 sumu@virtual-machine:~/workspace/c-learning/02-c-basic/21-debug [main  +1 ~0 -0 !]
$ gdb ./a.out -q
Reading symbols from ./a.out...
(gdb) show follow-fork-mode # <=== 1. 查看当前的默认调试的进程模式
Debugger response to a program call of fork or vfork is "parent".
(gdb) set follow-fork-mode child # <=== 2. 设置调试子进程
(gdb) show follow-fork-mode      # <=== 3. 查看是否生效
Debugger response to a program call of fork or vfork is "child".
(gdb) r                          # <=== 4. 运行程序
Starting program: /home/sumu/workspace/c-learning/02-c-basic/21-debug/a.out 
[Attaching after process 31600 fork to child process 31604]
[New inferior 2 (process 31604)]
[Detaching after fork from parent process 31600]
[Inferior 1 (process 31600) detached]
this is parent,pid = 31600
^Z                               # <=== 5. 到这里之后父进程不会自动结束,这里按下Ctrl+z将暂时挂起进程
Thread 2.1 "a.out" received signal SIGTSTP, Stopped (user).
[Switching to process 31604]     # <=== 6. 自动进入子进程
0x00007ffff7e9d1b4 in __GI___clock_nanosleep (clock_id=<optimized out>, clock_id@entry=0, 
    flags=flags@entry=0, req=req@entry=0x7fffffffccd0, rem=rem@entry=0x7fffffffccd0)
    at ../sysdeps/unix/sysv/linux/clock_nanosleep.c:78
78      ../sysdeps/unix/sysv/linux/clock_nanosleep.c: 没有那个文件或目录.
(gdb) p num                      # <=== 7. 查看num的值,现在处于sleep函数内部,所以无法查看
$1 = num
(gdb) bt                         # <=== 8. 查看栈帧
#0  0x00007ffff7e9d1b4 in __GI___clock_nanosleep (clock_id=<optimized out>, clock_id@entry=0, 
    flags=flags@entry=0, req=req@entry=0x7fffffffccd0, rem=rem@entry=0x7fffffffccd0)
    at ../sysdeps/unix/sysv/linux/clock_nanosleep.c:78
#1  0x00007ffff7ea2ec7 in __GI___nanosleep (requested_time=requested_time@entry=0x7fffffffccd0, 
    remaining=remaining@entry=0x7fffffffccd0) at nanosleep.c:27
#2  0x00007ffff7ea2dfe in __sleep (seconds=0) at ../sysdeps/posix/sleep.c:55
#3  0x00005555555551dd in main (argc=1, argv=0x7fffffffce28) at 039-gdb-multi_process.c:9
(gdb) frame 3                    # <=== 9. 将编号为3的栈帧设置为当前帧,这样就直接回到了源码中
#3  0x00005555555551dd in main (argc=1, argv=0x7fffffffce28) at 039-gdb-multi_process.c:9
9                   sleep(10);
(gdb) p num                      # <=== 10. 打印当前的num值
$2 = 10
(gdb) p num=1                    # <=== 11. 修改num值为1,将跳出循环
$3 = 1
(gdb) c
Continuing.
this is child,pid = 31604
[Inferior 2 (process 31604) exited normally]
(gdb)

4.2 调试多个进程

4.1.1 测试程序
shell
#include <stdio.h>
#include <unistd.h>

int main(int argc, const char *argv[]) {
    pid_t pid = fork();
    if (pid == 0) {
        int num = 10;
        while (num == 10) {
            sleep(10);
        }
        printf("this is child,pid = %d\n", getpid());
    }
    else {
        int mnum = 5;
        while (mnum == 5) {
            sleep(1);
        }
        printf("this is parent,pid = %d\n", getpid());
    }
    return 0;
}

该进程执行时会直接进入死循环。这样做的好处有 2 个,其一是帮助 attach 命令成功捕捉到要调试的进程;其二是使用 GDB 调试该进程时,进程中真正的代码部分尚未得到执行,使得我们可以从头开始对进程中的代码进行调试。

Tips:进程都已经进行死循环了,后续代码还可以进行调试吗?当然可以,以上面示例中给出的死循环,我们只需用 print 命令临时修改 num 变量的值,即可使程序跳出循环,从而执行后续代码。

我们在 ubuntu 中测试,所以直接用下面的命令编译:

shell
cd ~/workspace/c-learning/02-c-basic/21-debug
gcc 040-gdb-multi-process-detch-on-fork.c -g
4.1.2 调试示例
shell
 sumu@virtual-machine:~/workspace/c-learning/02-c-basic/21-debug [main  +1 ~0 -0 !]
$ gdb ./a.out -q
Reading symbols from ./a.out...
(gdb) set detach-on-fork off # <=== 1. 设置GDB可以调试多个进程
(gdb) l                      # <=== 2. 查看源码
1       #include <stdio.h>
2       #include <unistd.h>
3
4       int main(int argc, const char *argv[]) {
5           pid_t pid = fork();
6           if (pid == 0) {
7               int num = 10;
8               while (num == 10) {
9                   sleep(10);
10              }
(gdb) b 5                   # <=== 3. 第5行打断点(在fork这里停下)
Breakpoint 1 at 0x11bc: file 040-gdb-multi-process-detch-on-fork.c, line 5.
(gdb) r
Starting program: /home/sumu/workspace/c-learning/02-c-basic/21-debug/a.out 

Breakpoint 1, main (argc=1, argv=0x7fffffffce28) at 040-gdb-multi-process-detch-on-fork.c:5
5           pid_t pid = fork();
(gdb) n                     # <=== 4. 单步运行
[New inferior 2 (process 32508)] # <=== 新增一个子进程,id为32508
Reading symbols from /home/sumu/workspace/c-learning/02-c-basic/21-debug/a.out...
Reading symbols from /usr/lib/debug/.build-id/57/92732f783158c66fb4f3756458ca24e46e827d.debug...
Reading symbols from /usr/lib/debug/.build-id/db/3ae442c4308e6250049fb6159c302cf4274fa2.debug...
6           if (pid == 0) {
(gdb) n                     # <=== 5. GDB默认调试父进程,这里进入else分支
14              int mnum = 5;
(gdb) info inferiors        # <=== 6. 查看当前调试环境中进程数,*号所处进程为当前正在调试进程
  Num  Description       Executable        
* 1    process 32463     /home/sumu/workspace/c-learning/02-c-basic/21-debug/a.out 
  2    process 32508     /home/sumu/workspace/c-learning/02-c-basic/21-debug/a.out 
(gdb) inferior 2            # <=== 7. 进入Num为2的子进程
[Switching to inferior 2 [process 32508] (/home/sumu/workspace/c-learning/02-c-basic/21-debug/a.out)]
[Switching to thread 2.1 (process 32508)]
#0  arch_fork (ctid=0x7ffff7fb3810) at ../sysdeps/unix/sysv/linux/arch-fork.h:49
49      ../sysdeps/unix/sysv/linux/arch-fork.h: 没有那个文件或目录.
(gdb) info inferiors        # <=== 8. 查看当前调试进程
  Num  Description       Executable        
  1    process 32463     /home/sumu/workspace/c-learning/02-c-basic/21-debug/a.out 
* 2    process 32508     /home/sumu/workspace/c-learning/02-c-basic/21-debug/a.out 
(gdb) n                    # <=== 9. 单步运行
53      in ../sysdeps/unix/sysv/linux/arch-fork.h
(gdb) n                    # <=== 10. 单步运行
__libc_fork () at ../sysdeps/nptl/fork.c:78
78      ../sysdeps/nptl/fork.c: 没有那个文件或目录.
(gdb) n                    # <=== 11. 单步运行
83      in ../sysdeps/nptl/fork.c
(gdb) n                    # <=== 12. 单步运行
100     in ../sysdeps/nptl/fork.c
(gdb) n                    # <=== 13. 单步运行
102     in ../sysdeps/nptl/fork.c
(gdb) n                    # <=== 14. 单步运行
113     in ../sysdeps/nptl/fork.c
(gdb) n                    # <=== 15. 单步运行
126     in ../sysdeps/nptl/fork.c
(gdb) n                    # <=== 16. 单步运行
129     in ../sysdeps/nptl/fork.c
(gdb) n                    # <=== 17. 单步运行,到这里,才真正完成fork,即将进入子进程
main (argc=1, argv=0x7fffffffce28) at 040-gdb-multi-process-detch-on-fork.c:6
6           if (pid == 0) { 
(gdb) n                   # <=== 18. 已进入子进程调试
7               int num = 10;
(gdb)