Keyboard Shortcuts

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

One Register, Three Fields, and Thirty-Nine Meanings

26 minadvancedModule 1: The Modes: Getting In, Getting Out, and Being InterruptedContinues from a64-syscall
26 min · The AArch64 Machine: Modes, Memory and Faults

Why this matters

A kernel's exception handler is about forty instructions long and almost every one of them is a load from a system register. If you cannot read the value you loaded, the handler is a switch with a magic number in it, and magic numbers in a kernel are the difference between a five-minute diagnosis and a week. So the register layout is not trivia: it is the interface between a 32-bit field and everything the machine can go wrong with.

Before the layout, the constraint, because it decides what this page is allowed to claim. There is no AArch64 machine on the machine that wrote this course, no emulator, and no linker. Not one exception has ever been taken on this host. So the thirty-nine classes, the field widths, and the rule about the instruction-length bit are all QUOTED with a document and a section number beside them — and what is MEASURED-ON-BYTES is the encoding of the three instructions that read the three registers, and MEASURED is the arithmetic the compiler emits once it has the value.

That split is the page. A reader who blurs it is being taught a table they have never seen fire, and a reader who holds it learns something transferable: in a kernel, the layout is a document and the code is a measurement, and the two are checked by different means.

The model: a 64-bit register with a 32-bit syndrome in it

Start with the two instructions a handler begins with, and read the words as words:

$ python3 a64sys.py --section 3 | sed -n '/MRS and MSR/,/^$/p' | head -12
  [MEASURED-ON-BYTES] MRS and MSR: the register is a FOUR-field name
  source            word        L  op0 op1 CRn CRm op2 Rt   decoder says
  mrs x0, esr_el1   0xd5385200  1  11 000 5   2   0   0    mrs      x0, ESR_EL1
  mrs x1, elr_el1   0xd5384021  1  11 000 4   0   1   1    mrs      x1, ELR_EL1
  mrs x2, far_el1   0xd5386002  1  11 000 6   0   0   2    mrs      x2, FAR_EL1
  mrs x4, vbar_el1  0xd538c004  1  11 000 c   0   0   4    mrs      x4, VBAR_EL1
  msr vbar_el1, x0  0xd518c000  0  11 000 c   0   0   0    msr      VBAR_EL1, x0
  mrs x9, esr_el2   0xd53c5209  1  11 100 5   2   0   9    mrs      x9, ESR_EL2
                

Four things fall out of that block, and the first is a retraction on a claim in the plan this course was built from.

R1, and it is a WIDTH, which is the hard kind DRAFT "the syndrome is a 32-bit field: EC in the high six bits, IL next, and the rest is one field called ISS." QUOTED ESR_ELx[63:56] reserved ESR_ELx[55:32] ISS2 <- SError, and later extensions ESR_ELx[31:26] EC 6 bits ESR_ELx[25] IL 1 bit ESR_ELx[24:0] ISS 25 bits The register is 64 bits. The syndrome is 32. Both statements are true and the second one does not follow from the first, which is exactly why a 32-bit mask in a handler is a bug that a review of the layout will not catch.

The layout, quoted with its source

  DDI 0597, Shared Pseudocode, AArch64_ExceptionClass():

    ESR_ELx (target_el) = ( Zeros{8} :: iss2 :: ec[5:0] :: il :: iss );
                                                 ^^^^  ^^  ^^^^^
                                                 EC   IL  ISS

    and, verbatim in structure:

    if ec IN {0x24, 0x25} && iss[24] == '0' then
        il = '1';
    end;
                

Read that last line carefully, because it is the concept. For the two Data Abort classes a page fault arrives as, the instruction-length bit is not a fact about the instruction. It is forced to 1 to say there is no valid instruction syndrome. A handler that prints IL as "32-bit instruction" for such a fault is printing something the architecture told it to print and something that may be false.

The measurement: what the compiler does with the value

Here is the part that can be measured on a host with no AArch64 silicon, and it is more interesting than expected. The corpus contains a C function per field extraction, and the instructions are the evidence:

$ python3 a64sys.py --section 5 | sed -n '/the packed field/,/the instruction cost/p'
  [MEASURED-ON-BYTES] the packed field, and the arithmetic a handler does on it
     --- O2 ---
       ec_of                 2   lsr x0, x0, #26 ; ret
       il_of                 3   lsr x9, x0, #25 ; and x0, x9, #1 ; ret
       iss_of                2   and w0, w0, #0x1ffffff ; ret
       is_lower_data_abort   4   (see below)
       dfsc_of               2   and x0, x0, #0x7f ; ret
                

Those are the obvious forms and they are what a page would normally teach. Now the one that is worth reading twice:

$ python3 a64sys.py --section 5 | sed -n '/the one that is worth reading twice/,/^$/p'
  [MEASURED] the one that is worth reading twice
     mov	w8, #-1879048192                // =0x90000000
     and	x9, x0, #0xf8000000
     cmp	x9, x8
     cset	w0, eq
     ret
                

The C is return (ec == 0x24 || ec == 0x25); — two comparisons. clang emitted one mask, one compare and one conditional set. The reason is arithmetic you can do in your head: 0x24 is 0b100100 and 0x25 is 0b100101, so the two classes differ in one bit of the EC field and in nothing else. ANDing with 0xf8000000 clears that bit and collapses both cases into one value to compare against.

R7, and the lesson is about the SHAPE of the constants OBVIOUS lsr x0, x0, #26 cmp x0, #0x24 b.eq .yes cmp x0, #0x25 b.eq .yes ... SIX or more MEASURED and x9, x0, #0xf8000000 clears bit 27 of EC cmp x9, #0x90000000 cset w0, eq THREE The mask is not in any manual, because it is a property of the two CONSTANTS and not of the architecture. A page that taught the syndrome with the shift would have taught a correct reading and a compiler-shaped one, and a reader writing a handler in that shape would be writing worse code than the compiler would have written for them. At -O0 the same C is THIRTEEN instructions.

And the other measurement on this page, which is a register choice rather than an instruction count: a handler that reads all three registers at once gets x8, x9 and x10, not x0, x1 and x2.

     esr_all, -O2:
     mrs	x8, ESR_EL1
     mrs	x9, ELR_EL1
     mrs	x10, FAR_EL1
     eor	x8, x9, x8
     eor	x0, x8, x10
     ret
                

The AAPCS64 answer is x0, and using it for a temporary here would be legal and confusing. A reader who has just read the ABI course sees a handler reading three system registers into x8–x10 and learns something about the compiler that no document says.

The seven refusals

Seven identical diagnostics, and they say the same thing seven times:

$ python3 a64sys.py --section 3 | sed -n '/Nine refusals/,/^$/p'
  [MEASURED-ON-BYTES] ... Nine refusals, one diagnostic:
     svc #65536         REFUSED   immediate must be an integer in range [0, 65535].
     svc #-1            REFUSED   immediate must be an integer in range [0, 65535].
     brk #65536         REFUSED   immediate must be an integer in range [0, 65535].
     hlt #-1            REFUSED   immediate must be an integer in range [0, 65535].
     mrs x0, esr_el0    REFUSED   expected readable system register  matches the table
     mrs x0, elr_el0    REFUSED   expected readable system register  matches the table
     mrs x0, far_el0    REFUSED   expected readable system register  matches the table
     mrs x0, vbar_el0   REFUSED   expected readable system register  matches the table
     mrs x0, tcr_el0    REFUSED   expected readable system register  matches the table
     mrs x0, ttbr0_el0  REFUSED   expected readable system register  matches the table
     mrs x0, sctlr_el0  REFUSED   expected readable system register  matches the table
                

ESR_EL0 and its six siblings do not exist, and that is not a naming convention the assembler invented — it is the architecture saying EL0 has no syndrome register, because EL0 cannot take an exception it has to describe. A reader who assumed esr_el0 would assemble and found out at three in the morning has learned the rule the seven identical diagnostics were teaching all along. (The eighth row, mrs x0, Sctlr, is refused for a different reason: the EL-agnostic spelling does not exist on this architecture at all.)

Worked: the field is a discriminated union, and the table is the point

This is the one screen in the course that is worth keeping, and it is entirely QUOTED. Thirty-nine rows, abbreviated. The left column is the discriminant and the right column changes completely depending on it:

  EC    meaning                      the ISS [24:0] means
  ----  ---------------------------  --------------------------------
  0x00  Unknown                     nothing -- the syndrome is undefined
  0x01  WFI/WFE retirement          a 5-bit register and a 2-bit state
  0x02  SMC                         nothing
  0x03  SVC (a syscall from EL0)    imm16 -- the LITERAL in the SVC
  0x0c  BRK                         imm16
  0x11  SVC (from EL1)              imm16
  0x12  HLT (from EL1)              imm16
  0x17  SVC (from EL3)              imm16
  0x18  HLT (from EL3)              imm16
  0x20  Instruction abort, lower EL the FAR, and the DFS
  0x21  PC alignment fault          nothing
  0x22  Data abort, lower EL        the FAR, and the DFSC
  0x24  Data abort, lower EL, WRITE the FAR, and the DFSC
  0x25  Data abort, lower EL, READ  the FAR, and the DFSC
  0x2c  FP exception                the status and a 4-bit index
  0x2e  SError                      the FAR, the WnR bit, the DFSC
  0x32  IRQ                         nothing
  0x33  FIQ                         nothing
  0x34  SError interrupt            nothing
                

There is no bit of the ISS that means the same thing in two rows of that table. For EC 0x03 it is the sixteen-bit constant that was in the SVC — and concept 1 measured that constant in the instruction, so a handler that sees EC 0x03 can tell you which syscall trapped by reading bits 24:5 of the syndrome, without going back to the instruction at ELR. For EC 0x25 the same twenty-five bits are a fault address, a read/write flag, a fault-status code and an overflow flag.

THE CONCEPT IN ONE SENTENCE The ISS is not a NUMBER. It is a DISCRIMINATED UNION, and the discriminant is the six bits above it. A decoder that treats ESR_EL1 as an integer has got the width right and the MEANING wrong, and the wrongness is silent: every value it prints is a valid 32-bit number, and the number it prints for a page fault is the number a syscall would have printed.

And one trap in the table worth naming, because it is the row everybody forgets:

What this page cannot show

It cannot show an exception. Not one of the thirty-nine rows above has been observed happening, because there is no AArch64 machine, no emulator and no linker on the build host. The classes, the field widths, the per-class meanings and the IL rule are quoted from DDI 0597 with section numbers. The instructions that read the three registers and the arithmetic clang emits around them are measured, on bytes, and cross-read by both readers. The gap between those two is the gap between a table you look up and a sequence you can read, and concept 6 prints the whole boundary. The one thing this page measures that a table could never tell you is the three-instruction test — and it is measured because the constants in the table make it possible, not because the architecture says so.

Apply it

  1. Read a syndrome out of a register and check the field positions yourself. Take a value like 0x96000050 — a Data Abort from a lower EL, 32-bit instruction — and extract EC with (v >> 26) & 0x3f, IL with (v >> 25) & 1 and ISS with v & 0x1ffffff. (Expect EC = 0x25, IL = 1, ISS = 0x50, and then look up what DFSC = 0x10 means in the architecture manual: it is a synchronous external abort, not on a translation table walk. Now check bit 6 of the ISS, which is WnR — write-not-read — and see whether your example is a read or a write. Three fields, one register, and the answer to "was it a read?" is inside the union.)
  2. Find the seven refusals and then find the seventh row. Try mrs x0, esr_el0, mrs x0, elr_el0, mrs x0, far_el0, mrs x0, vbar_el0, mrs x0, tcr_el0, mrs x0, ttbr0_el0, mrs x0, sctlr_el0, and then mrs x0, Sctlr. (Expect seven refusals with expected readable system register and an eighth with the same message for a different reason. Then ask the question the refusals imply: where does an EL0 signal handler get its numbers? The answer is that the kernel put them in a siginfo and that the signal number is a Linux number, not an EC — the same division as concept 1's field-versus-convention.)
  3. Write the handler two ways and count the instructions. Compile return (ec == 0x24 || ec == 0x25); at -O0 and -O2. (Expect three instructions at -O2 — a mask, a compare and a cset — and about thirteen at -O0. Then change the constants to 0x22 || 0x24 and recompile: those two differ in a different bit, so expect a different mask, and expect the compiler to find it again. The generalisation is that a compiler can only exploit a coincidence in the constants, so the shape of your constant set is part of the shape of your generated code.)
  4. Write a decoder for the syndrome and test it against the table. Twenty lines: mask EC, IL, ISS, and print the class name from a 39-entry table. (Expect it to work on every class and to be wrong in a way you cannot detect for class 0x00, where the ISS is undefined and your code will print a number anyway. That is the same failure mode as the encoding course's HINT bug: a decoder that produces a plausible value for something that has none. A count of unmodelled words is a fact about a corpus, and a count of undefined fields is a fact about a specification.)
  5. Compare the AArch64 and x86-64 error reports and count what each gives you.The x86-64 exception concept gives you a #PF with a 14-bit error code whose bits are documented individually. Build the AArch64 equivalent: EC as a discriminant, then a per-class decoder. (Expect x86-64's report to be a single fixed-layout field and AArch64's to be a tagged union, and expect the tagged union to be strictly more informative when the class is rare — a class that does not exist on x86-64 has a five-bit code on AArch64 saying so, and a 14-bit x86-64 code that is reserved says nothing. The price is that you have to write the decoder; the x86-64 price is that you have to read the reserved rows.)

Connect

Backwards, specifically. Concept 1 measured the sixteen-bit constant that EC 0x03 reports back to you, so the two concepts close a loop: the number you put in the instruction is the number the syndrome gives you. The encoding course owns the class field at bits[28:25] and the dispatch order this page depends on — read its cross-check concept first if the terms CRn and op2 are new, because this page's whole measured half is about how a register name lives in nine bits. The ABI course's register concept explains why a handler's temporaries land in x8–x10 and not x0–x2.

Sideways, three neighbours that own the edges. The neutral course owns the vector model — what an exception is, in terms any architecture can use — and the x86-64 exception concept is the sibling reference for the same subject, where the report is a fixed-layout error code rather than a tagged union. The paging course owns what a translation fault is; the DFSC values in the table above are where that course's subject arrives as a number, and the connection runs both ways — a reader who has read the paging concept can look up DFSC 0b000100 and know what a level-1 translation fault looks like before ever seeing a table.

Outward, and the general form is the thing to carry. A register layout is a document; the code that reads it is a measurement; and the gap between them is where kernel bugs live. You cannot run the document. You can count the instructions the code takes, and the two results are of different kinds and must be reported as such. This is the same discipline concept 6 applies to the whole course, and it is the reason that concept exists: a course about a machine that was never here has exactly one advantage over a course about one that was, and the advantage is that it is forced to be explicit about which claims are which kind.

Forward, one promise. Concept 3 is about where the handler goes, and the connection to this page is a single instruction: the ELR_EL1 you read here is the address the CPU will return to, and the vector table of concept 3 is the address it goes to first. Two registers, one mechanism, and the order in which you read them is the order in which the machine used them.

render_lesson_js(&mut page)