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:
- The GDB developer's GNU Debugger tutorial, Part 1 is a general guide to debugging with the GNU Debugger.
- Debugging with GDB, the GDB manual, is the authoritative reference for using GDB.