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.
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 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.
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.
int1 and the trap flag arrive as the same vector with different si_code.int1 gives 1 and setting EFLAGS.TF gives 2. That is the #DB row's two classes showing up as two signals, and it is the only direct evidence in this course that the class field is a property of the cause rather than of the number.int $N vectors give exactly 128. Vectors 1, 5, 6 and 14 have descriptor privilege level 0; the unused vectors 32–255 have no gate at all. A user process does not get to choose where a vector goes, only that the gate's DPL permits entry. Both failures are the same number, and the number is a #GP with no address.1; the read-only write, the PROT_NONE touch and the execute of a non-executable page are all 2. The CPU's error code has a write bit and an instruction bit in it and a demand-paging system needs both; ring 3 sees one bit, and it is the present bit.volatile long z = 0; d / z; and at -O2 gcc saw a division by a constant zero, removed the division, and the probe returned. The table would have said a divide by zero does not fault on this machine, and it would have been the compiler talking. A volatile on a locally initialised object is not enough; the value has to arrive from somewhere the optimiser cannot see, and the instruction has to be named.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.
$ ./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.
#XM on Intel and #XF on AMD. Same number, same class, two names. A kernel that logs the mnemonic string has to branch on the vendor to render it.#VE on Intel and reserved on AMD; 28 is #HV on AMD and reserved on Intel. Two different vectors for two different hypervisor facilities, on two different vendors, and a guest that takes either one unconditionally is broken on one of them.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.
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.)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.)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.)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.)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.