<< Debugging multiprocessing programs in GDB - September 28, 2024

Example program:

/* main.c */

#include 
#include 

int main(void) {
    fprintf(stdout, "--multiprocessing example--\n");
    for (int i = 0; i < 5; i++) {
        pid_t pid = fork();
        if (pid == 0) {
            fprintf(stdout, ">>hello from child: %d\n", i);
            return 0;
        }
        if (pid < 0) {
            fprintf(stderr, ">>failed forking\n");
            return -1;
        }
    }

    fprintf(stdout, ">>hello from parent\n");

    return 0;
}

We are going to step through this program. For compilation, I am going to use GCC and turn on debugging info.

$ gcc -g3 main.c -o main

For the purposes of this demonstration, I turned off the following variable in gdb. This will block execution of parent as well as the forked process when fork() syscall is called.

$ set detach-on-fork off

Following the parent

This scenario is surprisingly simple, since it is done automatically by gdb.

[hemisquare@detached-hemi tmp]$ gdb main
(gdb) break main 
Breakpoint 1 at 0x1161: file main.c, line 7.
(gdb) run
Starting program: /home/hemisquare/tmp/main 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".

Breakpoint 1, main () at main.c:7
7	    fprintf(stdout, "--multiprocessing example--\n");
(gdb) s
--multiprocessing example--
8	    for (int i = 0; i < 5; i++) {
(gdb) 
9	        pid_t pid = fork();
(gdb) 
[New inferior 2 (process 51915)]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".
10	        if (pid == 0) {
(gdb) p pid
$1 = 51915
(gdb) # we are the parent

Following the child

Just like in the previous example, I will first step through the program until it calls fork().

[hemisquare@detached-hemi tmp]$ gdb main 
(gdb) break main
Breakpoint 1 at 0x1161: file main.c, line 7.
(gdb) run
Starting program: /home/hemisquare/tmp/main 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".

Breakpoint 1, main () at main.c:7
7	    fprintf(stdout, "--multiprocessing example--\n");
(gdb) s
--multiprocessing example--
8	    for (int i = 0; i < 5; i++) {
(gdb) 
9	        pid_t pid = fork();
(gdb) 
[New inferior 2 (process 52175)]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".
10	        if (pid == 0) {
(gdb)

fork() now creates a child process (inferior in gdb terms). To list all inferiors:

(gdb) info inferiors
  Num  Description       Connection           Executable        
* 1    process 52172     1 (native)           /home/hemisquare/tmp/main 
  2    process 52175     1 (native)           /home/hemisquare/tmp/main 
(gdb)

Star indicates the inferior we are currently attached to. In this case inferior 2 is the new child, so we can switch to it by typing:

(gdb) inferior 2 
[Switching to inferior 2 [process 52175] (/home/hemisquare/tmp/main)]
[Switching to thread 2.1 (Thread 0x7ffff7dab740 (LWP 52175))]
#0  0x00007ffff7e90b57 in _Fork () from /usr/lib/libc.so.6
(gdb) finish
Run till exit from #0  0x00007ffff7e90b57 in _Fork () from /usr/lib/libc.so.6
0x00007ffff7e966e2 in fork () from /usr/lib/libc.so.6
(gdb) finish
Run till exit from #0  0x00007ffff7e966e2 in fork () from /usr/lib/libc.so.6
0x0000555555555192 in main () at main.c:9
9	        pid_t pid = fork();
(gdb) nexti
10	        if (pid == 0) {
(gdb) p pid 
$1 = 0
(gdb) s
11	            fprintf(stdout, ">>hello from child: %d\n", i);
(gdb) 
>>hello from child: 0
12	            return 0;
(gdb)

And this is how you can step through the child. Note that the parent is still handing where we left it off. Next step would be to switch back to the parent and continue debugging from there.