Keyboard Shortcuts

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

The Same Idea Under Three Architectures

25 minadvancedModule 3: What Mode 3 Is Made OfContinues from priv-errorcode
25 min · Exceptions, Privilege and Mode Changes

Why this matters

“Ring 0” is a vendor spelling. A course that teaches the mechanism and then says “x86 calls it ring 0” has taught one vendor's vocabulary and left you unable to read the other two manuals, which is a strange thing to do to a reader who will eventually need them.

The reason this is worth a whole concept rather than a footnote is that the three designs genuinely differ, not just in naming. x86 puts the privilege level in a segment register and compares it against a field in a table. AArch64 puts it in the current exception level and compares it against a per-level vector base. RISC-V puts it in two bits of a status register and makes the question of who handles a given trap a writable field that firmware sets. Those are three different answers to the question “where does the privilege decision live,” and the third one is the interesting one.

The model: the five-part shape, and the three spellings

Strip the vocabulary away and every one of these machines has the same five things:

1. A TABLE the kernel owns and the user cannot read 2. A per-cause REASON CODE 3. A rule for WHO handles a given cause 4. A way to get BACK 5. A way for the kernel to PROVE the fault came from user mode Change the numbers. Keep the five.

Now the three.

x86-64
  levels         rings 0..3.  Only 0 and 3 are used by
                 an operating system today; 1 and 2 exist
                 for the historical OS/2 split.
  the table      IDT, 16 bytes per entry, indexed by
                 vector*16.  One for the whole machine.
  the rule       IF gate.DPL < CPL THEN #GP(vector,1,0)
                 The level is a NUMBER in CS, compared
                 against a FIELD in a table.
  the drop       SYSCALL.  No check at all; CPL := 0.
  the return     SYSRET, which is #GP(0) at CPL != 0.
                 SYSEXIT is #GP(0) at CPL != 0.
                 The return has a check and the entry
                 does not -- backwards from intuition.
  user proof     CS.RPL on the saved frame; error-code
                 bit 2 (U/S) on #PF.

AArch64
  levels         EL0..EL3.  MORE levels than x86 has
                 rings, and they are asymmetric in a way
                 rings are not: EL2 is a hypervisor, not
                 "the second OS".
  the table      VBAR_ELx.  ONE PER LEVEL.  A vector
                 table, like the IDT, but the register
                 you read it from depends on where you
                 are.
  the rule       PSTATE.PAN.  With PAN=1, EL1+ may not
                 access EL0 data.  Structurally the same
                 job as SMAP, and reached with a
                 different instruction: STAC/CLAC set
                 and clear the permission, exactly as
                 x86's STAC/CLAC toggle EFLAGS.AC.
  the drop       SVC #imm16 -- an ORDINARY instruction
                 at any EL.  A 16-bit immediate, not a
                 register, so no argument marshalling in
                 the instruction at all.
  the return     ERET.
  the record     THREE CSRs, and this is the big one:
                   ESR_ELx   the syndrome: which exception
                             AND which of its many causes
                   FAR_ELx   the faulting address
                   ELR_ELx   where to return to
                 x86 squeezes the same three things into
                 a stack frame plus CR2.  AArch64 keeps
                 them in registers.
  categories     Four, not three: from a lower EL, from a
                 higher EL, from the current EL with SP0
                 selected, and from the current EL with
                 the process stack.  That last distinction
                 is about WHICH STACK the exception landed
                 on, and it is a fourth thing to get right.

RISC-V
  modes          M, S, U.  And this is the one that
                 differs most.
  the mandatory   M mode is MANDATORY.  Every RISC-V hart
                 implements it.  So there is a mode BELOW
                 user mode by construction, and it is
                 not optional and not virtualised away.
                 Nothing equivalent is true on x86 or
                 AArch64, where the lowest mode is
                 unprivileged.
  the table      mtvec, stvec, utvec.  ONE PER MODE.
  the rule       medeleg and mideleg.  Two registers,
                 one bit per exception CAUSE, saying
                 whether S or U handles it.
                 x86 hardwires the split at CPL 0.
                 AArch64 hardwires it at the EL boundary.
                 RISC-V makes it a FIELD FIRMWARE WRITES.
  the drop       ECALL (and EBREAK for a breakpoint).
  the return     MRET, which restores privilege from
                 mstatus.MPP.
  the record     Three CSRs again, and the naming is
                 better than either of the others:
                   mcause   the reason, an ENUM
                   mtval    the offending value, and
                            OPTIONAL per cause -- the
                            spec says it may be written
                            as zero where there is nothing
                            meaningful to put there
                   mepc     where to come back to
  the one to     mstatus itself.  MPP records the
  not skip       privilege to return to, and the handler
                 must save it before it does anything
                 else at all.  Get that wrong and the
                 return drops to the wrong mode.

Two of those are worth a second look, because they are better ideas and they are portable.

RISC-V's mtval being optional per cause is a genuinely better design than x86's, and it is worth copying. x86's #PF error code is a fixed-format bitfield whether or not every bit is meaningful for the event that occurred, and bit 3 means “a reserved bit in a paging entry” — which is a completely different kind of thing from every other bit, sharing a field with them. AArch64's syndrome is explicitly a combination of an EC (exception class) and an ISS (instruction-specific syndrome) whose meaning depends on the class, which is the same idea done properly: the low bits are generic, the high bits are interpreted according to what kind of event this was. Three architectures, one field, and the two newer ones both decided that the meaning of the bits should depend on the class of the event.

RISC-V's delegation as a writable field is the other. It means an implementation can choose where a given trap is handled, and firmware can be changed to match the platform, without an ISA change. x86 and AArch64 fix the boundary in silicon. That is a real trade — a field firmware writes is a field firmware can get wrong, and a misconfigured delegation is a security bug — but it is the more flexible of the three designs and it is the direction the industry has been moving.

The reality: what the portability rule actually is

Strip the three tables down to a single line each, and the portable content is the shape rather than the number:

x86-64 the level is a NUMBER in a segment register, compared against a field in a table AArch64 the level is a NUMBER in the current EL, compared against a per-level vector base RISC-V the level is a PAIR OF BITS in a status register, and which level handles a given trap is DELEGATABLE, per cause

And here is what a compiler author actually needs from this, in the order they need it:

  1. Which instruction enters the kernel, and does it need a marshalling convention? x86: SYSCALL, and yes — a convention exists and it is the operating system's, not the architecture's. AArch64: SVC, and the immediate is a 16-bit constant, so the number is in the instruction and the arguments go in the ordinary registers. RISC-V: ECALL, with the number in a7 and arguments in a0–a5 — and a7 is the seventh register, chosen so that the first six arguments have the same numbers as x86's. Every ISA arrived at a register-based convention; only x86 had to bend one because the hardware took two registers first.
  2. Which registers are clobbered? x86: RCX and R11, chosen by the hardware and impossible to change. AArch64 and RISC-V: none, because the convention does not have to dodge a return address — you simply keep the return address somewhere else.
  3. How does the return happen, and what checks it? All three have a dedicated return instruction. Only x86's has a privilege check on it (SYSRET is #GP(0) at CPL = 0), and that asymmetry is a consequence of the design rather than a decision: because SYSCALL checks nothing, its return cannot rely on the entry having established anything.

The one-line portability rule that survives all three: the entry convention is not the architecture's, it is the operating system's, and it will outlive every kernel you have ever heard of. Emit against the ABI your target kernel documents, and treat the architecture manual as a source of constraints on that ABI rather than as a definition of it. The convention concept showed that on x86-64 this means five of the six facts come from Linux; on the other two machines the split is different, and the reason is the same.

Worked: the question to ask on a machine you have

You have an AArch64 or RISC-V machine, or a cross-compiler and an emulator. Do this, and it takes about twenty minutes.

AArch64 1. Read VBAR_EL1 out of /proc or with a debugger. Compare it to the IDT base you found in the x86 concept. Both are "where the gates are", both are per-mode, and on AArch64 there are four of them and you can only read one. 2. Find the vector table's entry for the exception you care about. It is a pair of 64-bit instructions: the branch, and the handler address. Not a 16-byte descriptor with a DPL in it -- there is no DPL. The check is PSTATE.PAN plus the EL transition, and it lives in the instruction that raises the exception, not in a table. 3. Ask: which stack did it land on, and who chose? RISC-V 1. Read medeleg and mideleg. They are in the machine-mode CSRs and you will need M-mode or a debugger to see them, which is itself the point: the delegation is a machine-mode register and a user-mode process cannot influence it. Compare with x86, where the equivalent decision is baked into the gate's DPL and a user-mode process at least gets a #GP if it guesses wrong. 2. Read mstatus.MPP in a trap handler. This is the field the x86 course could not show you, because x86 has no equivalent: the hardware does not record where it came from in a register you can read, it records it in CS. 3. Ask: is M-mode mandatory on this hart, and does this platform run an OS at all? M-mode is mandatory even on a hart with no OS on it.

And the question that ties all three together, which is the one worth carrying into the next course in this chain: in every case, the lowest privilege level is unprivileged, and in one case there is a mode below it that is not optional. That asymmetry is the difference between a design where the kernel is a guest and a design where the kernel is the ground floor. Both are legitimate; they answer different questions about what you are building on top.

Apply it

  1. Build the table yourself for a fourth architecture. A 32-bit ARM (ARMv7-A with the secure extensions) or an older PowerPC. (32-bit ARM is the interesting one: it has banked registers for the modes, so the privilege level determines which register bank you are in, which is a design neither of these three uses.)
  2. Answer the portability question for your target. Which of the five parts of the shape is your operating system responsible for, and which is the hardware's? (For x86-64: the table is the kernel's, the reason code is the hardware's, the rule is the hardware's, the return is split, and the user-mode proof is the hardware's but the report is the kernel's. Notice that part 1 and part 5 both leak across the boundary in different ways on different machines.)
  3. Argue the optional mtval design. Write two sentences for why “this field may be zero depending on the cause” is better than a fixed-format bitfield, and two for why it is worse. (Better: a fault that has no meaningful value does not have to invent one, so the zero is honest. Worse: a handler that reads it without checking the cause has no way to tell “zero” from “not applicable,” which is the same problem as a NULL return from a function that cannot fail.)
  4. Find something all three architectures do the same way that is not on the list of five.(Hint: all three have a defined reset state, all three have a way to enter the highest mode from the lowest, and all three have had at least one documented case where a fault in the handler is itself fatal. The third is the interesting one, and it is why RISC-V mandates that M-mode traps cannot be delegated.)

Connect

Forward, the harness concept closes the course by asserting which of these claims were measured and which were quoted — and the fact that this concept is the one where that distinction matters most is the argument for having the harness at all.

Backwards, this concept reads the other seven as one instance of a design and asks how many other instances there are. The vector concept's Intel-versus-AMD divergences are one data point: even within one architecture, “the manual's number” is not always portable. The doors concept'sSYSENTER result is a second, and a stronger one, because there the divergence is not a name but an instruction's existence. The error-code concept's three formats are a third: the same concept, expressed three ways, with the newest design making the best choice.

Outward, the collection has a gap this concept makes visible. Everything in the last four concepts is about a single processor crossing a boundary. The memory course's limits block named its own exclusion explicitly — cache coherence, NUMA, and multiprocessor memory are the subject of the Multiprocessor Architecture course, which does not exist yet. And the thing this course has just taught transfers to that one intact: a table the kernel owns, a reason code per event, and a rule about who handles it are the shape of a cache-coherence protocol too, and the same discipline applies — read the second vendor's manual, quote the shape rather than the number, and check the field order.

And the practical one. If you write a compiler backend, this concept is the one that saves you: the register convention for entering the kernel is not the architecture's, the number is not the architecture's, and the clobbered registers are the architecture's and you cannot change them. Five of the six facts in the x86-64 system-call ABI are an operating system's decision, and every one of the three architectures here has made a different set of them.

render_lesson_js(&mut page)