C++ • DEBUGGING AND TESTING

Using Debugger — Use a debugger (gdb/lldb or IDE) to step through code (intro)

Master interactive debugging tools to inspect program state, trace execution flow, and eliminate bugs efficiently.

Historical Context & Motivation

Before interactive debuggers existed, programmers relied on inserting print statements throughout their code to observe variable values and trace execution paths—a technique that remains surprisingly common today despite being labor-intensive and error-prone. As software systems grew in complexity during the latter half of the twentieth century, this approach became untenable for large codebases. The need for tools that could pause a running program, inspect its internal state, and allow a developer to step through execution one instruction at a time drove the creation of the modern interactive debugger. Understanding the evolution of these tools contextualizes why debuggers like GDB and LLDB are designed the way they are, and why mastering them is a non-negotiable skill for any serious C++ developer.

1947
The First 'Bug'
Grace Hopper's team documents an actual moth trapped in the Harvard Mark II relay, popularizing the term debugging. Early debugging was entirely manual: inspecting hardware, reading core dumps, and adding diagnostic output.
1968
Early Interactive Debuggers
Time-sharing systems like TOPS-10 introduce interactive debugging facilities. Programmers can now set breakpoints and inspect memory during execution, marking a paradigm shift from batch-mode debugging.
1986
GDB Released by GNU Project
Richard Stallman writes GDB (GNU Debugger) as part of the GNU project. It becomes the standard debugger for C and C++ on Unix-like systems, supporting breakpoints, watchpoints, and stack inspection.
2003
Visual Studio Debugger Matures
Microsoft's Visual Studio IDE integrates a highly polished graphical debugger with features like conditional breakpoints, data visualizers, and Edit and Continue, making GUI-based debugging mainstream for C++ developers on Windows.
2010
LLDB Emerges from LLVM
The LLDB debugger ships as part of the LLVM project, offering a modern architecture, superior expression evaluation using Clang, and first-class C++ support. It becomes the default debugger on macOS via Xcode.

Despite decades of innovation, the fundamental question a debugger answers has remained constant: "What is my program actually doing, and why does that differ from what I intended?" This lesson introduces the core workflow of using a debugger to methodically answer that question—replacing guesswork and scattered print statements with precise, interactive inspection of your program's runtime behavior.

Core Principles of Interactive Debugging

An interactive debugger operates by attaching to a running process (or launching one under its control) and leveraging operating system primitives—such as ptrace on Linux—to intercept and control execution. The debugger reads debug symbols embedded in your binary (generated by the -g compiler flag) to map machine instructions back to source lines, variable names, and type information. Without these symbols, the debugger can still operate at the assembly level, but the human experience degrades dramatically. The following foundational concepts underpin every debugger session you will ever run.

1

Breakpoints

A breakpoint is a marker that tells the debugger to pause execution when the program counter reaches a specific source line or function entry. The debugger typically replaces the instruction at that address with a trap instruction (e.g., int 3 on x86), transferring control to the debugger when hit.
2

Stepping (Step Over, Step Into, Step Out)

Stepping advances execution in controlled increments. Step Over executes the current line without descending into called functions. Step Into enters a function call. Step Out runs until the current function returns.
3

Inspecting State

While paused, you can examine variables, evaluate arbitrary expressions, inspect the call stack (the chain of function invocations leading to the current point), and even read raw memory addresses. This is the core value proposition of a debugger over print statements.
4

Watchpoints

A watchpoint (or data breakpoint) pauses execution whenever a specified memory location is read or written. This is invaluable for tracking down subtle state corruption bugs where you know what is changing but not where in the code it happens.
5

Debug Symbols & Compilation

Compile with g++ -g -O0 main.cpp to embed DWARF debug information and disable optimizations. The -O0 flag prevents the compiler from reordering or eliminating code, which would make stepping through source lines confusing and unpredictable.
✦ KEY TAKEAWAY
Think of a debugger as a DVR for your program's execution. Print statements are like trying to reconstruct a movie by taking random photos of the TV screen—you might catch the key moment, but you'll miss most of the story. A debugger lets you pause, rewind (via restart), slow-motion (step), and zoom in (inspect) on any frame of the program's execution, giving you a complete picture of what went wrong and why.

Visual Explanation: The Debugging Workflow

The following diagram illustrates the core loop of an interactive debugging session. Every debugger session—whether you are using GDB on the command line or the Visual Studio graphical interface—follows this fundamental cycle. You compile with debug symbols, launch under the debugger, set breakpoints at suspicious locations, run the program until a breakpoint is hit, inspect the state, and then either step through code to observe behavior or continue to the next breakpoint. The cycle repeats until you isolate the bug.

The debugging cycle: compile with debug symbols (step 1), launch under the debugger (step 2), set breakpoints (step 3), run (step 4), inspect state when paused (steps 5–6), and iterate until the bug is found (step 7). The dashed violet arrow represents the continue command, which resumes execution until the next breakpoint.

Notice that this workflow is inherently iterative. Rarely does a single breakpoint placement reveal the entire story; more often, initial inspection raises new hypotheses that you test by setting additional breakpoints or watchpoints. The efficiency of this cycle—relative to the edit-add-print-recompile-run loop—is what makes debugger proficiency so valuable. Each iteration through the debug loop costs seconds rather than minutes, and you never risk accidentally leaving diagnostic output in production code.

How Debuggers Work Under the Hood

Understanding the mechanism behind debuggers deepens your ability to use them effectively and helps you troubleshoot situations where the debugger behaves unexpectedly. At the system level, debuggers rely on a combination of OS process control primitives, hardware debug registers, and debug information formats (primarily DWARF on Unix systems, PDB on Windows). When you compile with -g, the compiler embeds metadata that maps each machine instruction address back to its corresponding source file, line number, column, and the names and types of variables in scope at that point.

Breakpoint Implementation

When you set a breakpoint at a source line, the debugger looks up the corresponding instruction address using the DWARF line table. It then saves the original byte at that address and replaces it with a software interrupt instruction (on x86-64, the single-byte 0xCC encoding of INT 3). When the CPU executes this instruction, it raises a SIGTRAP signal, which the OS delivers to the debugger process (the parent, via ptrace). The debugger then restores the original byte, adjusts the program counter, and presents you with the paused state.

Stepping Mechanism

Single-stepping leverages the CPU's trap flag (TF bit in the EFLAGS register on x86). When the debugger requests a single step, it sets this flag before resuming the inferior process. The CPU then executes exactly one instruction and immediately raises a debug exception, returning control to the debugger. For source-level stepping (as opposed to instruction-level), the debugger repeatedly single-steps at the machine level until the DWARF line table indicates a new source line has been reached. Step Over is implemented by setting a temporary breakpoint at the next instruction in the caller's frame and then continuing, so the called function runs at full speed.

The Impact of Optimization

Compiler optimizations (-O1 and above) can inline functions, reorder statements, eliminate dead code, and keep variables in registers rather than on the stack. This disrupts the one-to-one mapping between source lines and machine instructions, causing the debugger's line-stepping to appear jumpy or inconsistent, and variable inspection to report <optimized out>. This is why debug builds should always use -O0 (no optimization) to ensure a faithful debugging experience. When you must debug optimized code—say, a bug that only reproduces under -O2—you may need to fall back to assembly-level inspection and register examination.

💡 Practical Compilation Flags
For debugging, always compile with: g++ -g -O0 -Wall -Wextra -std=c++17 -o myprogram main.cpp. The -g flag generates DWARF debug info, -O0 disables optimization, and -Wall -Wextra enable additional warnings that may catch bugs at compile time before you even need the debugger.

Essential Debugger Commands: GDB, LLDB, and IDE Equivalents

Whether you work in a terminal or an IDE, the underlying operations are the same—only the syntax and interface change. The table below provides a comprehensive mapping of the most important debugging commands across GDB, LLDB, and typical IDE interfaces (Visual Studio, CLion, VS Code). Internalizing these commands is essential, because even when you primarily use a graphical debugger, the ability to drop into a command-line debugger on a remote server or minimal development environment is a skill you will need throughout your career.

Essential debugger commands across GDB, LLDB, and IDE environments
ActionGDB CommandLLDB CommandIDE Equivalent
Set breakpointbreak main.cpp:15b main.cpp:15Click line gutter
Run programrunrunF5 / ▶ Start Debugging
Step Overnext (or nnext (or nF10
Step Intostep (or sstep (or sF11
Step OutfinishfinishShift+F11
Continuecontinue (or ccontinue (or cF5
Print variableprint xp x or frame variable xHover over variable / Watch panel
View call stackbacktrace (or btbtCall Stack panel
List local variablesinfo localsframe variableLocals panel
Set watchpointwatch xwatchpoint set variable xData breakpoint in breakpoint panel
Quit debuggerquitquitShift+F5 / Stop
This diagram shows a code listing paused at line 7 (highlighted in amber). Step Over executes the function call and stops at line 8. Step Into enters the compute function and stops at line 2. Step Out (when inside compute) runs until the function returns.

Worked Example: Debugging a Segmentation Fault

Consider the following C++ program that crashes with a segmentation fault. We will walk through a complete GDB session to locate and understand the bug. The program attempts to compute the average of values in a vector, but something goes wrong at runtime.

🐛 Buggy Source Code (average.cpp)
#include <iostream> #include <vector> double average(const std::vector<int>& data) { double sum = 0.0; for (int i = 0; i <= data.size(); ++i) { // Bug: <= should be < sum += data[i]; } return sum / data.size(); } int main() { std::vector<int> scores = {85, 92, 78, 96, 88}; std::cout << "Average: " << average(scores) << std::endl; return 0; }
GDB Debugging Session
1
Step 1 — Compile with Debug SymbolsFirst, compile the program with -g and -O0 to ensure full debug information and no optimizations: g++ -g -O0 -std=c++17 -o average average.cpp. This produces an executable named average with DWARF debug symbols embedded.
Binary average is ready for debugging.
2
Step 2 — Launch GDB and RunStart GDB with gdb ./average. At the GDB prompt, type run. The program executes and crashes. GDB catches the crash and reports: Program received signal SIGSEGV, Segmentation fault. GDB also shows the exact line where the crash occurred and the instruction address.
GDB pauses at the crash site: sum += data[i]; in function average.
3
Step 3 — Examine the BacktraceType backtrace (or bt) to see the call stack. GDB shows frame #0 in average() at line 7 and frame #1 in main() at line 14. This tells us the crash happened inside the average function, called from main.
Call stack: main → average, crash at line 7.
4
Step 4 — Inspect VariablesType print i to check the loop index. GDB reports $1 = 5. Now type print data.size() which returns $2 = 5. The vector has 5 elements (indices 0–4), but i is 5, meaning the loop has overrun the valid range. The condition i <= data.size() allows one extra iteration.
Root cause identified: off-by-one error. The loop condition uses <= instead of <, causing an out-of-bounds access at index 5.
5
Step 5 — Fix and VerifyChange the loop condition from i <= data.size() to i < data.size(). Recompile with g++ -g -O0 -std=c++17 -o average average.cpp and run again under GDB. The program now outputs Average: 87.8 and exits normally. The entire debugging session took under two minutes—far faster than littering the code with print statements and recompiling repeatedly.
Bug fixed. Program outputs: Average: 87.8

Debugger vs. Print Debugging vs. Other Approaches

Debugging strategies exist on a spectrum from low-tech to highly sophisticated. While print-statement debugging (often called printf debugging) is the most accessible technique, interactive debuggers provide dramatically more power. However, each approach has contexts where it excels. The table below compares these approaches across several dimensions relevant to C++ development.

Comparison of debugging approaches for C++ development
CriterionPrint StatementsInteractive Debugger (GDB/LLDB)Sanitizers (ASan, UBSan)
Setup effortZero—just add std::coutCompile with -g -O0; learn commandsCompile with -fsanitize=address
Inspection depthOnly what you explicitly printFull: any variable, memory, registersFocused on memory/UB errors
Recompile needed?Yes, for every change to outputNo—explore interactivelyOnce, at compile time
Performance impactMinimal (I/O overhead)Significant when stepping; none when running between breakpoints2×–10× slowdown
Best forQuick checks, logging, production diagnosticsComplex logic bugs, crashes, understanding unfamiliar codeMemory errors, undefined behavior
Risk of leaving in codeHigh—forgotten prints in productionNone—debugger is externalNone—flag removed for release
✦ KEY TAKEAWAY
The best debugging strategy is not one tool—it is a layered approach. Use sanitizers as your first line of defense (they catch entire categories of bugs automatically), use the interactive debugger for investigative work on complex logic bugs and crashes, and reserve print-statement debugging for lightweight logging and situations where attaching a debugger is impractical (e.g., real-time systems, deeply embedded targets). Think of it like medical diagnostics: sanitizers are the blood panel that flags anomalies, the debugger is the MRI that gives you a detailed cross-section, and print statements are the doctor asking 'where does it hurt?'

Connection to Advanced Debugging Techniques

The breakpoint-step-inspect workflow introduced in this lesson is the foundation upon which more powerful debugging techniques are built. As your programs grow in complexity—multithreaded servers, template-heavy libraries, optimized release builds—you will need to extend your debugger skills in several directions. The table below previews these advanced techniques, contrasting them with the introductory skills covered here.

Introductory vs. advanced debugging techniques
Introductory (This Lesson)Advanced TechniqueUse Case
Simple breakpoint at a lineConditional breakpoints — break foo.cpp:20 if x > 100Stop only on specific conditions in a loop that runs millions of iterations
Single-threaded steppingThread-aware debugging — info threads, thread apply all btDiagnosing deadlocks, race conditions in concurrent programs
Inspecting local variablesCustom pretty-printers for STL containers and user typesReadable display of std::map, std::shared_ptr, etc.
Forward execution onlyReverse debugging — reverse-step in GDB with recordTracing back from a crash to the origin of corruption
Local process debuggingRemote debugging — gdbserver :1234 ./appDebugging on embedded devices, containers, or production-like environments

Each of these advanced techniques builds directly on the mental model you develop by mastering the basic workflow. Conditional breakpoints, for example, are simply breakpoints with an attached predicate—the same INT 3 mechanism fires, but the debugger evaluates the condition and silently resumes if it is false. Remote debugging uses the same GDB protocol but over a TCP socket instead of a local ptrace connection. Investing time in the foundations now yields compounding returns as you encounter increasingly challenging debugging scenarios in your career.

Practice Problems

PROBLEM 1 — CONCEPTUAL
Explain the difference between next (Step Over) and step (Step Into) in GDB. In what situation would you prefer step over next?
PROBLEM 2 — BASIC
You compile your program with g++ -O2 main.cpp -o main and attempt to debug it with GDB. When you try to print a local variable, GDB reports <optimized out>. What caused this, and what compilation flags would you change to fix it?
PROBLEM 3 — INTERMEDIATE
Consider the following GDB session excerpt. Describe what the developer learned at each step and what their next debugging action should be: (gdb) break mergeSort (gdb) run Breakpoint 1, mergeSort(arr=..., left=0, right=7) at sort.cpp:12 (gdb) print left $1 = 0 (gdb) print right $2 = 7 (gdb) next (gdb) next (gdb) print mid $3 = 3 (gdb) continue Breakpoint 1, mergeSort(arr=..., left=0, right=3) at sort.cpp:12 (gdb) continue Breakpoint 1, mergeSort(arr=..., left=0, right=1) at sort.cpp:12
PROBLEM 4 — APPLIED
You are debugging a linked list implementation. The program runs correctly for small inputs but crashes with a segmentation fault when the list contains more than 1000 nodes. Using only GDB commands and features described in this lesson, outline a systematic strategy to locate the bug. Include at least four specific GDB commands you would use and explain why.
PROBLEM 5 — CRITICAL THINKING
A colleague argues that modern IDEs with integrated debuggers have made command-line debuggers like GDB and LLDB obsolete for C++ development. Construct a nuanced argument addressing this claim. Under what circumstances might a command-line debugger be not only useful but essential? Conversely, what are legitimate advantages of IDE-integrated debuggers that the command line cannot easily replicate?

Lesson Summary

Interactive debuggers like GDB and LLDB allow you to pause a running C++ program, inspect its internal state, and step through execution one line at a time. The fundamental workflow involves compiling with debug symbols (-g) and disabled optimizations (-O0), setting breakpoints at suspicious locations, running the program, and using Step Over (next), Step Into (step), and Step Out (finish) to navigate through the code. Commands like print, backtrace, and info locals reveal the program's state at any paused moment.

This introductory workflow forms the foundation for advanced techniques including conditional breakpoints, watchpoints, thread-aware debugging, and reverse debugging. Whether you work from the command line or within an IDE, the underlying concepts are identical—the debugger pauses your program at controlled points, lets you examine state, and resumes execution on your command. Mastering this skill set eliminates guesswork from the bug-hunting process and dramatically accelerates your development productivity.

Varsity Tutors • C++ • Using Debugger — Use a debugger (gdb/lldb or IDE) to step through code (intro)