Featured image for C/C++ language topics.

Welcome back to this series about using the GNU debugger (GDB) to print information in a way that is similar to using print statements in your code. The first article introduced you to using GDB for printf-style debugging, and the second article showed how to save commands and output. This final article demonstrates the power of GDB to interact with C and C++ functions and automate GDB behavior.

Calling program-defined output routines

Our example program contains a function named print_tree that outputs a constructed tree. Suppose that you wish to use that function to examine the tree on which the insert function will operate each time it is called. This can be done by setting a breakpoint on insert and then specifying GDB commands to execute each time the breakpoint is hit. But before looking at how that's done, let's first look at how ordinary GDB breakpoints work.

Setting a breakpoint

A breakpoint can be set using the break command. We can use it to set a breakpoint at the start of the insert function, as follows:

(gdb) break insert
Breakpoint 1 at 0x40127a: file tree.c, line 40.

If you refer back to the source code from Part 1, you'll see that line 40 is the first executable line of the function. If you run the program, GDB stops at the breakpoint, showing the values of the arguments to the function in addition to the line at which GDB has stopped:

(gdb) run
Starting program: /home/kev/ctests/tree 

Breakpoint 1, insert (tree=0x0, data=0x40203f "dog") at tree.c:40
40      if (tree == NULL)

There are many interesting things you could do now, such as examining a stack trace via the backtrace command, or perhaps printing other values using GDB's print command. However, I wish to demonstrate the call and continue commands that we'll use in the next section.

Call and continue

In the following example, I issue the continue command to ask GDB to continue past that breakpoint seven times, stopping again the eighth time it is hit. By default, the continue command causes the program to execute to the next breakpoint. Providing a numeric argument to this command tells GDB to continue that number of times without stopping at the intermediate breakpoints. After the continue command stops, the call command calls print_tree():

(gdb) continue 8
Will ignore next 7 crossings of breakpoint 1.  Continuing.

Breakpoint 1, insert (tree=0x4052a0, data=0x402055 "gecko") at tree.c:40
40      if (tree == NULL)
(gdb) call print_tree(tree)
  cat
dog
    javelina
  wolf
(gdb) 

GDB's printf command

GDB also has a printf command, which I've used here:

(gdb) printf "tree is %lx and data is %s\n", tree, data
tree is 4052a0 and data is gecko

GDB's printf command prints to GDB's console, not to the program output. For this example, we will probably find it more useful to call the program's printf() function from the standard C library. It will print to the program's output, which is also where the program's print_tree function prints its output:

(gdb) call printf("tree is %lx and data is %s\n", tree, data)
tree is 4052a0 and data is gecko
$1 = 33
(gdb) 

This output differs from GDB's built-in printf command by printing an additional line ($1 = 33). What's happening here is that GDB is calling printf() to output the expected result. The printf() function returns an integer representing the number of characters printed. This return value is printed to the GDB console and saved to the value history. If you want to suppress the printing of the return value (as well as its appearance in the value history), cast the return value of printf() to void, like this:

(gdb) call (void) printf("tree is %lx and data is %s\n", tree, data)
tree is 4052a0 and data is gecko

Attaching commands to a breakpoint

We are now ready to use GDB's commands command to attach a list of commands to a previously set breakpoint or list of breakpoints. When no breakpoint number or list is provided, commands adds commands to the most recently defined breakpoint. Assuming that to be the case for the breakpoint set in the previous section, we can use the commands command as follows:

(gdb) commands
Type commands for breakpoint(s) 1, one per line.
End with a line saying just "end".
>silent
>call (void) printf("Entering insert(tree=%lx, data=%s)\n", tree, data)
>if (tree != 0)
 >call (void) printf("Tree is...\n")
 >call (void) print_tree(tree)
 >end
>continue
>end
(gdb) 

The first command associated with the breakpoint is silent. It tells GDB not to print the usual messages that are printed when stopping at a breakpoint.

Next is a call command. It invokes printf(), which prints a message showing that the insert() function has been entered along with the values of tree and data.

Next comes an if command. It checks whether the value of tree is non-zero (that is, non-NULL). If this condition is true, then the two call commands are executed, because there is data in tree. If not, those call commands are skipped. The end command terminates the block of commands for the if command.

A continue command comes next. It causes GDB to resume execution until either another breakpoint is hit or the program terminates.

Finally, an end command terminates the list of commands to attach to the breakpoint.

With the breakpoint and its associated commands in place, running the program produces the following output:

(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/kev/ctests/tree 
Entering insert(tree=0, data=dog)
Entering insert(tree=4056e0, data=cat)
Tree is...
dog
Entering insert(tree=0, data=cat)
Entering insert(tree=4056e0, data=wolf)
Tree is...
  cat
dog
...
Entering insert(tree=405970, data=scorpion)
Tree is...
  gecko
javelina
Entering insert(tree=0, data=scorpion)
cat coyote dog gecko javelina scorpion wolf 

  cat
    coyote
dog
      gecko
    javelina
      scorpion
  wolf
[Inferior 1 (process 326307) exited normally]

Saving the insert function breakpoint

Let's use the info breakpoints command to look at the breakpoint for insert:

(gdb) info breakpoints 
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x000000000040127a in insert at tree.c:40
    breakpoint already hit 19 times
        silent
        call (void) printf("Entering insert(tree=%lx, data=%s)\n", tree, data)
        if (tree != 0)
          call (void) printf("Tree is...\n")
          call (void) print_tree(tree)
        end
        continue
(gdb) 

Observe that the info breakpoints output shows the number of times that the breakpoint has been hit. In this program, we see that insert was called 19 times. Although it's not especially relevant for the current discussion, knowing how many times a particular function was called might be useful for optimization or performance analysis.

Let's save this breakpoint to a file named my-insert-breakpoint:

(gdb) save breakpoints my-insert-breakpoint
Saved to file 'my-insert-breakpoint'.

The my-insert-breakpoint file now contains GDB commands that, when run, will recreate the insert() breakpoint plus its associated commands for use in a future GDB session:

break tree.c:insert
  commands
    silent
    call (void) printf("Entering insert(tree=%lx, data=%s)\n", tree, data)
    if (tree != 0)
      call (void) printf("Tree is...\n")
      call (void) print_tree(tree)
    end
    continue
  end

Running a program with insert and dprintf breakpoints

I now have two files with saved breakpoints, one named my-dprintf-breakpoints and the other named my-insert-breakpoint. Let's start GDB, load the dprintf and breakpoint commands listed in the files, and then run the program with output redirected to my-program-output:

$ gdb -q ./tree
Reading symbols from ./tree...
(gdb) source my-dprintf-breakpoints
Dprintf 1 at 0x401281: file tree.c, line 41.
Dprintf 2 at 0x4012b9: file tree.c, line 47.
Dprintf 3 at 0x4012de: file tree.c, line 49.
(gdb) source my-insert-breakpoint
Breakpoint 4 at 0x40127a: file tree.c, line 40.
(gdb) set dprintf-style call
(gdb) run >my-program-output
Starting program: /home/kev/ctests/tree >my-program-output
[Inferior 1 (process 327130) exited normally]
(gdb) quit

Note that the set dprintf-style call command had not been automatically added to either of the files loaded via the source command. It might make sense to manually add it to the my-dprintf-breakpoints file. Alternately, it could be placed into another file—let's call it tree-debugging-commands:

file tree
source my-dprintf-breakpoints
source my-insert-breakpoint
set dprintf-style call

This file, tree-debugging-commands, first specifies the program to debug via the file command. In earlier examples, we caused tree to be loaded by mentioning it on the gdb command line; here, however, we don't list it on the command line, but instead cause it to be loaded via the file command.

The remaining commands in tree-debugging-commands should be familiar by now. Commands contained in the my-dprintf-breakpoints and my-insert-breakpoints files are executed, followed by the set dprintf-style call command. Recall that this command causes dprintf breakpoints to call printf() in the program being debugged (instead of using GDB's internal printf command).

With that file in place, we can run GDB as follows:

$ gdb -q -x tree-debugging-commands
Dprintf 1 at 0x401281: file tree.c, line 41.
Dprintf 2 at 0x4012b9: file tree.c, line 47.
Dprintf 3 at 0x4012de: file tree.c, line 49.
Breakpoint 4 at 0x40127a: file tree.c, line 40.
(gdb) run >my-program-output
Starting program: /home/kev/ctests/tree >my-program-output
[Inferior 1 (process 351102) exited normally]
(gdb) quit

Additional commands

It should also be possible to achieve the same effect, but without needing to interact with GDB, by using the following command:

$ gdb -q -x tree-debugging-commands -ex 'run >my-program-output' -ex quit

As noted in Part 1 of this series, the -q option suppresses the GDB banner, copyright, and help information when GDB starts up. The -x option, in this case, causes GDB to load and execute the commands from the file tree-debugging-commands. The -ex options cause the command following the option to be run. So, in this case, after loading and running the commands in tree-debugging-commands, a run command is issued from the first -ex option; moreover, the output from the run is redirected to the file my-program-output. The command following the second -ex option is quit; this causes GDB to quit without ever showing a prompt.

The run and quit commands could also be placed in the command file, tree-debugging-commands. If this were done, the command line would be shortened to look like this:

$ gdb -q -x tree-debugging-commands

A bugfix for dprintf breakpoints

While writing this article, I discovered a bug in GDB that caused dprintf breakpoints to be (essentially) disabled when running a program from the command line or from within a GDB script. This bug has been fixed in the upstream GDB sources. On Fedora Linux, gdb-11.1-5 (and later) contain this fix. If you are using a version of GDB without this fix, you will need to issue the run command from the GDB prompt.

Go further with GDB

I hope this series has been useful to developers familiar with debugging their code using print statements, but who previously had little or no familiarity with GDB. I also hope that it whets your appetite for doing more with GDB.

This article demonstrated how to set a breakpoint and run until it is hit, a very common use of GDB. Once GDB is stopped at a breakpoint, you can enter a variety of GDB commands to reveal more about the state of the program at that point. If you want to go further with GDB commands, I recommend the following:

Last updated: June 14, 2023