Keyboard Shortcuts

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

All Thirty-Two, As an x86-64 Reference

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

Why this matters

There are thirty-two vector numbers reserved for the processor's own use and the rest of a byte's range belongs to whoever installs a gate. Every number in the first thirty-two has a name, a class, and a decision about whether it pushes an error code — and roughly a third of those numbers mean nothing at all on the silicon you are running on.

The privilege course printed the thirty-two as a list. This concept prints them as a reference, and the difference is a column: every row says whether this course raised the exception on this machine or is quoting a document. Six rows say MEASURED. Twenty-six say QUOTED. That ratio is the honest state of the subject from ring 3, and a table without the column would have presented twenty-six quotations with the same authority as six observations.

The model: class, error code, and why the class is the interesting field

Every exception carries three things beyond its number: a mnemonic, a class, and whether it pushes a 32-bit error code. The class is the field that matters, because it decides where the saved instruction pointer points when the handler runs — and that is the only reason the class exists.

THE FOUR CLASSES, and the class is the ONLY reason the saved RIP differs. F fault saved RIP = the FAULTING instruction, which has NOT run T trap saved RIP = the NEXT instruction; this one HAS run A abort the saved RIP is UNDEFINED; the machine state is not reliable enough to return from I interrupt not an exception at all: an external event, async, and the handler returns to the next instruction - reserved nothing is defined One vector has two classes. #DB is a TRAP when a data breakpoint fires and a FAULT when a task switch with the TSS T bit does. The class is a property of the CAUSE, not of the vector.

The error code is a second, separate question, and it has its own shape per exception: the three segment faults and #PF push a selector-and-present word, #DF pushes zero, and #CP pushes a third format that is not the other two. What a ring-3 process gets instead of all of that is si_code, which is the kernel's translation — and section 4 of the artifact measures how much of the original survives the translation. One of six bits.

The reality: the six that can be raised, and what came back

Every arm below is a forked child with the signal handlers reset, and the result crosses the fork through a MAP_SHARED page the child's own handler wrote. Nine int $N vectors are in the table too, because the shape of their failure is the point.

$ ./sysdump | sed -n '/^  RAISE | ud2/,/^  RAISE | int \$0x01/p' \
    | sed 's/0x[0-9a-f]\{12\}/<per-run address>/'
  RAISE | ud2                                               | #UD 6   | sig 4  | 2    | 0x<per-run address> | the faulting INSTRUCTION, for SIGILL
  RAISE | int3                                              | #BP 3   | sig 5  | 128  | 0x<per-run address> | the RIP the kernel chose to report, for SIGTRAP
  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
  RAISE | write through a NULL pointer                      | #PF 14  | sig 11 | 1    | 0x<per-run address> | the exact ADDRESS, a mapping failure
  RAISE | write to a PROT_READ page                         | #PF 14  | sig 11 | 2    | 0x<per-run address> | the exact ADDRESS, a protection failure
  RAISE | touch a PROT_NONE page                            | #PF 14  | sig 11 | 2    | 0x<per-run address> | the exact ADDRESS, a protection failure
  RAISE | execute a page mapped RW and not X                | #PF 14  | sig 11 | 2    | 0x<per-run address> | the exact ADDRESS, a protection failure
  RAISE | READ a PROT_READ page, the control                | none    | sig 0  | 0    | 0x<per-run address> | the control: the page WAS readable
  RAISE | int $0x01                                         | #GP 13  | sig 11 | 128  | 0x<per-run address> | nothing: a #GP has no address
  RAISE | int $0x05                                         | #GP 13  | sig 11 | 128  | 0x<per-run address> | nothing: a #GP has no address
  RAISE | int $0x0e                                         | #GP 13  | sig 11 | 128  | 0x<per-run address> | nothing: a #GP has no address
  RAISE | int $0x41                                         | #GP 13  | sig 11 | 128  | 0x<per-run address> | nothing: a #GP has no address
  RAISE | int $0xff                                         | #GP 13  | sig 11 | 128  | 0x<per-run address> | nothing: a #GP has no address
                

The addresses are elided by the sed in that command rather than by an author, and the reason is worth stating once for the whole course: an address is not a measurement. This is a position-addressed binary with ASLR and a randomised heap, so every run produces different values, and a page that quotes one invites a reader to run the artifact, see a different number, and conclude that something changed when nothing did. What is worth quoting is the shape: si_addr = 0 for a #GP and for a null dereference, and a page address for the other three.

Four things in that table that are not in the textbook

Worked: the whole table, and the census that had to be made to add up

All thirty-two, with the two columns that make it a reference rather than a list. Abbreviated to the interesting columns — the full table is in the artifact and the harness asserts all thirty-two rows exactly:

$ ./sysdump | sed -n '/^  VEC | num/,/^  VEC | 31/p'
  VEC | num | mnemonic | class | error code | source | note
  VEC | 0   | #DE     | F     | no         | MEASURED | divide error
  VEC | 1   | #DB     | T     | no         | MEASURED | debug; TRAP for a breakpoint, FAULT for a task switch
  VEC | 2   | NMI     | I     | no         | QUOTED   | not an exception
  VEC | 3   | #BP     | T     | no         | MEASURED | INT3; saved RIP is the NEXT instruction
  VEC | 4   | #OF     | T     | no         | QUOTED   | INTO; saved RIP is the NEXT instruction
  VEC | 5   | #BR     | F     | no         | QUOTED   | BOUND; not encodable in 64-bit mode
  VEC | 6   | #UD     | F     | no         | MEASURED | invalid opcode; saved RIP is THIS one
  VEC | 8   | #DF     | A     | yes        | QUOTED   | abort; the saved RIP is UNDEFINED
  VEC | 13  | #GP     | F     | yes        | MEASURED | general protection
  VEC | 14  | #PF     | F     | yes        | MEASURED | page fault; the error code has its OWN format
  VEC | 19  | #XM     | F     | no         | QUOTED   | SIMD FP; AMD calls it #XF
  VEC | 20  | #VE     | F     | no         | QUOTED   | virtualization; Intel defines it, AMD RESERVED
  VEC | 28  | -       | -     | no         | QUOTED   | RESERVED on Intel; AMD calls it #HV
  VEC | 29  | #VC     | F     | no         | QUOTED   | VMM communication; Intel's own table says reserved
  VEC | 30  | #SX     | A     | no         | QUOTED   | security; Intel's own table says reserved
                

Six rows are measured: 0, 1, 3, 6, 13 and 14. The rest are quoted, and the reason is not laziness: deciding a class needs the saved RIP, and a ring-3 process never sees one, because the kernel does not put it in the siginfo. So the class column is quoted on all thirty-two rows even where the vector itself was raised, and the MEASURED in the source column means the fault happened and the signal number was read.

The vendor census, which is a partition and had to be made to be one

$ ./sysdump | grep '^  VENDOR |' | head -4
  VENDOR | reserved on BOTH 9 | Intel only 1 (#20) | AMD only 1 (#28)
  VENDOR | same number, different name 1 (#19) | defined on both 20
  VENDOR | and the four counts are a PARTITION: 20 + 1 + 1 + 9 + 1 = 32
  VENDOR | Two rows that the second draft got wrong and this one
                

Four categories, and they add up to thirty-two because a table of vendor differences that does not add up is a table of a typo. The first version printed 21, 1 and 0: it read row 28 twice, once as AMD-only and once as reserved, and counted #19 as “defined on both” rather than as the renamed row it is — because a number with two names is neither of the categories a three-column table offers. The harness asserts the partition, not the three numbers, so the class of mistake cannot recur silently.

And the one number here that is measured rather than cited: 9 of the 32 vectors are reserved on this part, and 6 more raised a real fault, so 15 are reachable to a user process and 17 are not. The gap is not a permissions decision. It is what the silicon implements, and no gate for #VE exists to make because the vector does not.

Apply it

  1. Reproduce the table and count the two columns.cd courses/x86sys/assets/samples && ./build_samples.sh, then grep -c '| MEASURED' sysdump.out. (Expect 6, and expect the vector numbers to be 0, 1, 3, 6, 13, 14 — not 0, 1, 3, 6, 14. #GP is measured in the third concept's boundary table, all thirty-nine times, and a table that has not noticed that is a table with a hole in it.)
  2. Break the partition deliberately. Change the census in sysdump.c so that row 28 counts as reserved and as AMD-only, which is the original bug. (Expect the harness to fail on the partition check and on nothing else — the individual counts will still look plausible. A census that fails on the sum but not on the columns is the only kind of check that catches this.)
  3. Make the divide-by-zero arm lie again. Change p_div0 back to volatile long z = 0; long r = d / z; and rebuild. (Expect the row to read RETURNED and the crosscheck to fail on that row. A probe that stops faulting without a diagnostic is the failure mode that costs a day, and this one has already cost this course one.)
  4. Add a vector you can actually raise. Take a program with a floating-point divide by zero and confirm the signal is the same SIGFPE that the integer divide produced. (Expect the same signal and, on this kernel, the same si_code of 1. Then confirm the saved RIP is the faulting divide for the integer case, by looking at where the handler's context points — which is the class, and the only way to see it.)
  5. Decide what your kernel does with a reserved number. Write a handler that logs every vector it receives for an hour of ordinary work. (Expect the interesting answer to be none of them on a well-behaved machine, and expect the interesting variant to be the machine that logs one you have never heard of. That is the argument for a kernel logging numbers rather than names: a name is a claim about a vendor, and a number is a fact.)

Connect

Backwards, the previous concept is the one way in and this one is everything that can come back out. The privilege course's vector concept is the parent of this page and the difference in the two tables is the source column; its error-code concept measured what ring 3 sees of the word the CPU pushed, and the second section of this page re-measures the same three page faults as a table rather than as a paragraph.

Forwards. The next concept is the table every #GP consults and the three comparisons it can fail, and the connection is si_code 128: a #GP has no error code the process can read, and 128 is what the kernel reports when there is nothing to report — the same 128 a non-canonical address produces, which is why the two are indistinguishable from ring 3 and why the sixth concept has to bisect for a boundary rather than read a signal. The fourth concept is about the registers that decide which of these gates the CPU will even look at.

Outward. This table is the one an operating-system port reads, and the one an embedded kernel's vector table is transcribed from. The row that costs the most time in practice is #14: it is the only vector in the table whose error code carries information a demand-paging system needs, and it is the one whose error code reaches userspace with one bit of six intact.

render_lesson_js(&mut page)