Programmers often debug software by adding print statements to source code. Knowing that a certain point in the program has been reached can be immensely helpful. It's also useful to print values of variables at various points during program execution. An obvious drawback of this technique is the need to change source code, both to add the print statements and later to remove or disable them after the bug has been fixed. Adding new code can potentially introduce new bugs, and if you've added many print statements, you might forget to remove some of them when cleaning up after debugging.
You can use the popular GNU Project Debugger (GDB) to perform the same style of debugging for various programming languages, especially C and C++, without changing source files. This article is the first of a series describing how to use GDB to add print statements to your C and C++ code. We'll start with some basics and move through more advanced ways to call program-defined functions that display data.
Prerequisites
To use the techniques described in this article, you need to satisfy the following prerequisites:
- You must have a C/C++ compiler such as GCC or Clang installed on your development machine.
- Likewise, you will need GDB installed on your development machine.
- The program that you wish to debug needs to be compiled with debugging information. When using either the
gcc
orclang
command, add the-g
option to enable debugging information. - The program—or at least the source files on which you wish to use GDB's
dprintf
command—should be compiled without optimization.
Regarding the last point, you can disable optimization either by removing any existing -O
, -O2
, etc., options from the set of compiler flags (for example, CFLAGS
or CXXFLAGS
) or by adding either -O0
or -Og
as the very last optimization option. When you run gcc
without specifying any optimization options, -O0
is used as the default. When the program is being compiled with a lot of options, it may be easier to simply append either -O0
or -Og
to the list of compiler options, because the final optimization option overrides any previous optimization options. The -O0
option is slightly different from the -Og
option, because the latter enables some optimizations that won't affect the debugging experience whereas -O0
disables all optimizations.
Note, too, that the latter two points might require you to recompile and relink your program with a debugging option enabled and with optimization disabled.
The techniques presented here might also work for some optimized code. In recent years, when gcc
is used, the quality of the debugging information has improved greatly for optimized code; however, there are still cases where values of variables are unavailable or possibly even incorrect. Programmers can avoid these problems by disabling optimization.
Example code
This article demonstrates the use of GDB to add printf-style output for a little function named insert
. This function is from a small program that I wrote for pedagogical purposes. The program, which is a little over 100 lines long, is contained in a single source file named tree.c
that is available from my GitHub repository.
The following insert
function inserts a node into a binary search tree. Note the line numbers, which we'll use later in the article:
37 struct node *
38 insert (struct node *tree, char *data)
39 {
40 if (tree == NULL)
41 return alloc_node (NULL, NULL, data);
42 else
43 {
44 int cmp = strcmp (tree->data, data);
45
46 if (cmp > 0)
47 tree->left = insert (tree->left, data);
48 else if (cmp < 0)
49 tree->right = insert (tree->right, data);
50 return tree;
51 }
52 }
The main
function contains calls to insert
plus additional calls to functions for printing the tree. Note the line numbers here:
96 struct node *tree = NULL;
97
98 tree = insert (tree, "dog");
99 tree = insert (tree, "cat");
100 tree = insert (tree, "wolf");
101 tree = insert (tree, "javelina");
102 tree = insert (tree, "gecko");
103 tree = insert (tree, "coyote");
104 tree = insert (tree, "scorpion");
105
106 print_tree_flat (tree);
107 printf ("\n");
108 print_tree (tree);
I compiled tree.c
for use with GDB using the following command:
$ gcc -o tree -g tree.c
The -g
option places debugging information in the binary. Also, the program is compiled without optimization.
Using GDB for printf-style output
With the properly compiled binary on your system, you can simulate print statements in GDB.
Debugging with GDB
We can use the gdb
command to debug the example program:
$ gdb ./tree
This command starts by printing a copyright message along with legal and help information. If you wish to silence that output, add the -q
option to the gdb
command line. This is what output should look like when using the -q
option:
$ gdb -q ./tree
Reading symbols from ./tree...
(gdb)
If you also see the message (No debugging symbols found in ./tree)
, it means that you did not enable the generation of debugging information during the compilation and linking of the program. If this is the case, use GDB's quit
command to exit GDB and fix the problem by recompiling with the -g
option.
Virtual print statements
We'll now use GDB's dprintf command to place a special kind of breakpoint that simulates the addition of a comparable printf()
statement to the source code. We'll place virtual print statements on lines 41, 47, and 49:
(gdb) dprintf 41,"Allocating node for data=%s\n", data
Dprintf 1 at 0x401281: file tree.c, line 41.
(gdb) dprintf 47,"Recursing left for %s at node %s\n", data, tree->data
Dprintf 2 at 0x4012b9: file tree.c, line 47.
(gdb) dprintf 49,"Recursing right for %s at node %s\n", data, tree->data
Dprintf 3 at 0x4012de: file tree.c, line 49.
(gdb)
The first dprintf
command shown for line 41
is roughly equivalent to adding three lines of code near lines 40 and 41:
if (tree == NULL)
{ /* DEBUG - delete later. */
printf ("Allocating node for data=%s\n", data); /* DEBUG - delete later. */
return alloc_node (NULL, NULL, data);
} /* DEBUG - delete later. */
Note that, when adding a call to printf()
in the traditional fashion, three lines of code would need to be added in this particular place. (If you added the printf()
without the curly braces, the if
statement would execute only the printf()
, and the return alloc_node
statement would no longer be conditionally executed—instead, it would always be executed.)
As indicated by the comments, you would need to delete these added lines later when debugging is done (although the added braces are actually fine to leave in place). If you add lots of debugging statements to your code, you might forget to delete some of them when debugging is complete. As noted earlier, this is a distinct advantage of using GDB's dprintf
command: No source code is modified, so subtle bugs won't be introduced when adding the print statement; there's also no need to remember all the places where a print statement was added when cleaning up after debugging.
Run the program
Use GDB's run
command to run your program. Once the command is issued, GDB output and program output appear mixed together in the terminal used for the GDB session. Here's an example running our tree program:
(gdb) run
Starting program: /home/kev/ctests/tree
Allocating node for data=dog
Recursing left for cat at node dog
Allocating node for data=cat
Recursing right for wolf at node dog
Allocating node for data=wolf
Recursing right for javelina at node dog
Recursing left for javelina at node wolf
Allocating node for data=javelina
Recursing right for gecko at node dog
Recursing left for gecko at node wolf
Recursing left for gecko at node javelina
Allocating node for data=gecko
Recursing left for coyote at node dog
Recursing right for coyote at node cat
Allocating node for data=coyote
Recursing right for scorpion at node dog
Recursing left for scorpion at node wolf
Recursing right for scorpion at node javelina
Allocating node for data=scorpion
cat coyote dog gecko javelina scorpion wolf
cat
coyote
dog
gecko
javelina
scorpion
wolf
[Inferior 1 (process 306927) exited normally]
(gdb)
In this display, the user typed the run
command at the (gdb)
prompt. The rest of the lines are output either from GDB or from the program. The only program output occurs towards the end, starting with the line "cat coyote dog..." and finishing with the line "wolf." Lines starting with either "Recursing" or "Allocating" were output by the dprintf
commands established earlier. It's important to understand that, by default, these lines were output by GDB. This is different from traditional printf-style debugging, and we'll look at this difference in the next article in this series. Finally, there are two lines of GDB output, the second line and the penultimate one, which show that the program is starting and exiting.
Comparing dprintf and printf()
There are differences and similarities between GDB's dprintf
command and the C-language printf()
function:
- The
dprintf
command does not use parentheses to group the command's arguments. - The first argument of the
dprintf
command specifies a source location at which a dynamicprintf
statement should be placed. Output from the dynamicprintf
is printed prior to the execution of that source location. The source location may be a line number, such as41
, but the location will often include a filename plus a line number, such astree.c:41
. The location could also be the name of a function or an instruction address in the program. For a function location, the output from the dynamicprintf
occurs prior to the first executable line of the function. When the location is an instruction address, output occurs before the instruction at that address is executed. - The
dprintf
command creates a special kind of breakpoint. It is only when one of these special breakpoints is hit during the program run that output is printed. - The format string used by
dprintf
is the same as that used byprintf()
. In fact, as we shall see later, the format string specified in thedprintf
command might be passed to a dynamically constructed call toprintf()
in the program being debugged. - In both
dprintf
andprintf()
, comma-separated expressions follow the format string. These are evaluated and output according to the specification provided by the format string.
Conclusion
This article has offered the basics of printf-style debugging in GDB. The next article in this series takes you to a higher level of control over debugging, by showing you how to save your dprintf
commands and the GDB output for later use.