Developer Tools · Debuggers
Hardware Breakpoints and Watchpoints in the Debug Registers
Four address slots, a length encoding with alignment rules, and a trap that fires after the store: what the processor can observe without editing code.
A watchpoint on a variable is a different kind of object from a breakpoint on a line. The processor, not the debugger, compares every memory access against a small set of addresses held in registers, and raises an exception when one matches. Nothing in the program’s text changes, so the technique works on code in read-only memory, on code that has not been written yet, and on data that no instruction in the program ever loads as an instruction. The price is scarcity: four slots on x86, each with a length and an access type, and no way to watch a fifth address without giving one up.
Four slots and a length encoding
x86 provides four address registers, DR0 through DR3, plus DR6 for status and DR7 for control. In 64-bit mode each holds a linear address. DR4 and DR5 exist as aliases of DR6 and DR7 unless a control-register bit enables the older interpretation, and debuggers ignore them.
DR7 is a bit field, and its layout explains most of the mechanism’s limitations. For each slot n, bit 2n is the local enable and bit 2n+1 is the global enable. Bits 16 through 31 hold two-bit fields per slot: RWn selects the access type and LENn selects the size. RWn of 00 means instruction execution, 01 means data writes, 11 means data reads and writes, and 10 means I/O port access, which is available only when a control-register bit turns on the extension. There is no read-only data mode. A debugger that wants to stop when a variable is read has to accept a trap on every write as well and filter afterwards.
LENn is not a plain byte count. The two bits encode 1, 2, 8, and 4 bytes in that order: 00 is one byte, 01 is two, 10 is eight, 11 is four. The address must be aligned to the length, so a four-byte watch on a value at an address that is not a multiple of four is not representable; the debugger either widens the region or reports that the watchpoint cannot be set. Because the encodings top out at eight bytes, watching a sixteen-byte structure needs two slots, and watching a heap object of unknown alignment may need more slots than exist.
The four slots are shared between code and data. An execution breakpoint uses RWn = 00, with LENn ignored, and occupies one of the same four registers. A session that sets two watchpoints and two hardware breakpoints has exhausted the machine. Debuggers report this as a resource error rather than silently degrading, which is why hardware breakpoints are usually described as a fallback for cases where the byte patch of software breakpoints is unavailable.
The local and global enable bits differ in lifetime rather than in effect. A local enable is cleared on a task switch, so it applies to the thread that set it; a global enable survives the switch, which means it applies to whichever thread runs next. Kernel code that wants a breakpoint to fire for every thread sets the global bit; a debugger, which is tracing one thread, sets the local bit. Setting both for the same slot is legal and is what some debuggers do when they want the breakpoint to survive a resume that might migrate the thread to another processor.
Other architectures make the same trade with different numbers. AArch64 exposes separate breakpoint and watchpoint register files, typically a small number of each, with a mask field on watchpoints that allows a power-of-two aligned region to be covered by one comparator. The count is discoverable from a processor feature register rather than fixed by the architecture, so a debugger queries it instead of assuming four.
What a write watchpoint means when the trap arrives
For a data access, the exception is reported after the instruction has completed. The store has already reached the cache, the destination register or memory holds the new value, and the saved instruction pointer points at the following instruction. A debugger that stops on a watchpoint therefore shows the program after the assignment, not before it, and the reported source line may be the next statement.
This is a fault-versus-trap distinction with practical consequences. Stepping from a write watchpoint re-executes nothing; the write is done. Inspecting the old value requires a separate record, and debuggers that offer “watch the previous value” keep one themselves. It also means a watchpoint cannot prevent a store, only observe it, so it cannot be used to emulate a memory-mapped register or to veto an access.
For an execution breakpoint in a debug register, the exception is a fault raised before the instruction executes, with the saved address equal to the breakpoint. No byte was patched, so there is nothing to restore and no single-step is needed to get past it. Resuming simply continues, which is why hardware breakpoints are preferred when the text cannot be modified: code in flash, code in a read-only mapping backed by a file that must not be touched, or code shared with other processes through a MAP_SHARED mapping.
Watchpoints that do not fire
The comparison happens on memory accesses issued by the processor core in the context of the traced thread. Several kinds of write are therefore invisible.
A device writing through DMA does not go through the core’s load-store path, so no watchpoint triggers. A kernel routine writing into a user buffer on behalf of a system call performs the store in kernel mode; the debug registers are part of the thread’s context and are not applied to the kernel’s own accesses on that thread’s behalf in the way a user-mode store would be. A debugger watching a buffer filled by read sees the change only when the program next touches the memory, if it does at all. The same applies to memory written by another process through shared memory: the store is executed by a different core in a different context.
Watchpoints are also per-thread, because the registers are part of the thread context that the scheduler saves and restores. The local enable bits are cleared by a hardware task switch, and operating systems that switch tasks in software save and restore the register set themselves. Either way, a watchpoint set on one thread does not fire when a sibling thread writes the address. Debugging a race by watching a variable requires setting the watchpoint on every thread, which consumes slots per thread rather than per process.
Finally, the length and alignment rules mean that a watchpoint can be too narrow. A four-byte watch on a field that is written as part of an eight-byte store does fire, because the accessed range overlaps the watched range, but a watch on the first four bytes of a sixteen-byte structure misses writes to the upper half entirely. The debugger has no way to know which store instructions exist; it only knows the range that was requested.
Programming the slots from a debugger
On Linux the debug registers are reachable through PTRACE_POKEUSER at fixed offsets inside struct user. The sequence is: stop the thread, write the address into a slot, set that slot’s type, length, and enable bits in DR7, then resume. The kernel saves the register file on context switch, so the setting survives scheduling.
#include <stddef.h>
#include <sys/ptrace.h>
#include <sys/user.h>
/* Watch four bytes at addr for writes, using slot 0. */
static int set_write_watch(pid_t pid, unsigned long addr)
{
unsigned long dr7;
const size_t dr0 = offsetof(struct user, u_debugreg) + 0 * sizeof(long);
const size_t dr7off = offsetof(struct user, u_debugreg) + 7 * sizeof(long);
if (ptrace(PTRACE_POKEUSER, pid, (void *)dr0, (void *)addr) == -1)
return -1;
dr7 = (unsigned long)ptrace(PTRACE_PEEKUSER, pid, (void *)dr7off, 0);
dr7 &= ~(0x3UL << 16);
dr7 |= (0x1UL << 16); /* RW0 = 01: data writes */
dr7 &= ~(0x3UL << 18);
dr7 |= (0x3UL << 18); /* LEN0 = 11: four bytes */
dr7 |= (0x1UL << 0); /* L0: local enable for this thread */
return ptrace(PTRACE_POKEUSER, pid, (void *)dr7off, (void *)dr7) == -1 ? -1 : 0;
}
When the trap arrives, the debugger reads DR6 to find which slot fired. Bits 0 through 3 correspond to DR0 through DR3; a separate bit reports the trap flag single-step described in single-stepping and the trap flag, and another reports an access to the debug registers themselves. DR6 is not cleared by hardware in every case, so a debugger that ignores the status bits and assumes the first enabled slot fired will misattribute events after the first one.
The kernel can use the same registers for its own purposes. Hardware tracing and some performance facilities reserve debug registers, and a request that conflicts fails rather than sharing. On a machine where such a facility has claimed the slots, watchpoints are unavailable regardless of what the debugger asks for, and the failure surfaces as an error at the point of setting the watchpoint rather than at the point of the access.
References
- Intel 64 and IA-32 Architectures Software Developer Manuals, Volume 3: System Programming Guide — Intel Corporation
- AMD64 Architecture Programmer's Manual, Volume 2: System Programming — AMD
- ptrace(2) — Linux manual page — Linux man-pages project