Debug Registers and Hardware Breakpoints
DR0 through DR3, DR6, and DR7 are the CPU’s debug registers, and a kernel can program them directly, with no GDB or external debugger involved at all, to implement its own memory watchpoints: useful for catching an unexpected write to a specific structure at the exact instruction responsible, rather than single-stepping through code hunting for it. Emulating & Debugging already covers the same four address registers from the opposite side, GDB’s hbreak and watch commands, backed by QEMU’s faithful emulation of them.
The four address registers
Section titled “The four address registers”DR0 through DR3 each hold one linear address to watch, giving the CPU four independent breakpoint slots active simultaneously. Each address is compared against every relevant instruction fetch or memory access as it happens in hardware, with no software polling or page-fault trick involved, which is what makes a hardware watchpoint dramatically faster than the alternative of marking a page not-present and catching every access to it as a page fault: only the specific address matters, not the whole page it lives on, and every other access to that same page proceeds at full speed untouched.
DR7: enabling and configuring each slot
Section titled “DR7: enabling and configuring each slot”DR7 controls all four slots at once, with two bits per slot governing whether it’s active at all, and a further two-bit field per slot selecting what condition triggers it: 00 for instruction execution, 01 for a data write, 11 for a data read or write, with 10 reserved unless CR4.DE is set, in which case it means an I/O read or write. A separate two-bit length field per slot (00, 01, 10, 11 mapping to 1, 2, 8, and 4 bytes respectively, with the 8-byte encoding only valid in 64-bit mode) sets how many bytes starting at the address in the matching DRn register the condition applies to, letting a watchpoint cover an entire multi-byte field rather than only its first byte.
#define DR7_LOCAL(n) (1u << (2 * (n)))#define DR7_RW(n, rw) ((rw) << (16 + 4 * (n)))#define DR7_LEN(n, len) ((len) << (18 + 4 * (n)))
// arm DR0 as a 4-byte write watchpoint, active in the current task onlyuint64_t dr7 = DR7_LOCAL(0) | DR7_RW(0, 0b01) | DR7_LEN(0, 0b11);__asm__ volatile("mov %0, %%dr7" :: "r"(dr7));Each slot’s enable bit actually comes in two flavors, a local bit that stays set only until the next task switch and a global bit the CPU never clears on its own: a kernel implementing per-task watchpoints has to reprogram DR7 (and the corresponding DRn) on every switch into a task that owns one, using the local form, since a global breakpoint left active would otherwise fire against every task sharing the same linear address for something entirely unrelated to whatever the watchpoint was meant to catch.
DR6: finding out which one fired
Section titled “DR6: finding out which one fired”Once any armed condition matches, the CPU raises #DB (vector 1) and sets a bit in DR6 identifying which of the four slots actually triggered, B0 through B3 corresponding to DR0 through DR3, letting a single shared #DB handler distinguish between up to four independently armed watchpoints rather than needing a separate handler per slot. DR6 is not cleared automatically by the CPU on exception entry, and its bits are cleared by writing zero to them explicitly, which the handler has to do itself before returning; a handler that reads DR6 but never clears it finds the same stale bits still set the next time any watchpoint fires, at that point unable to tell a genuinely new trigger apart from a leftover one from before.
void db_handler(void) { uint64_t dr6; __asm__ volatile("mov %%dr6, %0" : "=r"(dr6));
if (dr6 & (1 << 0)) handle_watchpoint(0); if (dr6 & (1 << 1)) handle_watchpoint(1); // ...
__asm__ volatile("mov %0, %%dr6" :: "r"(0ULL)); // clear before returning}Implementation notes
Section titled “Implementation notes”Reading or writing any debug register at all requires ring 0, with no equivalent of the TSS I/O permission bitmap that lets a kernel selectively grant port access to ring 3: a userspace debugger such as GDB never touches these registers directly, going instead through a kernel-mediated interface (ptrace on Linux) that performs the actual mov to DRn on the debugger’s behalf after checking permissions. CR4’s DE (Debugging Extensions) bit additionally must be set for the RW field’s 10 encoding to mean an I/O breakpoint at all, rather than being left undefined, a detail relevant mainly to I/O breakpoints rather than the data watchpoints this article otherwise covers and safe to leave at its default in most kernel debug-register code. Because a hardware breakpoint on an instruction fetch triggers before that instruction executes while a data watchpoint triggers after the access completes, a handler for the two cases can’t share identical assumptions about what state has already changed by the time it runs.
References
Section titled “References”- ^ Intel, Intel 64 and IA-32 Architectures Software Developer’s Manual, Volume 3B, Chapter 18: the complete debug-register reference, including
DR7’s field layout and#DBdelivery conditions.
See also
Section titled “See also”- Emulating & Debugging: the same DR0-DR3 registers used from GDB’s
hbreak/watchside, via QEMU’s emulation. - The IDT: the reserved exception vector table
#DBbelongs to. - The Task State Segment: the ring-3 access-control model debug registers notably lack an equivalent of.