Software Dev Notes

Home › Testing & Debugging

How Debuggers Work: Breakpoints, Single-Stepping and Debug Symbols

Learn how debuggers pause programs with breakpoints, single-step through instructions, and map machine state back to source lines using debug symbols.

2026-10-19 · 5 min read

A debugger feels like a special power. You set a breakpoint, run the program, and it stops exactly where you asked, showing the values of your variables. Behind that experience is a small set of operating system and processor features, combined with a table that maps machine addresses back to source lines. Understanding these pieces makes debugging faster, explains why some breakpoints fail, and shows what a debugger can and cannot see. This post walks through the mechanisms on a typical Unix-like system and on x86-64 processors.

Debuggers are controllers

A debugger is a separate process that controls another process, called the target. The operating system provides the link. On Linux, the classic interface is the ptrace system call, which lets one process observe and modify another. With it, the debugger can stop the target, read and write its memory and registers, and resume it. macOS and Windows offer equivalent interfaces, such as Mach exceptions and the Windows debugging API. The principle is the same everywhere: the operating system gives the debugger control that the target cannot refuse.

Because the debugger is a different process, the target does not need to be recompiled to be inspected, although debug information makes the experience far better. Some debuggers attach to a process that is already running, while others launch it under their control from the start.

Software breakpoints

The most common breakpoint is a software breakpoint. On x86 processors, the instruction INT3 is a one-byte opcode, hexadecimal 0xCC, that raises a trap when executed. To set a breakpoint, the debugger saves the original byte at the target address and writes 0xCC in its place. When the program reaches that address, the processor traps into the operating system, which notifies the debugger. The debugger sees that the program stopped at a breakpoint, restores the original byte, and sets the program counter back so the real instruction can run when resumed.

To keep going past the breakpoint, the debugger must put the original instruction back, single-step over it, and then re-insert the trap. This sequence explains why a breakpoint on code that is modified at runtime can behave strangely.

Hardware breakpoints and watchpoints

Processors also have debug registers. On x86-64 there are four address registers, each of which can trigger on execution, on writes or on reads and writes, for a chosen length. Hardware breakpoints do not change the code bytes, so they work on read-only memory and avoid side effects. They are limited in number, which is why a debugger may refuse a fifth hardware breakpoint.

Watchpoints use the same mechanism to stop when a variable changes. This is one of the most useful tools for finding memory corruption, because it reports the exact instruction that wrote the bad value. Without it, a bug may show up far away from its cause.

Single-stepping

Single-stepping runs one instruction and then stops. On x86 processors, the trap flag in the flags register causes a debug exception after each instruction. Debuggers use this for "step into" and "step over" commands. The difference is what the debugger does after a call instruction. Step into stops at the first instruction of the called function, while step over places a temporary breakpoint at the return address and runs until it is reached.

Single-stepping is slow, because each step involves a round trip through the kernel. Debuggers avoid it where possible, for example by using temporary breakpoints for step over and by stepping through whole source lines rather than individual instructions.

Mapping machines back to source

Machine code has no variable names and no line numbers. The compiler records the mapping in debug information. The most common format on Unix-like systems is DWARF, which is stored in sections of the executable or in a separate file. A line table maps address ranges to source files and line numbers. A symbol table maps function names to addresses. Type descriptions tell the debugger how to interpret bytes as integers, pointers or structures.

Variable locations are more complex. DWARF describes where a variable lives at each point in the program, which may be a register, a stack offset, or nothing at all after optimization. This is why debugging optimized code often shows "optimized out" for some variables. The compiler discarded or moved the value, and the debug information honestly reports that.

Stripped binaries have no symbols, so the debugger can show only addresses. Keeping a separate symbol file lets you debug a release binary later without shipping debug information in the product.

Stack frames

When the program stops, the debugger needs to show the call stack. On many platforms, it walks frames using unwind information, which describes how to recover the caller's registers at each address. Without that information, the stack trace may be incomplete. Frame pointers, when kept, make walking simpler but are not required on modern compilers that emit unwind tables.

The stack trace is a snapshot of how the program got here. Reading it from the bottom up shows the sequence of calls, and reading the top frame shows where the stop happened. Combined with the local variables of each frame, it often answers the question "how did we arrive in this state."

A comparison of techniques

The table below compares the main ways to observe a running program. Each has a place, and experienced developers combine them.

Technique What it does Typical cost Best for
Software breakpoint Replaces an instruction with a trap Low, but modifies code Stopping at a known line
Hardware breakpoint Uses CPU debug registers Very low, limited count Read-only code and quick checks
Watchpoint Stops when memory is written Low to medium Finding corruption
Single-step Stops after every instruction High Tracing tricky control flow
Logging Prints chosen values Medium, changes timing Long runs and production

Practical habits

Build with debug information while debugging, and check that the binary you run matches the source you are reading. A common confusion is that the debugger shows lines from an old build because the symbols belong to a different file. Prefer conditional breakpoints for loops, so the debugger stops only on the iteration that matters. Use watchpoints when a value changes unexpectedly, instead of guessing where the write happens.

Remember that a debugger changes timing. A race condition may disappear when you step through it, because the threads no longer overlap in the same way. In those cases, logging with timestamps or tracing tools is often more informative than stopping the program.

Key takeaways

Debuggers rely on operating system interfaces that let one process control another. Software breakpoints replace an instruction with a trap, and hardware breakpoints and watchpoints use CPU debug registers to stop without changing code. Single-stepping relies on the processor's trap behavior and is slow but precise. Debug information in formats such as DWARF maps addresses to lines, functions and variables. Understanding these layers helps you choose the right tool, read stack traces with confidence, and recognize when the debugger itself is changing what you observe.

More in Testing & Debugging

Testing

Unit vs Integration Testing: What Each Catches and Where Each Fails

Compare unit and integration tests by scope, speed and failure diagnosis, and learn how to balance them in a test suite using the test pyramid.

5 min read