Keyboard Shortcuts

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

Numbers, and the Ones That Are Not Numbers

24 minadvancedModule 1: The Table and the NumbersContinues from priv-table
24 min · Exceptions, Privilege and Mode Changes

Why this matters

The previous concept found a table of 4096 slots and could not read any of them. This one asks what the first thirty-two are for, and the answer is more interesting than a list.

Two reasons to care. First, a number is not a name: vector 13 is #GP and vector 14 is #PF, and the difference between them changes whether your program's memory error is recoverable. Second — and this is the part that will cost you an afternoon if you do not know it — eight of the thirty-two mean different things depending on which company wrote the chip. Not “are spelled differently.” Different. One of them is a virtualization exception on Intel and a reserved vector on AMD; another is reserved on Intel and a hypervisor-injection exception on AMD. A kernel that reads the Intel table and runs on an AMD part has a bug that will appear on hardware that is not in the room.

The model: three classes, and why the class matters more than the name

Every vector 0–31 is one of three kinds, and the kind determines where the saved instruction pointer points. That single fact is the reason the class exists as a separate column:

FAULT the instruction did not complete. saved RIP points AT the faulting instruction. -> it can be re-executed once the cause is fixed. TRAP the instruction completed. saved RIP points AFTER it. -> resuming is safe, and a debugger can single-step. ABORT the machine state is unreliable and RIP is UNDEFINED. You cannot resume. INTERRUPT not an exception at all. It arrived from outside, asynchronously. saved RIP points after the interrupted instruction.

So when you read a manual that says “divide error”, the question you actually need answered is can I retry this? — and the answer lives in a column most tables put in small type.

$ ./privbench | sed -n '/vec   name  cls/,/^   19/p'
   vec   name  cls  errcode  note
   0     #DE   F    no       divide error
   1     #DB   T    no       debug; class depends on the cause
   2     NMI   I    no       not an exception
   3     #BP   T    no       INT3; RIP points at the NEXT instruction
   6     #UD   F    no       invalid opcode; RIP points at the FAULTING instruction
   8     #DF   A    yes      abort; the saved RIP is UNDEFINED
  14     #PF   F    yes      page fault; error code has its OWN format
  16     #MF   F    no       x87; saved RIP is the NEXT DEFERRED point, not the fault
  17     #AC   F    yes      alignment check
  19     #XM   F    no       SIMD FP; AMD calls it #XF

Three rows in that extract are worth more attention than the other thirty.

Vector 1, #DB, has no single class. A debug register that fires on an instruction fetch is a fault, so you may retry; one that fires on a data access is a trap, so you may not. The class is a function of the cause, and the manual's class column for vector 1 is not a simplification — it is the honest answer.

Vector 8, #DF, is the one that is not a number at all in the useful sense. It is a class of its own, it pushes an error code that is always zero, and its saved RIP is undefined because the double fault means the machine could not even complete the bookkeeping for the first one. Worse, if a second fault occurs while the double fault is being delivered, the processor shuts down. There is no third state. If your operating system's fault handler is itself faulting, you get exactly two chances and then the machine stops — which is the entire reason the architecture has this vector and the entire reason the “use a separate stack, via the IST field” mechanism exists.

Vector 16, #MF, is a trap whose saved RIP points somewhere unexpected. The manual is unusually explicit: the saved RIP is the x87 instruction that was about to be executed when the error condition was generated, which is not the instruction in which the error was detected. The real faulting address is in the FPU's own instruction-pointer register. This is a three-way distinction — the CPU pushed one address, the fault was in another, and the difference is in a floating-point register — and it is the clearest example in the whole architecture of a saved RIP that will not mean what you assume it means.

The reality: where the two vendors part company

The full table, with the disagreements marked in place:

$ ./privbench | sed -n '/vec   name  cls/,/defined on both/p'
   9     -     -    no       coprocessor segment overrun; RESERVED
  15     -     -    no       RESERVED
  20     #VE   F    no       virtualization: Intel DEFINES IT, AMD says RESERVED
  28     -     -    no       RESERVED on Intel; AMD calls it #HV
  29     #VC   F    no       VMM communication; Intel's Table 7-1 wrongly says reserved
  30     #SX   A    no       security; Intel's Table 7-1 wrongly says reserved
  31     -     -    no       RESERVED

      defined on both: 19    Intel only: 1 (#20)    AMD only: 1 (#28)

There are three distinct kinds of disagreement here, and treating them as one is how people get this wrong.

Hard divergence (vector 20). Intel defines #VE, a virtualization exception raised on an EPT violation when the “EPT-violation #VE” VM-execution control is set. AMD's table says plainly “20 Reserved.” Same number, same slot in the IDT, opposite meanings. A program that installs a #VE handler and enables the control will, on AMD hardware, catch something else entirely — or nothing.

An AMD-only concept (vector 28).#HV, hypervisor injection, has no Intel assignment at all. If you are writing to the intersection of the two architectures, vector 28 is available to you. If you are writing to a specific one, it is not.

An Intel documentation error (vectors 29 and 30). This one is worth dwelling on because it is a different category again. Intel's own Table 7-1 — the table every course quotes — says vectors 22–31 are reserved. But Intel defines #VC (VMM communication) at 29 and #SX (security) at 30 elsewhere in the same volume, in the hypervisor and trusted-execution chapters, and the Linux kernel has X86_TRAP_VC 29 in its own trap-number header. So the summary table in the authoritative manual contradicts the rest of the manual, and the summary table is the one people read.

What is safe to rely on, then? vectors 0..18 agree between vendors (19 is the same vector, named #XM or #XF) vector 19 same meaning, different name vector 20 HARD divergence vector 28 AMD-only vectors 29,30 real on both; Intel's Table 7-1 is wrong vector 21 #CP, agrees vectors 22..27 reserved on both vector 31 reserved on both

And one more piece of documentation unreliability, in the vector that never fires, which is almost charming: vector 9's class is stated three different ways by the same manual. The summary table calls it a fault; the reference section calls it an abort; a third table lists it among “benign exceptions.” It is moot — vector 9 has not been generated since the 486-era coprocessor, and AMD documents the condition as raising #GP today — but it is a clean demonstration that citing a table is not the same as citing a fact, and that the right response to a contradiction is to name it rather than pick one.

Worked: raise some of them for real

Reading a table is not the same as watching one happen. The artifact raises six of them on the actual CPU:

$ ./privbench | sed -n '/Now raise some of them/,/does NOT fault/p'
   Now raise some of them for real.  These are ACTUAL faults on THIS cpu:
   divq by zero            (#DE 0)    SIGFPE   si_code=1    (0x01)
   idivq by zero           (#DE 0)    SIGFPE   si_code=1    (0x01)
   ud2                     (#UD 6)    SIGILL   si_code=2    (0x02)
   int3                    (#BP 3)    SIGTRAP  si_code=128  (0x80)
   int $0x80               (vector 128) no fault
   int $0x28               (vector 40) SIGSEGV  si_code=128  (0x80)

Read the last two rows together, because they are the demonstration and the prose afterwards is the conclusion.

Vector 128 is inside 32–255, the user-defined range. This kernel owns vector 128 for the i386 system-call ABI, so int $0x80succeeded — it did not fault, it went to a real handler and came back with an answer. Vector 40 is also in the user-defined range, and this kernel has installed nothing there, so the process died.

The user chose the vector in both cases. The kernel decided what happened. That is the division of responsibility, and it is the whole reason the DPL check in the previous concept is a check against the kernel's table rather than against the caller's intent. A process may name a door; it may not choose what is behind it.

And note int3 giving si_code 128 rather than something more specific. si_code is the kernel's translation, and a breakpoint is a deliberate event with no fault classification to report. The error-code concept is about exactly this gap between what the CPU pushed and what a process can see.

Apply it

  1. Write a trap handler that retries. Which of vectors 0–31 would it be legal to resume from after fixing the cause? (The faults: 0, 5, 6, 7, 10–14, 16, 17, 19, 20, 21. Not the traps 3 and 4, not the aborts 8 and 18, not the interrupt 2, and vector 9 never fires anyway.) Then say what you would do about #MF specifically, given that its saved RIP is not the faulting instruction.
  2. Prove a vector is reserved, by triggering it.int 0xFF is in the user-defined range so the kernel handles it. But a reserved vector in 0–31 cannot be reached by int at all — write down why, in terms of the DPL the kernel would have had to install.
  3. Write the portability rule for your own target. Pick a vector you might use for your own signal-like mechanism. State which of the four portability classes it falls into (agreed / renamed / hard divergence / documentation error) and what you do about it. (For example: on a system with a hypervisor you care about, 20 and 29 are both live, on different vendors, for different reasons.)
  4. Find a fourth kind of documentation problem. The course names three here: a hard divergence, a vendor-only addition, and a summary table that contradicts its own volume. Look for a fourth in a manual you have access to — a number that is stated in two places and differs, or a class column that disagrees with a reference section. Write down what you would have believed if you had only read the summary.

Connect

Forward, the doors concept uses the fact you just established — that a process names a vector and the kernel owns it — to explain why SYSCALL is faster than int $0x80: it skips the table entirely, which is exactly why it also skips the DPL check. The error-code concept takes the “error code” column of this table and finds that it is not one format but three, and that #PF's is a bitfield with a bit whose name is a trap.

Backwards, the previous concept gave you the table these numbers index into. And the forward-reference chain is worth naming: the ISA course's opcode map already told you 0F 0B is UD2, a deliberately undefined opcode, and the memory course told you a page fault is the price of a cold access. Both were naming vector 6 and vector 14 without saying so. Every course in this chain touches these numbers; none of them needed to teach them until the course whose subject they are.

Outward: the JVM course teaches a software exception mechanism, and the comparison is exact and instructive. A JVM's exception table is a table of numbers indexed by program counter, and it is a fault/trap distinction in the same shape — a bytecode handler may resume at the same instruction, a Java handler may not. The JVM reimplemented the fault/trap distinction in an object model, which is why Java checked exceptions exist and why “finally” blocks are not optional in the language. Nothing in the JVM knows this table exists; it had no need to.

render_lesson_js(&mut page)