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)