TRT
Sign in

Foundations · Module 1 · Lesson 7 of 9

Stepping Through a Program With a Debugger

Build a program for debugging, pause it at a breakpoint, step through it line by line and inspect variables, and learn how a debugger stops a running program without changing your source.

FreeAbout 8 minutes

By the end you can

  • Build a program so that a debugger can show source lines and variable names
  • Choose between step over, step into, step out and continue for a given situation
  • Explain how a breakpoint stops a running program
  • Use a debugger to find where a computed value first becomes wrong

Builds on Variables Are Names for Bytes, Reading Compiler Errors and Warnings.

01 · A question first

Your program compiles without a single warning and prints 7 when the answer should be 7.5. You could add print statements, rebuild and rerun again and again. Or you could freeze the program mid-run, ask it what its variables hold, and watch it go line by line. How does a debugger stop a running program at exactly the line you choose?

Make a guess, then step through the trace below to check it.

02 · Watch it run

Finding a wrong averagestep 1 of 5
int total = 7 + 8;int count = 2;double avg = total / count;std::cout << avg << "\n";
  1. var 0total: ?
  2. var 1count: ?
  3. var 2avg: ?

The program prints 7 but we expected 7.5. Instead of guessing, we run it under a debugger with a breakpoint on line 1. The debugger stops the program before that line runs. The three cells show total, count and avg, which hold leftover bytes for now.

Machine work: debugger: break at line 1, then run; the program is paused before line 1

03 · The idea

Start with the problem

A program compiles with no warnings and prints the wrong answer:

cpp
#include <iostream>
 
int main() {
    int total = 7 + 8;
    int count = 2;
    double avg = total / count;
    std::cout << avg << "\n";
}

It prints 7, and the answer should be 7.5. You could add std::cout lines, rebuild and rerun, but each experiment costs an edit-compile-run cycle, and you only see what you thought to print in advance. A debugger works the other way around. It pauses the running program, lets you read any variable, and walks forward one line at a time, so you watch what happens instead of guessing.

How the idea works

Build for debugging. Compile with debug information and without optimization:

bash
g++ -std=c++20 -Wall -Wextra -g -O0 main.cpp -o main

The -g flag records a table that maps machine-code addresses back to source lines, and memory locations back to variable names and types. This is the table lesson 3 meant when it said that names survive only as debug information. The -O0 flag turns the optimizer off, so the machine code stays in step with the source.

Run under a debugger. Use gdb ./main on Linux, lldb ./main on macOS, or the Debug button of an IDE such as VS Code, CLion or Visual Studio. The commands below are gdb's, and IDEs show the same operations as buttons:

  • break 6 or break main sets a breakpoint: pause before that line runs.
  • run starts the program, which stops at the first breakpoint.
  • next is step over: run the current line completely, including any calls on it, and stop at the next line.
  • step is step into: if the line calls a function, enter that function.
  • finish is step out: run until the current function returns.
  • continue runs until the next breakpoint or the end of the program.
  • print total / count evaluates an expression using the program's own types and current values, and info locals lists every local variable.
  • backtrace shows the call stack, the chain of functions that led to the current line.
  • watch total sets a watchpoint: stop whenever that variable changes.
  • x/4xb &total examines memory: four bytes in hexadecimal, starting at the address of total.

The session in the trace. The trace above debugs the four lines inside main. Each step highlights the line that is about to run, not the line that ran before it. Steps 2 and 3 use next to execute the two declarations. Step 4 is the one that matters: before line 3 runs, the debugger is asked to evaluate total / count and answers 7. The bug is visible without running the line, because both operands are int, so the division drops the fraction before the result is stored in the double. A real session looks like this:

text
(gdb) break 6
(gdb) run
(gdb) print total / count
$1 = 7
(gdb) x/4xb &total
0x7ffc2b3a9e5c:	0x0f	0x00	0x00	0x00

The last command connects back to lesson 3: 15 is 0x0f, stored as the four bytes 0f 00 00 00.

Why it is correct

A breakpoint needs no cooperation from your program. The debugger takes control of your process through the operating system, looks up the address of the chosen line in the debug table, and overwrites the first byte of the instruction there with a one-byte trap instruction, which is int3, byte 0xCC, on x86-64. When the CPU reaches it, it traps into the operating system, which stops your program and notifies the debugger. To resume, the debugger puts the original byte back, executes that one instruction, re-inserts the trap and lets the program continue. The breakpoint never existed in your source, which is why you can add and remove breakpoints without rebuilding.

Stepping with next and step works by running to the next line boundary in the table. Watchpoints use debug registers inside the CPU, typically four on x86, that trap when a chosen address is written, so they cost almost nothing while the program runs. And print follows the same C++ rules as compiled code, which is why it reports 7 for total / count: the debugger is not guessing what you meant, it is telling you what the language does.

What it costs

Slower, larger debug builds. Without optimization, code commonly runs several times slower than an -O2 build. Debug information can make the file much larger, though it is not loaded into memory when the program runs normally.

Changed timing. Pausing a program changes when things happen. Bugs that depend on timing, such as races between threads, may vanish under a debugger, and a network program may time out while you stand at a breakpoint.

Your time. For a one-value question, a print statement is quicker. As soon as you add a second print statement, or need to see many variables or the call stack, the debugger wins, because asking a new question never requires an edit, a rebuild and a rerun.

Pitfalls and variations

Debugging an optimized build. With -O2 the debugger may show optimized out instead of a value, and execution jumps around the source because the compiler reordered and merged code. Use -O0, or -Og, which is GCC's debug-friendly optimization level.

Stepping into the library. Using step on a line such as std::cout << avg can take you into the standard library. Use next for lines you trust, and finish to climb back out.

Reading the wrong line. The highlighted line has not run yet, so the variables you inspect show the state before it.

Garbage before initialization. Before line 1 runs, the variables hold leftover bytes, the ? cells in the trace. Do not mistake them for results.

Variations. Conditional breakpoints, such as break 20 if i == 99, pause only when a condition is true. A crashed program can leave a core dump, and gdb ./main core then shows its state at the crash. Sanitizers, such as -fsanitize=address, catch memory errors without stepping, and a later module covers them. VS Code, CLion and Visual Studio wrap all of this in a graphical interface.

Where it is used

Debuggers sit behind every IDE's Debug button, and in production they are how engineers read crash dumps: the first thing to look at after a crash is the backtrace. Later modules lean on this skill when pointers, memory and object lifetimes enter the picture, because a debugger shows addresses and bytes directly, exactly as in lesson 3.

That completes the first module. You can now build a program, read its source, understand how its values sit in memory, take input safely, read compiler messages, and step through a running program. The next module builds on this with decisions, loops and functions.

04 · Prove it

Question 1 of 4 · answer from memory

Which build gives the best experience when you want to step through a program in a debugger?

05 · Practice

Something wrong with this lesson? Report a problem

Not started

Sign in to save your learning progress.

Sign in to save