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.
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.
ESR_EL1 is CRn = 5, CRm = 2, op2 = 0. ELR_EL1 is CRn = 4, CRm = 0, op2 = 1. The shape of the instruction is identical and the register name is not, which is exactly what you would expect from an encoding that spends nine bits naming something and three bits on an operation.0xd538c004 XOR 0xd518c000 is bit 21 and the register field. So the mnemonic name is not computable from the bits — it is a table of fifteen four-field tuples, and this file's decoder carries the table rather than a formula. Concept 3 uses the same bit to install a vector table.ESR_EL1 and ESR_EL2 differ in op1 and in nothing else. A handler at EL1 and one at EL2 execute the same bytes to read the same conceptual register.ESR_ELx is a 64-bit register and the syndrome occupies bits 31:0. Bits 55:32 are named ISS2 and they are the field an SError handler needs. A handler that reads the register and masks 0xffffffff has thrown away half of the reason it took the exception. 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.
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.
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.
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.)
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.
And one trap in the table worth naming, because it is the row everybody forgets:
0x22 exists and is a Data Abort.0x22 is 0b100010 and 0x24 is 0b100100 — they differ in one bit of the EC field, and 0x24 is write while 0x22 is a read-or-write abort. A handler that tests for 0x24 and 0x25 and concludes "a write fault and a read fault" has used a table that is right about those two rows and silent about the third, which is the row a translation fault on a single instruction can also produce.0x24 and 0x25 are the ones a page fault arrives as, and they are the two the quoted pseudocode singles out for the IL rule. So the two most common exceptions on the machine are the two with the least trustworthy instruction-length bit.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.
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.)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.)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.)#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.)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.