Lesson 2 of 6
Printing values works until the program is large, the bug is intermittent, or you do not yet know where to print. A debugger lets you stop the program and look around.
Build with -g
g++ -std=c++20 -Wall -Wextra -g -O0 main.cpp -o main
-g includes debug information: the mapping from machine code back to your source lines and
variable names. Without it the debugger shows addresses and nothing else.
-O0 disables optimisation. At -O2 the compiler reorders and merges code, so stepping jumps
around unpredictably and variables report <optimized out>. Debug at -O0.
The program we will debug
#include <iostream>
int sum_range(const int* values, int count) {
int total = 0;
for (int i = 0; i <= count; ++i) { // BUG: <= reads one past the end
total += values[i];
}
return total;
}
int main() {
int* data = new int[5];
for (int i = 0; i < 5; ++i) {
data[i] = i + 1;
}
std::cout << "sum: " << sum_range(data, 5) << "\n";
delete[] data;
return 0;
}
Before reading on, note what this program prints when you run it normally: sum: 15. That is the
correct answer for 1+2+3+4+5, produced by a program that reads past the end of its array. The
sixth element happened to hold 0.
This is lesson 8.7's point made concrete: the output tells you nothing about correctness.
A real session
Start the debugger, set a breakpoint, and run:
$ gdb ./buggy
(gdb) break sum_range
Breakpoint 1 at 0x11b8: file buggy.cpp, line 4.
(gdb) run
Breakpoint 1, sum_range (values=0x55555556b2b0, count=5) at buggy.cpp:4
4 int total = 0;
(gdb) info args
values = 0x55555556b2b0
count = 5
(gdb) print values[0]
$1 = 1
(gdb) next
5 for (int i = 0; i <= count; ++i) {
(gdb) next
6 total += values[i];
(gdb) next
5 for (int i = 0; i <= count; ++i) {
(gdb) print total
$2 = 1
(gdb) backtrace
#0 sum_range (values=0x55555556b2b0, count=5) at buggy.cpp:5
#1 0x0000555555555269 in main () at buggy.cpp:17
Read what that told us. Execution stopped at the top of sum_range with count = 5. After one
pass through the body, total is 1 — the first element. And backtrace shows we got here from
main at line 17.
The addresses will differ on your machine (lesson 7.1), but everything else will match.
The commands worth memorising
| Command | Short | Does |
|---|---|---|
| break sum_range | b | stop when that function is entered |
| break buggy.cpp:6 | b | stop at that line |
| run | r | start the program |
| next | n | run one line, stepping over calls |
| step | s | run one line, stepping into calls |
| continue | c | run until the next breakpoint |
| finish | | run until the current function returns |
| print expr | p | evaluate and show an expression |
| info args | | the current function's arguments |
| info locals | | all local variables |
| backtrace | bt | the call stack |
| quit | q | leave |
next versus step is the one people get wrong. next treats a function call as a single line;
step goes inside it. Use next until you reach the call you actually suspect.
Conditional breakpoints
Stopping on every iteration of a loop that runs a thousand times is useless. Stop only when it matters:
(gdb) break buggy.cpp:6 if i == 5
(gdb) run
Breakpoint 1, sum_range (...) at buggy.cpp:6
6 total += values[i];
(gdb) print i
$1 = 5
(gdb) print values[5]
$2 = 0
There it is. i reached 5, which is one past the last valid index of a five-element array, and
the value read happened to be 0 — which is why the wrong program printed the right answer.
Conditional breakpoints are what make a debugger better than print statements. You cannot easily
printf only on the iteration that matters.
Watchpoints
A watchpoint stops when a variable changes, wherever that happens:
(gdb) watch total
Hardware watchpoint 2: total
(gdb) continue
Hardware watchpoint 2: total
Old value = 0
New value = 1
Use this when something is being modified and you cannot find the culprit by reading. It answers "who wrote to this?" directly.
Debugging a crash
For a program that crashes, you need no breakpoints at all:
$ gdb ./crasher
(gdb) run
Program received signal SIGSEGV, Segmentation fault.
0x... in process (p=0x0) at crash.cpp:5
5 return *p;
(gdb) backtrace
#0 process (p=0x0) at crash.cpp:5
#1 0x... in main () at crash.cpp:10
(gdb) print p
$1 = (int *) 0x0
run, wait for the crash, then backtrace. The stack shows the path in and print shows the bad
value — p was null. Lesson 11.5 covers reading these traces in detail.
LLDB
On macOS you will more often have LLDB, which does the same job with slightly different spelling:
| GDB | LLDB |
|---|---|
| break f | breakpoint set --name f or b f |
| run | run or r |
| next | next or n |
| print x | print x or p x |
| backtrace | bt |
The short forms are nearly identical, so the habits transfer.
When to reach for it
A debugger is not always the fastest tool. Use it when:
- You do not know where the problem is. A breakpoint plus
backtracebeats scattering prints. - The state is complex. Inspecting a whole struct beats printing fields one at a time.
- The bug is conditional. A conditional breakpoint beats an
ifaround a print. - The program crashes.
runthenbacktraceis the fastest route to the line.
For a small function where you already suspect the answer, a print statement really is faster. Use both.
Try it
Take the sum_range program, build it with -g, and use GDB to find the off-by-one without
reading the source for the answer. Set a conditional breakpoint on the last iteration and
inspect i, count, and values[i].
Checkpoint
- Build with
-gfor debug symbols and-O0so stepping is predictable. break,run,next,step,print,backtracecover most sessions.nextsteps over calls;stepgoes into them.- Conditional breakpoints (
break file:line if cond) are what prints cannot do. watch xstops whenever a variable changes.- For a crash, just
runand thenbacktrace. - A program can print the right answer while doing something undefined.
Questions about this lesson
Log in to ask a question.