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.
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\000'
c = 0 ' 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.
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\000'
c = 0 ' 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.
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
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:
$11 - 5
(gdb) print $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.
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:
By the way, you can exit the debugger by hitting Ctrl-D
a few times.↩︎
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.↩︎
You may see different results! What is in the variables is just whatever was leftover in memory.↩︎
Pro tip: you can repeat the most recent gdb
command just by hitting return.↩︎
This is why printing an assignment statement actually changes variable contents. Note also that even assignment statements have a type and value!↩︎
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.↩︎