Keyboard Shortcuts

Ctrl+K
Open search
Esc
Close search / dialog
↑
Back to top

DR0 to DR7 and the Mask a Debugger Must Program

24 minadvancedModule 1: The Machine's Privileged Side, as an InventoryContinues from x86-cr
24 min · The x86-64 Machine

Why this matters

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 model: four addresses and one control word

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.

THE LENGTH FIELD, and this is the trap. LEN | 00 | 1 byte LEN | 01 | 2 bytes LEN | 10 | 8 bytes, ONLY defined in 64-bit mode LEN | 11 | 4 bytes READ THAT AGAIN. The two-bit code for FOUR bytes is 11, and the code for EIGHT is 10. 10 means LONGER. A table that says 10 = 4 bytes is not a typo in a table; it is a broken debugger, because the mask will be accepted by the hardware, the breakpoint will be set, and it will fire on a different width than the one that was asked for -- or not fire at all, depending on which way the mistake went. And the other half: R/W | 00 | instruction EXECUTE R/W | 01 | data WRITE R/W | 10 | I/O read or write, ONLY R/W | | defined while CR4.DE=1 R/W | 11 | data READ or WRITE

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.

The reality: the mask, computed rather than quoted

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.

The two layouts, and why the file cannot say which is right

$ ./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.

Worked: the one debug mechanism ring 3 can reach

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:

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.

Apply it

  1. Recompute the mask by hand and then let the artifact do it.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.)
  2. Reproduce the 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.)
  3. Find a real debugger's constant. Search a debugger source for the 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.)
  4. Single-step from user mode and count the signals. Set the trap flag, execute forty instructions, and count the 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.)
  5. Decide what your kernel's GD bit does.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.)

Connect

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.

render_lesson_js(&mut page)