Keyboard Shortcuts

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

The Class Field Is Four Bits, and the Field Map Is Measured

30 minadvancedModule 2: The Encoding: What the Bytes AreContinues from a64-asm
30 min · AArch64: Encoding From The Ground Up

Why this matters

The last concept established that the length is a constant, and the immediate consequence is that everything else has to fit in the remaining 32 bits with no escape. There is no second word to fall back into. So the field map is not a convenience on this architecture — it is the curriculum, and a decoder is a decision tree over a fixed width rather than a length table followed by an opcode table.

Which makes the field map the thing worth being careful about, because it is exactly the thing a person reads out of a manual once and then trusts. This concept measures it instead: 58 field positions, each one obtained by assembling an instruction, changing one operand, and OR-ing the XOR over a sweep of variants. The guards in the artifact are built from that table, and the table is written to fields.txt so you can check it.

The reason a measurement rather than a reading, in one line: a field map read out of a manual is a claim, and a claim cannot be debugged.

The class field: four bits, and eleven of sixteen

Four bits at position 25 are the first thing a decoder looks at. The class names are quoted from the architecture's reference manual; the class membership is measured, by taking the instructions out of five object files, reading four bits out of each, and asking llvm-objdump-21 what that instruction is:

$ ./a64dec.py --section=3 | sed -n '/bits    n /,/Empty here/p'
  bits    n      what landed there, and how often        the QUOTED name
  00100   23     ldp:15 stp:8                            Loads and Stores (pair)
  00101   111    add:40 mov:34 subs:6 cmp:5 neg:4        Data Processing -- Register
  01100   151    ldr:83 str:59 ldur:2 ldrb:2 ldrsw:2     Loads and Stores
  01101   101    csel:22 cset:14 mul:10 madd:10 lsl:5   Data Processing -- Register
  01011   142    ret:134 tbz:4 br:1 blr:1 tbnz:1         Branches, Exception Gen and Sys
  01001   134    mov:45 and:28 movk:19 lsl:9 orr:8      Data Processing -- Immediate
  01000   114    add:41 sub:31 cmp:20 subs:11 mov:4     Data Processing -- Immediate
  01010   84     b:27 bl:10 b.ne:8 b.eq:7 cbz:6          Branches, Exception Gen and Sys
  00110   1      ldp:1                                  Advanced SIMD (pair)
  00111   6      add:3 movi:2 addv:1                     Advanced SIMD
  01111   1      fmov:1                                 Advanced SIMD / FP

  11 of the 16 values carry at least one real instruction in this
  corpus.  Empty here: 00000, 00001, 00010, 00011, 01110.
                

Two numbers to take from that. Four bits, not two. The two-bit rule this decoder first used put ret in “Data Processing — Register”, because bits[28:26] of 0xd65f03c0 is 0b110; ret is 134 of the 868, the single largest entry in the distribution. And 01101 is a class a two-bit rule cannot see at all — it holds csel, cset, mul and madd, 101 instructions. That is not cosmetic: 01101 and 00101 are different code paths through a decoder, with different guards and different operand layouts.

And then the honest caveat, which is the part a caption usually leaves out. Five of the sixteen values are empty in this corpus, and four of those five carry SIMD. That is a fact about 25 functions of ordinary C, not about the architecture. The correct statement is that a decoder may implement 11 of the 16 classes and be correct on every instruction in this corpus — and the 5 empty values are values whose contents this corpus did not exercise, not "unimplemented classes". A class field is four bits wide whether or not a program uses it.

The method, and the mistake that made it necessary

One line long: assemble an instruction, change one operand, assemble it again, and every bit that moved belongs to that operand.

The first version of this measurement was wrong in a way that looked like a result, and the mistake is the most useful thing on this page. A single pair marks the bits where the two values differ, which is a subset of the field and not the field. Measured with one pair, movz w0, #0x1111 against movz w0, #0x2222 gives an 8-bit imm16 — because 0x1111 and 0x2222 happen to differ in eight bit positions — when the field is sixteen bits wide. The table looked wrong because the table was.

$ ./a64dec.py --section=4 | sed -n '/^  field /,/imm16/p'
  field              xor mask    the positions         base instruction
  Rd                 0000000f    bits[3:0]             add w0, w1, w2
  Rn                 000001c0    bits[8:6]             add w0, w1, w2
  Rm                 000f0000    bits[19:16]           add w0, w1, w2
  sf                 80030001    bits[0,16,17,31]      add w0, w1, w2
  op                 40000000    bits[30:30]           add w0, w1, w2
  S                  20000000    bits[29:29]           add w0, w1, w2
  shift amt          0000fc00    bits[15:10]           add x0, x1, x2, lsl #0
  shift type         00e0e000    bits[13,14,15,21,22,23]  add w0, w1, w2, lsl #3
  opc                60000000    bits[30:29]           and w0, w1, w2
  imm12              003ffc00    bits[21:10]           add w0, w1, #0x0
  imm16              001fffe0    bits[20:5]            movz w0, #0x0000
  hw                 00600000    bits[22:21]           movz x0, #0x1111
                

The fix is to OR the XOR over a sweep of variants, one per single bit of the operand, so every position the operand can reach is marked. And the artifact is explicit about what the result is: a mask is a LOWER BOUND. Every bit in it really does belong to that operand; a bit not in it is a bit the sweep did not happen to move. The Rn row above is four bits wide and the field is five, because the sweep visited odd register numbers and never moved bit 5 — and the file prints the not-moved bits per row rather than rounding the mask up to a span.

Four rows in that table that are worth reading slowly

$ ./a64dec.py --section=4 | sed -n '/^  pair Rt2/,/pair scale/p'
  pair Rt            0000001f    bits[4:0]             stp x0, x1, [sp]
  pair Rt2           00007c00    bits[14:10]           stp x0, x1, [sp]
  pair Rn            000003e0    bits[9:5]             stp x0, x1, [sp]
  pair imm           001f8000    bits[20:15]           stp x0, x1, [sp]
  pair mode          01bf0000    bits[16,17,18,19,20,21,23,24]  stp x0, x1, [sp]
  pair L             00400000    bits[22:22]           stp x0, x1, [sp]
  pair scale         84000000    bits[26,31]           stp w0, x1, [sp]
                

What a mask is not

Two of the rows in that table are not contiguous, and the gaps are not measurement failures. In the conditional-select family bit 30 and bits[11:10] together pick the operation, and the file prints the gap so you can see the difference between "not measured" and "not one field". In the bitfield family the same bits[21:10] window holds immr for a BFXIL and imms for a BFM — two instructions sharing a field, which is not one operand.

And a field this method cannot find is a field that is never written: reserved bits, the 0b11 value of the add/sub scale (the assembler refuses it), and the top bit of a shift amount on a 32-bit register (lsl #32 on w registers is refused too). Those are measured in section 9 of the artifact by asking the assembler and printing its diagnostic verbatim.

Three things that are cheaper to state than to prove

Each of these was asserted in a draft of this course, measured, and retracted. They are here rather than at the end of the course because each one is a place where the obvious thing to write down is wrong, and knowing which three is worth more than knowing the three facts.

Worked: the 21-model dispatch

Every model checks its own fixed bits first and returns false if they do not match, so the dispatch is a decision tree a reader can follow top to bottom rather than a table of magic numbers. --audit prints it with the guard each model checks, and the harness asserts that the table has 21 rows:

$ ./a64dec.py --audit | head -26
  model            the guard it checks                    claim
  m_adr            bits[28:24] = 0b10000, op = bit 31     ADR and ADRP
  m_addsub_imm     bits[28:24] = 0b10001                  ADD/SUB, 12-bit immediate
  m_logical_imm    bits[28:23] = 0b100100                 AND/ORR/EOR/ANDS, rotate+run
  m_movewide       bits[28:23] = 0b100101                 MOVZ/MOVN/MOVK
  m_bitfield       bits[28:23] = 0b100110                 SBFM/UBFM/BFM/BFXIL
  m_extr           bits[28:23] = 0b100111                 EXTR, and ROR as its alias
  m_bcond          bits[31:24] = 0b01010100               B.cond, imm19 at 23:5
  m_b_bl           bits[30:26] = 0b00101                  B and BL, imm26
  m_cbz            bits[30:25] = 0b011010                 CBZ and CBNZ
  m_tbz            bits[30:25] = 0b011011                 TBZ and TBNZ, imm14
  m_br_blr_ret     bits[31:23] = 0b110101100, opc=22:21   BR, BLR, RET
  m_hint           bits[31:5] = 0xd503201f >> 5             NOP, WFI, YIELD
  m_ldst_uimm      bits[29:27]=0b111, bits[26:24]=0b001   LDR/STR, 12-bit scaled
  m_ldst_uimm9     bits[29:27]=0b111, bits[26:24]=0b000   LDUR/STUR, 9-bit unscaled
  m_ldst_pair      bits[29:27] = 0b101                    LDP/STP, Rt2 at 14:10
  m_addsub_shifted bits[28:24] = 0b01011                  ADD/SUB shifted; Rd=31 is sp
  m_logical_shifted bits[28:24] = 0b01010                 AND/ORR shifted; Rn=31 is xzr
  m_condselect     bits[28:21] = 0b11010100               CSEL family
  m_3src           bits[30:24] = 0b0011011                MADD/MSUB/MNEG
  m_2src           bits[30:21] = 0b0011010110             LSLV..RORV, UDIV, SDIV
  m_clz_rev        bits[30:21] = 0b1011010110             CLZ, REV, REV16

  21 models, and 21 claims -- the two numbers are printed
  together because a model with no claim is a model nobody can check.
                

The order is the dispatch order, and it is not alphabetical and not arbitrary: the two models with the largest fixed patterns (m_adr at five bits and m_hint at twenty-seven) belong near the top, because a cheap-and-specific guard belongs above an expensive general one. Reordering this list changes what the decoder resolves in the overlap, and that is a design decision rather than a refactor.

Three guards in that table are worth expanding, because each is a place where a wrong assumption is silent rather than loud:

Apply it

  1. Re-measure one field yourself and compare to fields.txt. Pick pair imm and write the sweep yourself: stp x0, x1, [sp] against [sp, #8], [sp, #16], [sp, #32], [sp, #64], [sp, #128], [sp, #256]. (Expect mask 0x001f8000, bits[20:15], and expect the row to teach you why the reach is ±512 and not ±4095: the field is seven bits scaled by eight. Then ask the assembler for [sp, #4] and read the diagnostic — "index must be a multiple of 8 in range [-512, 504]" — which is a boundary stated in the words of the thing that enforces it.)
  2. Reproduce the single-pair mistake on purpose. In a64dec.py, change the imm16 case in FIELD_CASES to a single variant, movz w0, #0x2222. (Expect the mask to come out 0x00066660 — eight bits — for a sixteen-bit field, and expect nothing else in the run to notice. That is the point: a measurement that is merely wrong looks exactly like a measurement that is right unless the sweep is part of the method.)
  3. Break a guard by one bit and watch the corpus. In m_hint, change the guard from 0xd503201f >> 5 to 0xd603201f >> 5. (Expect the three nops in the corpus to stop being named and to be counted as unmodelled instead, expect the section 5 bit budget to lose a row, and expect the section 12 cross-check to report fewer NAMED instructions rather than a disagreement — because an unmodelled word is out of scope, not wrong. A decoder that silently returns fewer instructions is more dangerous than one that returns a wrong name, and this is the case where you can see the difference.)

Connect

Backwards, concept 1 is the reason this page has no length arithmetic in it, and isa-modrm is the contrast case: the ModRM byte exists because x86 needed to name a register-from-memory operand out of a fixed field, and AArch64's load/store pair spends five bits on its second register instead of two.

Forwards, concept 3 takes the imm12 row and the imm16 row and finds out what the twelve unreadable bits reach, and concept 4 takes the cond row and the two non-contiguous rows and finds that a fourth of the conditional-select group is UNDEFINED. Concept 5 is where the register-31 mistake in this page is actually found — it was not found by reading this table twice.

render_lesson_js(&mut page)