Software Breakpoints
Software Breakpoints
The mechanism by which a debugger pauses a program at a chosen instruction. The clever-but-simple trick: the debugger overwrites the instruction at the target address with a single-byte trap (INT3 on x86, 0xCC), then restores the original byte after the trap fires.
The flow
You set a breakpoint at line 42, which DWARF tells the debugger lives at address 0x4011a8. The first byte of that instruction is, say, 0x55 (start of push rbp).
- Debugger uses ptrace (
PTRACE_PEEKDATA) to read the byte at0x4011a8. Reads0x55. - Debugger uses ptrace (
PTRACE_POKEDATA) to overwrite that byte with0xCC.0xCCis the x86 instructionINT3— a 1-byte software interrupt. - Debugger remembers: "address
0x4011a8originally held0x55."
Now the program runs. Eventually the CPU's instruction pointer reaches 0x4011a8 and executes INT3.
- CPU raises a software interrupt. Kernel converts it to a
SIGTRAPsignal aimed at the debuggee. - Kernel pauses the debuggee. Debugger (which had
PTRACE_ATTACH'd) gets notified. - Debugger checks RIP (instruction pointer): it's at
0x4011a8. "That's our breakpoint."
The user inspects state, then presses continue. Now the debugger has to execute the original instruction (0x55) and put the breakpoint back so we can hit it again next time:
- Restore:
PTRACE_POKEDATA(0x4011a8, 0x55). - Single-step:
PTRACE_SINGLESTEP. The CPU executes one instruction (push rbp) and pauses. - Re-insert:
PTRACE_POKEDATA(0x4011a8, 0xCC). - Resume:
PTRACE_CONT.
That's a complete software-breakpoint cycle.
Why a single byte
Different architectures have different trap instructions, but all are short (often 1–4 bytes) and chosen so that overwriting any aligned instruction's first byte with the trap is safe. On x86, INT3 is exactly 1 byte (0xCC), which means you can replace the first byte of any instruction without disturbing the next instruction's location. Critical: the instruction stream is variable-length on x86; longer trap instructions could overflow.
ARM uses BKPT (16-bit on Thumb, 32-bit on ARM); RISC-V uses EBREAK. Same idea.
Hardware breakpoints
The CPU also has dedicated debug registers (DR0–DR7 on x86) that can pause execution when a specific address is accessed. These are hardware breakpoints:
- Pros: don't modify code (so they work in read-only memory, ROMs, code that uses self-modifying tricks). Faster.
- Cons: very limited number (4 on x86). Can only break on access to a few specific addresses.
Used for things like watchpoints (pause when memory at address X is written) where software breakpoints don't work cleanly. Most line breakpoints in normal source code are software breakpoints.
What can go wrong
- Code in read-only memory: software breakpoints can't be installed because ptrace can't write there. Most modern OSes allow ptrace to write read-only mappings via copy-on-write; some don't. JIT'd code may also live in pages the debugger can't write.
- Multi-threaded race: thread A hits the breakpoint, debugger removes the trap to let A single-step; while A is single-stepping, thread B reaches the same address — and finds the original instruction, missing the breakpoint. Real bug; debuggers solve via "stop all threads when stopping one" (DAP
allThreadsStopped: true). - Self-modifying code: code that overwrites itself can replace the breakpoint trap with the original instruction, defeating the breakpoint silently.
See also
- ptrace — the syscalls used to install breakpoints
- DWARF Debug Symbols — what tells the debugger which address corresponds to which line
- How Debuggers Actually Work — full worked example with this flow