Eight registers hold four addresses and the control word that says what to do about them. That is the entire hardware debug facility on x86, and it is the reason a debugger can set a data breakpoint that fires on the exact instruction that stored — a capability that most other architectures do not offer and that every C programmer has relied on without knowing it existed.
It is also a set of registers a user process cannot touch, in a set of registers with two authoritative layouts that disagree with each other, and where the field that says “break on a four-byte write” is a two-bit field followed by a two-bit field in a specific order that is the single most common DR7 bug there is. That is a reference with a retraction in it, and this page is where both live.
The addresses are trivial: DR0 to DR3 each hold one linear address, and there are four of them because there are four, and that is the whole of the hardware's breakpoint capacity. DR4 and DR5 are not registers at all — they are aliases of DR6 and DR7 when CR4.DE is clear, and #UD when it is set, which is the one time on this whole page where a CR4 bit from the previous concept reappears.
DR6 is status and DR7 is control. DR6 has four bits for “breakpoint n fired”, three more for single-step, task-switch and debug-register access, and a large reserved field. DR7 is where the interest is:
$ ./sysdump | sed -n '/1B4\./,/LEN | 11/p' | head -40
1B4. DR0-DR7. THE MASK, THE FORMULA, AND THE TWO SOURCES.
DR0-DR3 | one LINEAR address each; four breakpoints, no more
DR4,DR5 | aliases of DR6,DR7 when CR4.DE=0, #UD when CR4.DE=1
DR6 | status: bits 0-3 B0-B3, 13 BD, 14 BS, 15 BT
DR7 | control: the eight enable bits, the GD bit, and EIGHT
| four-bit fields, one per breakpoint
DR7 | 0 | L0 | local enable, breakpoint 0, cleared on a switch
DR7 | 1 | G0 | global enable, breakpoint 0
DR7 | 2 | L1 | local enable, breakpoint 1
DR7 | 3 | G1 | global enable, breakpoint 1
DR7 | 4 | L2 | local enable, breakpoint 2
DR7 | 5 | G2 | global enable, breakpoint 2
DR7 | 6 | L3 | local enable, breakpoint 3
DR7 | 7 | G3 | global enable, breakpoint 3
DR7 | 8 | LE | local exact-breakpoint enable, ignored after the 486
DR7 | 9 | GE | global exact-breakpoint enable, ignored after the 486
DR7 | 10 | ONE | architecturally reserved to 1, the RESET value's bit
DR7 | 11-12 | - | reserved
DR7 | 13 | GD | general detect: #DB on any DR0-DR7 access
DR7 | 14-15 | - | reserved
DR7 | 16-19 | FP0 | bits 17:16 R/W0, bits 19:18 LEN0
DR7 | 20-23 | FP1 | bits 21:20 R/W1, bits 23:22 LEN1
DR7 | 24-27 | FP2 | bits 25:24 R/W2, bits 27:26 LEN2
DR7 | 28-31 | FP3 | bits 29:28 R/W3, bits 31:30 LEN3
DR7 | 32-63 | - | reserved, read as 0
Two things about that layout are the whole reason this page exists. First, the eight enable bits are contiguous — 0x55 for all four local, 0xAA for all four global — which is the 80386 arrangement and the reason the high two breakpoints have gaps in the bit numbering. Second, the four control fields are four bits wide each starting at bit 16, and each is split R/W in the low two, LEN in the high two. Not two fields of two bits side by side. One four-bit field, and getting the order of its halves wrong produces a mask that is entirely plausible and entirely wrong.
10 in the R/W field is the only one of the four that is undefined on a modern part with CR4.DE clear, and it is undefined rather than reserved, which is a stronger word: a kernel that programs it gets behaviour it may not rely on.
Four cases, each built from the formula, each checked for internal consistency before it is printed:
$ ./sysdump | grep '^ DR7MASK'
DR7MASK | case | value | fields disjoint
DR7MASK | four breakpoints, 4-byte, data write, global | 0x00000000dddd06aa | yes
DR7MASK | four breakpoints, 4-byte, data read/write, global | 0x00000000ffff06aa | yes
DR7MASK | four breakpoints, 8-byte, data read/write, global | 0x00000000bbbb06aa | yes
DR7MASK | one breakpoint, 1-byte, execute, LOCAL | 0x0000000000000601 | yes
DR7MASK | the mask for four 4-byte WRITE breakpoints | 0x00000000dddd06aa
DR7MASK | derived from the formula above, not typed in
DR7LOCAL | the same four, all LOCAL, no GD | 0x0000000055550455
DR7RESET | the reset value: the fixed one and nothing else | 0x0000000000000400
Read the mask for four 4-byte write breakpoints, 0xdddd06aa, field by field, because that is the exercise and because a mask you can read is a mask you can debug:
0x0aa L0 G0 L1 G1 L2 G2 L3 G3 all four GLOBAL
0x200 GE global exact, what Linux always sets
0x400 ONE the reset value's fixed one
0x4000 R/W0 = 01 data WRITE, breakpoint 0
0x30000 LEN0 = 11 FOUR bytes <-- the 11 that means four
0x1000000 R/W1 = 01
0x3000000 LEN1 = 11
0x10000000 R/W2 = 01
0x30000000 LEN2 = 11
0x100000000 R/W3 = 01
0x300000000 LEN3 = 11
------
0xdddd06aa
And the DR7LOCAL row next to it is worth stopping on, because the first version of that line was wrong in a way that would have disabled every breakpoint it named. The intent was “the same four, all local”, and the code cleared the global bits out of the all-global mask:
$ ./sysdump | grep -E '^ DR7(LOCAL|MASK) \| (the same|the mask for)'
DR7MASK | the mask for four 4-byte WRITE breakpoints | 0x00000000dddd06aa
DR7LOCAL | the same four, all LOCAL, no GD | 0x0000000055550455
The two lines differ in exactly one operation, and the operation is a logical OR. The first version computed the all-local mask by clearing the global bits out of the all-global mask — which is arithmetic on a mask that never had them set, so the result had zero enable bits and four correct control fields. That withdrawn value is 0x55550400, and the correct one is 0x55550455: the difference is the low 0x55, the four local enable bits that were missing. A debugger programmed with the withdrawn value would run correctly and break on nothing at all, and a user would conclude the program has no bugs. The general form is the same as the %al lesson in the ABI course: a mask that says “do nothing” is the correct mask for a debugger that is about to stop, which is exactly why it is so easy to ship by accident.
0x55550400 against the correct 0x55550455. The 0x555 in the low bits is the residual of the global bits, not the local ones: clearing a set bit leaves nothing, so the mask that came out had zero enable bits and four correct control fields. A debugger programmed with it would run correctly and break on nothing at all, and a user would conclude the program has no bugs.
$ ./sysdump | sed -n '/DISAGREEMENT, PRINTED RATHER/,/in five lines/p'
DISAGREEMENT, PRINTED RATHER THAN SETTLED. A third description,
the Intel SDM as summarised in several secondary sources, places
L2 and G2 at bits 8 and 9, LE and GE at 16 and 17, and starts the
R/W0 field at bit 22 with FOUR-bit LEN fields. Linux
DR_LOCAL_ENABLE_MASK=0x55, DR_GLOBAL_ENABLE_MASK=0xAA,
DR_CONTROL_SHIFT=16, DR_CONTROL_SIZE=4, DR7_FIXED_1=0x400 and
QEMU's DR7_TYPE_SHIFT=16, DR7_LEN_SHIFT=18 all say the table
above. Two implementations that work on real silicon agree with
each other. This file CANNOT MEASURE WHICH ONE ITS OWN CPU
IMPLEMENTS, because DR7 is unreadable from ring 3: section 3
shows the MOV failing. The instrument that would settle it is a
one-line ptrace program running at CPL 0.
There is a third piece of evidence in the table itself, and it is the one that settles it for a kernel even though it settles nothing for a user process: bit 10 is architecturally reserved to 1, and it is the reset value's only set bit. Under the layout this file prints, bit 10 is a reserved hole between the enable block and the GD bit. Under the alternative, bit 10 is L3 — the local enable for breakpoint 3 — and a kernel could not have a fixed-1 bit there without permanently enabling a breakpoint. That single fact eliminates one of the two descriptions, and it eliminates it by arithmetic rather than by authority.
It still does not tell you whether this particular CPU implements the surviving layout, because the only way to ask is to write to DR7 and read it back. Every attempt does this:
$ ./sysdump | grep -E '^ EDGE \| mov (dr|reg)'
EDGE | mov dr0 -> reg | debug registers | FAULTED | sig 11 | si_code 128 | si_addr 0x000000000000
EDGE | mov dr6 -> reg | debug registers | FAULTED | sig 11 | si_code 128 | si_addr 0x000000000000
EDGE | mov dr7 -> reg | debug registers | FAULTED | sig 11 | si_code 128 | si_addr 0x000000000000
EDGE | mov reg -> dr7 | debug registers | FAULTED | sig 11 | si_code 128 | si_addr 0x000000000000
Five of eight, all si_code 128. DR4 and DR5 are not in the table because whether they are #UD depends on CR4.DE, which is a bit this process cannot read, so a probe of them would measure the bit and be reported as a measurement of the register. The honest thing is to leave them out and say why.
The debug registers are unreachable. The debug exception is not, and the reason is that vector 1 is delivered by a completely separate mechanism: set bit 8 of the flags and the CPU raises a #DB after every instruction, without consulting a single one of the eight registers.
$ ./sysdump | grep -E '^ RAISE \| (int1|EFLAGS)'
RAISE | int1 | #DB 1 | sig 5 | 1 | 0x<per-run address> | the RIP the kernel chose to report, for SIGTRAP
RAISE | EFLAGS.TF set, then two more instructions | #DB 1 | sig 5 | 2 | 0x<per-run address> | the RIP the kernel chose to report, for SIGTRAP
Both arrive as SIGTRAP on vector 1, and they arrive with different si_code values — 1 for the int1 and 2 for the trap flag. That is the exception table's class column showing up in a signal payload: the same vector, two conditions, two codes, and a handler that treats si_code 2 as “a single step happened” is reading something the hardware actually decided rather than something it inferred.
Three consequences worth having:
DR0–DR3 to single-step. It writes the trap flag, runs one instruction, and takes the #DB. The four address registers exist for watchpoints — breaks on a data access — and single-stepping is a different mechanism entirely. That is why single-stepping works on every architecture x86-64 has an emulator for, and why watchpoint counts are always a hard four.pushfq; pop %rax; or $0x100, %rax; push %rax; popfq is five instructions in a user program. The privilege boundary that stops a user program reading DR7 does not stop it asking for a single step, and that asymmetry is the honest shape of the debug facility: a user can ask for a one-instruction trace and gets one, and cannot ask for a four-instruction watchpoint set.#DB is an exception, the kernel delivers a signal, the handler runs, the handler returns, and the next instruction executes. A single-stepping debugger is therefore a signal-driven debugger and is limited by the cost of a signal. The privilege course's mode-change concept measured that cost; this is where it is spent.And the limit, which is the same limit as everything else on this page: this file cannot set a watchpoint, and so cannot tell you what a watchpoint costs or whether the addresses are compared before or after translation. The addresses in DR0–DR3 are linear, so a watchpoint on a page that is not currently mapped compares against a translation that does not exist — which is a real design decision in every debugger, and one this artifact has no instrument for.
cd courses/x86sys/assets/samples && ./build_samples.sh, then read section 1B4. (Expect 0xdddd06aa for four 4-byte write breakpoints, and expect the harness to recompute it from the printed formula and compare. If you change the LEN order in the formula the harness must fail, which is the point of asserting the arithmetic rather than the constant: the constant would have passed a wrong formula.)DR7LOCAL bug on purpose. Change the | 0x55ULL back out of that line and rebuild. (Expect 0x55550400 and a harness failure on the local-mask check. It is a one-character deletion and it produces a mask that disables everything, which is the most expensive possible place to have an off-by-a-bit.)DR7_FIXED_1 or the local-enable mask and check which of the two layouts it encodes. (Expect 0x400 and 0x55, and expect that the debugger also sets the global bit 9 for reasons that are historical — exact breakpoints have been free since the 486, and the bit is a leftover. A debugger that still sets a bit whose only function was to slow down a 386 is carrying a scar, and the scar is harmless, which is why nobody has removed it.)SIGTRAPs. (Expect forty, and expect the count to match the instruction count exactly rather than approximately — which is the interesting result, because it means the trap flag is honoured per instruction and not per something coarser. Then set the flag, take a page fault, and see whether the count survives the fault.)CR4.DE and DR7.GD together decide whether a user program can detect that something is inspecting it. (Expect there to be no complete answer — a debugger using watchpoints is invisible to the traced program, and a debugger using DR4 or DR5 is not. That asymmetry is a security property of the hardware and it is one of the very few that nobody has to choose to implement.)Backwards, the previous concept is where CR4.DE lives and where the general shape of “a bit decides whether an instruction exists” was established. The privilege course's vector concept is where #DB appeared as a single row; the second concept here is what measured the two conditions behind that row.
Forwards, and the connection is the reason the next concept is where it is. #DB is vector 1, and vector 1 is delivered for a non-canonical address — except it is not, a non-canonical address is vector 13 by way of a general protection fault — and the whole of the sixth concept is about a boundary that is not a privilege boundary at all. The privilege check that fails when you cross into the hole in the address space is a canonicality check, it happens during effective-address generation, and it produces exactly the same si_code 128 as a #GP for a privileged instruction. Two different mechanisms, one number, and the artifact measures both so the pages can say so.
Outward. Debugging is where a compiler backend meets this page, and the meeting point is len: the four available breakpoints have a width each, so a char* can be watched at 1-byte granularity and a long at 8, and a struct larger than 8 bytes cannot be watched in one breakpoint. Every debugger you have used has a “watch this variable” feature whose width is silently rounded, and that rounding is this page's LEN field.