2020 December 20,
A debugger is a program that lets you examine the state of a program while it is running. As the name suggests, a debugger can greatly accelerate your process of diagnosing and fixing errors.
This page explains the basics of using a debugger for C programs. It simultaneously treats Clang/LLVM/LLDB (which students in macOS are probably using) and GCC/GDB (which students in Ubuntu Linux are probably using). Many features go unmentioned. To learn more, search the Web for bigger tutorials.
First, you need to compile your program with the -g flag (in addition to any other flags and linking that you need). This flag causes the compiler to embed a bunch of debugging information into the compiled code. As always, the compiler gives you an executable file, which by default is a.out. Then you run LLDB or GDB on this executable file. Now you're in the debugger. To quit the debugger, enter quit.
Clang/LLVM/LLDB | GCC/GDB | |
compile ordinarily | clang example.c | cc example.c |
compile for debugging | clang -g example.c | cc -g example.c |
run ordinarily | ./a.out | ./a.out |
run in debugger | lldb a.out | gdb a.out |
quit debugger | quit | quit |
The remaining sections of this tutorial explain some of the things that you can do while you're in the debugger.
A break point is a place in your code, where you'd like to pause execution and examine the variables. Usually, the first thing you do in a debugger is set some break points. You can set a break point on a particular line of your C file, or at the start of a particular function. By default, the break point is set in your main file, where your main function is. You can also set break points in other files by specifying them explicitly. You can also delete break points that you don't want anymore.
LLDB | GDB | |
set break point at line 13 | b 13 | break 13 |
...in file helper.c | b helper.c:13 | break helper.c:13 |
set break point at function foo | b foo | break foo |
...in file helper.c | b helper.c:foo | break helper.c:foo |
list all current break points | br list | info break |
delete break point #6 | br del 6 | delete 6 |
delete all break points | br del |
Once you have some break points set up, run the program by entering run. It runs until it hits the first break point. Then it pauses. Enter continue to run to the next break point and pause there. Enter continue again to run to the next break point.
Maybe you'd like finer control. Instead of pausing at the next break point, maybe you'd like to pause at the next line of code. You can take such tiny steps by entering next or step. To put it vaguely, step pauses more often than next, which pauses more often than continue.
LLDB | GDB | |
start running | run | run |
run to the next break point | continue | continue |
step (over) to the next line | next | next |
step (into) to the next line | step | step |
To actually understand the distinction between step and next, imagine that you're stepping through a function foo, and you come to a line where it calls another function bar. Do you want to step into bar and go through that function line-by-line? Then use step. Or do you want to regard bar as a single, indivisible step, step over it, and stay in foo? Then use next.
Thus far, we haven't done anything useful. We've just practiced a complicated way of making a program run slowly. The power of the debugger is that it lets you examine the state of the program while it is paused. Here are some options.
LLDB | GDB | |
show backtrace (call stack) | bt | bt |
show arguments to function | frame variable | info args |
show other local variables | frame variable | info locals |
print value of variable baz | p baz | p baz |
set value of variable baz to 5 | expr baz = 5 | set var baz=5 |
(You can also alter your break points while the program is paused.)