Debugging (and more) with gdb

Introduction + setting things up

Debuggers are tools for inspecting programs as they run, and as such can be an invaluable tool for revealing logical errors and misunderstandings of the language. It’s pretty easy to get started with gdb, so let’s do it already!

First, we need a program to inspect. The following nonsense program that just declares a few variables and gives them values will suffice.

int main()
{
    /* declare a few variables of different types: */
    int x;
    char c;
    double y;

    /* give them some values: */
    x = 77;
    c = 'h';
    y = 99.99;

    return 0;
}

Save the above in a file named test.cpp. As you might imagine, the conversion from C++ source to those tiny little machine instructions we talked about might not be very reversible. In particular, there might not be a natural way to figure out which C++ statements correspond to which machine instructions. But the compiler ought to know, at least during compilation, right? And indeed it does. But we have to request this information to be generated and stored. Here’s how to do it in gcc:

$ g++ -g test.cpp

Note the -g flag. Once you’ve run the above command, your executable a.out will be equipped with debugging symbols so we can make the best use of gdb. Start gdb with the following command:1

$ gdb a.out

And you’ll see something like this:

$ gdb a.out
GNU gdb (GDB) 7.12
Copyright (C) 2016 Free Software Foundation, Inc.
...
Reading symbols from a.out...done.
+(gdb)

(If we hadn’t compiled with -g, we’d see a message about “no debugging symbols found” in that last line, and none of what follows would work very well.)

The first thing we’ll do is to set a breakpoint. This is just a place (e.g. a line of code) where the debugger will pause execution and let you look around. Let’s set one for the main() function, which is more or less where our program will begin:

+(gdb) break main
Breakpoint 1 at 0x40061a: file test.cpp, line 8.

Cool. We can now run the program and it will stop when it gets to main. Try it out:

+(gdb) run
Starting program: a.out

Breakpoint 1, main () at test.cpp:8
8               x = 77;

At this point, you may already find yourself becoming disenchanted by the austerity of gdb. But don’t despair! We can make it a little more friendly. Before we get too far, let’s turn on the “text user interface” (a.k.a. the “tui”) which is a nice visual aid that shows relevant pieces of your C++ source as the program runs:

+(gdb) tui enable

Ha! You should now have a picture of your program at the top of the screen, and you’ll note that the line where the program is frozen is marked with a > on the left. The B+ shows you that there’s also a breakpoint there.2 Alright – now that we’ve covered the essential practical matters, let’s see what gdb can do for us.

Examining variable contents

You can examine the contents of your variables in a variety of ways. First, you can just print them using their names:

(gdb) print x
$1 = 0
(gdb) print c
$2 = 0 '\000'
(gdb) print y
$3 = 6.9533558073749304e-310

Note that since the program is paused before the assignment statements, we have some random stuff left in our variables.3 You can also check all local variables at once with one quick command:

(gdb) info locals
x = 0
c = 0 '\000'
y = 6.9533558073749304e-310

NOTE: you can also set variable contents, either via the set var command, e.g., set var x=23 or by printing an assignment statement (which evaluates it and delivers its side-effects). See the documentation for more.

Stepping through code

To advance your program one statement, use the step command:

(gdb) step

You’ll now see the arrow (>) has moved to the next line. And if we print the contents of our variables, we’ll see x is now 77:

(gdb) info locals
x = 77
c = 0 '\000'
y = 6.9533558073749304e-310

Let’s execute a few more lines:4

(gdb) step
(gdb) step
(gdb) info locals
x = 77
c = 104 'h'
y = 99.989999999999995

Not much to it, but this can be far more convenient than a manual trace (that is, tracking the state of all your variables with cout statements).

NOTE: the step command will step into function calls which may not always be desirable. If you want to execute a line containing a call as a single unit, use the next command instead.

Examining types

One more feature I’d like to point out is gdb’s ability to report datatype information. The command is ptype, and can be used like so:

(gdb) ptype x
type = int
(gdb) ptype c
type = char
(gdb) ptype y
type = double

Nothing surprising. But you can also give it expressions! Observe:

(gdb) ptype (x+y)
type = double
(gdb) ptype (x+c)
type = int

Take careful note of the conversions that took place: adding an int to a double produced something of type double, while adding an int to a char gives another int. Perhaps you’ve already figured it out, but the general idea is to try to promote the result to the type that has more “information” or “precision”. Notice that the char datatype takes only one byte of storage, whereas int takes 4, and double requires 8:

(gdb) print sizeof(c)
$4 = 1
(gdb) print sizeof(x)
$5 = 4
(gdb) print sizeof(y)
$6 = 8

GDB as an interpreter

Normally when one would like to test out some behavior of C or C++, one must write and save a program, compile it, and then run it. For simple things, we can dodge a bit of this hassle using gdb. Remember what we learned above: gdb can handle expressions as we saw with the ptype command. Let’s try printing some slightly more interesting expressions than plain old variable names:

(gdb) print x*x
$7 = 5929
(gdb) print x+y
$8 = 176.99000000000001

As you no doubt anticipated, printing an expression causes the expression to be evaluated, and the evaluated result is what’s printed.5

Note the rounding error with floating point arithmetic… More on that later. For now, let’s explore one more possibly counterintuitive thing: division with mixed datatypes.

(gdb) print x/10
$9 = 7

Huh? Shouldn’t the answer be 7.7? Well, the literal constant 10 is of type int, and so is x. When dividing the two, we get back an int result.

(gdb) ptype 10
type = int
(gdb) ptype x/10
type = int

This closure property is nice, but as a consequence the fractional part of our division gets dropped. If we really wanted the fractional part, all we have to do is convert one of the two operands to a floating point type. Note that the literal 10.0 is not the same as 10—it is of type double:

(gdb) ptype 10.0
type = double
(gdb) print x/10.0
$10 = 7.7000000000000002

We can also use the typecast operator on x, like this:

(gdb) print (double)x/10
$11 = 7.7000000000000002

In case it isn’t clear, what’s going on is this: the expression (double)x has datatype double, and has a value of whatever was in x.6 When we divide that by the int constant 10, we get back a double result (recall how type promotion worked above).

One more thing: you may have noticed that each print statement gives you something of the form $N = ... for increasing values of N. You can use these later on in other expressions if you need them:

(gdb) print $11 - 5
$12 = 2.7000000000000002

Note: you can also invent new variables that are not part of your program, but this feature probably won’t come up too often.

Conclusion + further reading

To say our coverage here was brief is quite an understatement, but hopefully you at least know enough now to make good use of gdb. However, if you’re interested to learn more exciting details of gdb’s capabilities, you needn’t look any further than your own hard drive – you can access the full documentation with the command $ info gdb. The following tutorials might also be of interest:

In particular, maybe read the sections on breakpoints:


Back to the 103 homepage


  1. By the way, you can exit the debugger by hitting Ctrl-D a few times.↩︎

  2. You may be wondering why it stopped us at the line x = 77; instead of what looks like the first line int x;. This is because declarations of local variables don’t really make sense as a place to stop a program. This is all wrapped up in the call to main(), and will happen differently depending on the underlying architecture. This will make more sense later on when we study the run-time stack.↩︎

  3. You may see different results! What is in the variables is just whatever was leftover in memory.↩︎

  4. Pro tip: you can repeat the most recent gdb command just by hitting return.↩︎

  5. This is why printing an assignment statement actually changes variable contents. Note also that even assignment statements have a type and value!↩︎

  6. Note that it may not always be possible to exactly preserve the value, since the different datatypes have different representations in the hardware. E.g., for 64 bit integer types, a conversion to double may lose information about the value.↩︎