Featured image for C-language topics.

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 or clang 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 dynamic printf statement should be placed. Output from the dynamic printf is printed prior to the execution of that source location. The source location may be a line number, such as 41, but the location will often include a filename plus a line number, such as tree.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 dynamic printf 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 by printf(). In fact, as we shall see later, the format string specified in the dprintf command might be passed to a dynamically constructed call to printf() in the program being debugged.
  • In both dprintf and printf(), 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.

Last updated: June 7, 2023