Debugging with GDB
2022-02-27
My first programming language was C++ and for a long time, I was using Visual Studio Express in Windows. When I had to debug something, I would click on the line number to set a breakpoint and press F5. Maybe I'd add some variables to the watch window, and press some F-keys a couple of times. Recently, I switched jobs and I'm now mostly developing firmware in C. Since I switched to using Linux and Vim ~7 years ago, I needed to update my debugging workflow. I actually didn't find a neat tutorial that covers the whole picture, so I figured I could write one myself.
During the last 5 years, I mostly worked with Python, and I really like
debugging in Python. You'd just put a breakpoint()
function call into your
code somewhere and repl into the program. I wanted to see how close I get to
that in C. As C is not an interpreted language, my hopes weren't that high, but
it turns out debugging in C is quite comfortable. As a bonus, you can use GDB
not only to debug C and C++, but also Rust, Nim, and probably any language that
respects the C-ABI (althoug I don't know this for a fact).
First, we need a program to debug. Let's take this one:
// program.c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
void print_product(int a, int b) {
int product = a * b;
printf("Product: %d\n", product);
}
void print_arguments(int argc, char **argv) {
for (int i = 1; i < argc; i++) {
printf("%s ", argv[i]);
}
printf("\n");
}
int main(int argc, char **argv) {
print_arguments(argc, argv);
srand(time(NULL));
for (int i = 0; i < 10; i++) {
print_product(rand(), rand());
}
return 0;
}
To debug the program, it's important that it contains debugging symbols. To
add debugging symbols when compiling with gcc add the -g
flag. So let's
compile this program:
gcc -g -o program program.c
We can now debug this program by running:
gdb program
this will launch us into a
repl. To
run the program until it hits the main function, type start
. Now you have the
typical debugger actions that you can execute by typing them into the repl. You
can go to the next line by entering n
(of course you have to press return
too), s
to step into the current function call, r
to run until you hit a
breakpoint, and
some more. To
set a breakpoint, type b <line>
where <line>
is the line number you want
it in. If your project consists of multiple files, you'll have to type
b <filename>:<line>
however, <filename>
does not need to be a complete path,
just the name is enough. Btw, gdb allows for tab completion. Alternatively you can
set a breakpoint to a function like this b <functionname>
.
To see the context of a variable, we can use p
like p product
if we break
at line 6. But we can also evaluate nearly arbitrary c expressions e.g. p
product * 5
.
This is still not nice though, now we'd have to enter all breakpoints over and over again, when we restart the debugging session. But there is a way out of this: gdb scripts. If you invoke gdb with the -x option, you can pass it a file containing gdb commands, that it should execute.
gdb -x gdbscript program
we can now write our breakpoints into the script, e.g.
# gdbscript
b 6
b 11
r
And now our breakpoints are stored. But there is more to it. Typically I want to check some variables, and I'm not very interested in running the program further than that. Let's say, we wanted to see the value of the product variable, for all loops, and dont care about the rest:
# gdbscript
b 6
commands
p product
c
end
run
and then run gdb with --batch
like this:
gdb -x gdbscript program --batch
Now gdb will quickly print out the value of product
for all iterations, and
exit again. A commands
statement after a breakpoint will execute all
contained commands once a breakpoint is hit. The last command, the c
means
continue
and will resume the program flow until the next breakpoint is hit,
once the breakpoint was hit the last time, gdb will end execution thanks to the
--batch
parameter.
Another cool example: let's say, for whatever reason, we are interested in the
value of product
only during the third call of print_product()
. (Which is
of course silly, since the program prints it anyway, but in real world
debugging, a situation like this might arise)
set $n = 0
b 6
commands
set $n = $n + 1
if $n == 3
printf "Product is: %d\n", product
else
c
end
end
run
As you can see we define a variable $n
and use it to count up, and print our
required information in the third round. After the third round, gdb quits
automatically thanks to --batch
. For a more complete tour through gdb's
scripting capabilities, I recommend the tutorial on adacore.com:
part 1 and
part 2.
There is a script, that gdb will always implicitly load if it exists: ~/.gdbinit You can put functions you use often in there, or configuration. Mine looks like this:
set width 0
set height 0
set print pretty on
set confirm off
Setting width and height to 0, will prevent gdb from paging long outputs, the third line makes gdb prettify the output, and the last line prevents it from asking if we really want to quit.
This is largely what I have. In general, I'm quite happy with the power and flexibility. It might be a little more work than placing a breakpoint in an IDE, but you get so much more out of it. Also, in IDEs I always found it annoying to call the debugger with arguments, because you'd have to find the right options dialog. Here you can simply append them to the debugger invocation:
gdb -x gdbscript --batch --args program --flags and some args
Also, if you require a more complex setup for debugging, e.g. because you have to start other programs too, you can put all of that together in a bash script.
Of course, there is much more to know about gdb, and this is barely an introduction.