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.

Programming braces with a command terminal and binary digits.
Inside this lesson 10 sections 5 questions 5 figures

SECTION 1

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.

SECTION 2

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.

SECTION 3

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.

Interactive Debugging Workflow1. Compileg++ -g -O0 main.cpp2. Launchgdb ./a.out3. Set Breakpointsbreak main.cpp:424. Runrun5. Breakpoint Hit!Program paused — inspect state6. Inspect & Stepprint var / next / stepbacktrace / info localscontinue (loop back to breakpoint)7. Bug Found!Fix code, recompile,and re-testRecompile and restart if neededCommon GDB Commandsbreak, run, next, step, finishprint, backtrace, continue, quitinfo locals, watch, display
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.

SECTION 4

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.

SECTION 5

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.

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

Essential debugger commands across GDB, LLDB, and IDE environments

Step Over vs Step Into vs Step OutSource Code:1int compute(int a, int b) {2 int result = a * b + 10;3 return result;4}5int main() {6 int x = 5;7 int y = compute(x, 3);← Current line (breakpoint)8 std::cout << y;9 return 0;10}From line 7:Step Over → goes to line 8(compute runs fully, then stops)Step Into → goes to line 2(enters compute function body)Step Out → returns to caller(if inside compute, runs to line 7)
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.

SECTION 6

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. 1

    Step 1 — Compile with Debug Symbols

    First, 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. 2

    Step 2 — Launch GDB and Run

    Start 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. 3

    Step 3 — Examine the Backtrace

    Type 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. 4

    Step 4 — Inspect Variables

    Type 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. 5

    Step 5 — Fix and Verify

    Change 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

SECTION 7

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.

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

Comparison of debugging approaches for C++ development

✦

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?'

SECTION 8

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 (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

Introductory vs. advanced debugging techniques

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.

SECTION 9

Practice Problems

Question 1: CONCEPTUAL

Explain the difference between next (Step Over) and step (Step Into) in GDB. In what situation would you prefer step over next?
Show answer
The next command executes the current source line completely, including any function calls on that line, and stops at the next line in the same function. The step command, by contrast, descends into any function called on the current line, stopping at the first line of that function's body. You would prefer step when you suspect the bug lies inside a function being called on the current line and you want to inspect its internal execution. If you trust the called function is correct and want to focus on the caller's logic, use next to avoid getting lost inside irrelevant functions.

Question 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?
Show answer
The -O2 flag enables aggressive compiler optimizations, which can keep variables in CPU registers instead of stack memory, eliminate variables whose values are computed but never stored, or inline functions entirely. Without the -g flag, there are no debug symbols at all, making source-level debugging impossible. To fix this, recompile with g++ -g -O0 main.cpp -o main. The -g flag embeds DWARF debug symbols, and -O0 disables optimizations so that every variable is stored predictably and every source line maps cleanly to machine instructions.

Question 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
Show answer
The developer set a breakpoint at the entry of the mergeSort function to observe recursive calls. The first hit shows the initial call with the full range [0, 7]. After stepping twice, they inspected mid = 3, confirming the midpoint computation is correct for an 8-element array. Subsequent continues show recursive calls with [0, 3] and [0, 1], demonstrating the left-half recursion is proceeding correctly. Their next action should be to continue until the recursion reaches a base case (right − left ≤ 1), then step through the merge logic to verify that the merge operation correctly combines sorted subarrays. If the bug is in merging rather than partitioning, a conditional breakpoint like break merge if left==0 && right==3 would help isolate the first non-trivial merge.

Question 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.
Show answer
First, compile with g++ -g -O0 and run the program under GDB with a 1000+ node input. When the segfault occurs, use backtrace to identify which function and line caused the crash—this reveals whether it is in insertion, traversal, or deletion. Use print to inspect the pointer that was dereferenced (e.g., print current->next); a null or garbage address indicates the bug's nature. Set a watch on the suspicious pointer to catch the exact moment it becomes invalid—this is more efficient than stepping through 1000 iterations manually. Finally, use info locals in each stack frame to check for uninitialized pointers or incorrect loop counters. Common culprits in linked list crashes include failure to null-terminate a new node, incorrect pointer updates during insertion that create cycles, or off-by-one errors in length-based traversal.

Question 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?
Show answer
The claim oversimplifies a nuanced landscape. Command-line debuggers remain essential in several scenarios: debugging on remote servers or embedded systems where no GUI is available; automating debugging tasks via GDB's scripting interface (Python API) for reproducible test harnesses; debugging core dumps post-mortem on production machines; and working within CI/CD pipelines where attaching a graphical debugger is impractical. GDB also supports reverse debugging (record and reverse-step), which many IDE integrations do not expose. However, IDE debuggers offer genuine advantages: visual data structure rendering (e.g., viewing a tree's structure graphically), seamless integration with the editor (click to set breakpoints, hover to inspect), simultaneous display of source, variables, call stack, and console without switching contexts, and lower cognitive overhead for developers new to debugging. The strongest position is that both tools serve the same underlying protocol—many IDEs use GDB/LLDB as their backend—so learning the command-line debugger deepens understanding of what the IDE does automatically, while the IDE accelerates common workflows. A competent C++ developer should be fluent in both.

SUMMARY

Lesson Summary

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)