Almost everything written about AArch64 instruction encoding opens by telling you the length is fixed, and then moves on, and the reason to spend a concept on it is that the fixed length is not a detail of this encoding — it is what makes the encoding easy to decode and hard to encode. Every other design decision in the architecture is a consequence of a decision to spend 32 bits on every instruction, and you cannot evaluate any of them until you know what that purchase cost.
The purchase is not money. It is range: if every instruction is four bytes, then every field has to fit in 32 bits, and there is no escape to a second word. AArch64 therefore has a 12-bit add-immediate where x86-64 has an 8-bit one with a 32-bit escape, a 19-bit conditional branch where x86-64's is 8-bit-and-relative-to-16-bits, and a 21-bit PC-relative address — and each of those numbers is a consequence, not a choice. The next two concepts are about those fields. This one is about the constant underneath all of them.
Five object files, four optimisation levels plus a hand-written corpus, 868 instructions:
$ ./a64dec.py --section=2 | sed -n '/file /,/VERDICT/p'
file section bytes %4 insns bytes/insn chain
a64.o .text 540 0 135 4.0000 EXACTLY
corpus_O0.o .text 1352 0 338 4.0000 EXACTLY
corpus_O1.o .text 496 0 124 4.0000 EXACTLY
corpus_O2.o .text 588 0 147 4.0000 EXACTLY
corpus_Os.o .text 496 0 124 4.0000 EXACTLY
868 instructions over 5 code sections. Every section is a
multiple of four, every walk ends exactly on the section end, and
bytes/insn is 4.0000 in every row including the hand-written one.
VERDICT: fixed width, on this corpus
Two columns matter more than the rest. The %4 column is zero in all five rows, so no section has a ragged tail. And chain closed EXACTLY is the artifact walking each section from its first byte to its last and landing on the end rather than near it — which is the property that has no analogue in the previous course. A decoder that guesses a length wrong in a variable-length encoding is reading garbage forever after, and there is no resynchronisation point. Here the next instruction starts four bytes on whether or not the previous one made sense.
So here is what it buys, stated as the negative space rather than the positive claim, because every item is a class of bug this decoder cannot have:
Four bytes is four bytes for nop, which does nothing. The x86-64 encoding has a one-byte nop and a two-byte ret; this one pays four bytes each. The artifact measured the whole of that and the answer is not “AArch64 is worse”:
$ ./a64dec.py --section=13 | sed -n '/-O0 a64/,/-Os a64/p'
-O0 a64 1352 bytes / 338 insns x86 1174 bytes / 393 insns ratio 1.152 AArch64 LARGER
-O1 a64 496 bytes / 124 insns x86 610 bytes / 200 insns ratio 0.813 AArch64 smaller
-O2 a64 588 bytes / 147 insns x86 738 bytes / 236 insns ratio 0.797 AArch64 smaller
-Os a64 496 bytes / 124 insns x86 384 bytes / 151 insns ratio 1.292 AArch64 LARGER
THE RESULT, and it is not the one this course was drafted around.
AArch64 is LARGER at 2 of the 4 levels.
Two of four. The first draft of this page asserted “AArch64 loses on code density”, and the measurement retracts it rather than softening it — the ratio changes sign with the optimisation flag, so it was never a property of the architecture. The defensible sentence is: fixed width costs code size when the code is spill-heavy and saves it when the code is not, and the -O0 row shows why in one line. AArch64 emitted fewer instructions than x86-64 (338 against 393) and still produced more bytes (1352 against 1174), which can only be true if the instructions it did not emit were cheap on the other side — and at -O0 they are, because every spilled parameter is a 4-byte store against a 2-or-3-byte one.
What the encoding stores is an opcode and operands. The name you type is the assembler's, and the two are related by a table the architecture defines. Every row below was measured by assembling both lines and comparing the two 32-bit words — the only way to say what an alias actually stands for rather than what it looks like:
$ ./a64dec.py --section=2 | sed -n '/52824680/,/19 of 22/p'
52824680 mov w0, #0x1234 == movz w0, #0x1234 MOVZ with hw = 0
6b01001f cmp w0, w1 == subs wzr, w0, w1 the flag-writing form, with
the destination thrown away
3100041f cmn w0, #1 == adds wzr, w0, #1 the same, on the other side
4b0103e0 neg w0, w1 == sub w0, wzr, w1 the zero register as a SOURCE
2a2103e0 mvn w0, w1 == orn w0, wzr, w1 NOT is OR with all ones -- and
the ZERO is the FIRST operand
6a01001f tst w0, w1 == ands wzr, w0, w1
2a0103e0 mov w0, w1 == orr w0, wzr, w1 a MOVE IS AN OR with a zero
9100001f mov sp, x0 == add sp, x0, #0 register 31 is the STACK POINTER
910003e0 mov x0, sp == add x0, sp, #0 in this group
1b027c20 mul w0, w1, w2 == madd w0, w1, w2, wzr a MULTIPLY IS a three-source
with an addend of zero
93407c20 sxtw x0, w1 == sbfm x0, x1, #0, #31 a sign extend, as a FIELD
d3407c20 uxtw x0, w1 == ubfm x0, x1, #0, #31
531d7020 lsl w0, w1, #3 == ubfm w0, w1, #29, #28 a shift is a bitfield whose
two halves are adjacent
d65f03c0 ret == ret x30 and the link register is 30, so
it is the DEFAULT
d503201f nop == hint #0 TWENTY-SEVEN FIXED BITS and a
5-bit hint number
11400420 add w0, w1, #0x1000 == add w0, w1, #1, lsl #12 one imm12 field, with a
SHIFT on it
1a9f17e0 cset w0, eq == csinc w0, wzr, wzr, ne THE TRAP: the condition is
INVERTED
1a9f07e0 cset w0, ne == csinc w0, wzr, wzr, eq
5a9f13e0 csetm w0, eq == csinv w0, wzr, wzr, ne
1a811420 cinc w0, w1, eq == csinc w0, w1, w1, ne Rn == Rm, and again INVERTED
5a81b420 cneg w0, w1, ge == csneg w0, w1, w1, lt
10ffffc0 adr x0, T != 90000000 adrp x0, T NOT AN ALIAS, and the row is
here for that reason
19 of 22 pairs are the same 32 bits.
Read that for the architecture rather than for the aliases. cmp is a subs with a destination nobody keeps. mvn is an orn. mov is an orr. mul is a madd with a zero addend. lsl is a bitfield whose two halves happen to be adjacent. Every one of those is a name the architecture added on top of a smaller set of real encodings, and every one of them costs nothing: same four bytes, same decode cost, one more mnemonic for a human.
That is the general fact, and it runs both ways: the mnemonic is free and the mnemonic is also where the errors live. The row for mvn has a trap in it that is worth naming. The correct spelling is orn w0, wzr, w1 — zero first — and the natural guess orn w0, w1, wzr assembles to 2a3f0020, a different word. The two sources are in a fixed order in the encoding and the assembler does not accept the other one, so a reader who has the alias right and the operand order wrong gets a plausible instruction that is not the one they meant.
And one row is not an alias at all. adr x0, T and adrp x0, T are one bit apart — bit 31, which is sf in every other group in the architecture — and the difference in reach is 4,096. That is concept 2's subject, and it is the first of three places where bit 31 means something other than what its name suggests.
Combine them and you get the thing no other 32-bit architecture in this collection has: an instruction whose length you know before you know what it is. That is what lets a decoder be a decision tree over a fixed width instead of a length table followed by an opcode table, and it is why this course's artifact is 21 models and the ISA course's decoder has a length arithmetic with a resynchronisation story.
This course has a habit the courses before it established, and it is worth showing before the worked example rather than after the last concept. Each of the three was asserted in a draft of this course. Each was measured. Each was retracted in public.
adr against adrp — one bit, 4,096 of reach — and the twenty-first is mvn, whose correct spelling is orn w0, wzr, w1 with the zero first. The natural guess orn w0, w1, wzr assembles without a diagnostic and means something else. A wrong operand order in an alias is a plausible wrong instruction, not an error.sp where the encoding says zero, until the cross-check found it. A flag with a default is a flag nobody checks.All seventeen retractions are printed in full by the artifact and asserted as text by the harness, so a later edit cannot quietly delete one.
The artifact prints a derivation for every field it reads, which is the part a disassembler cannot do and the reason a disassembler is not evidence:
$ ./a64dec.py --why 1204cc00
0x1204cc00 and w0, w0, #0xf0f0f0f0
class field bits[28:25] = 1001 -> Data Processing -- Immediate
. N = 0 and opc = 0, so the word spells AND. There is no invert bit:
clang emits AND for `bic w0,w0,#0xff` by negating the CONSTANT to
0xffffff00, and the inversion is arithmetic, not encoding.
. LOGICAL IMMEDIATE: len = the position of the highest set bit of
N:NOT(imms) = 0b0001100, so esize = 1 << 3 = 8 bits
. LOGICAL IMMEDIATE: S = imms & 0b111 = 3 is the WIDTH of the run of
ones (S+1 = 4 of them); R = immr & 0b111 = 4 is how far RIGHT that
run is rotated inside the element
. LOGICAL IMMEDIATE: a run of 4 ones is 0b1111; rotate it right by 4
within 8 bits and you get 0b11110000 = 0xf0
. LOGICAL IMMEDIATE: repeat that element 8 times: the 64-bit pattern
is 0xf0f0f0f0f0f0f0f0
. LOGICAL IMMEDIATE: the destination is 32 bits, so the answer is the
low 32 of that pattern: 0xf0f0f0f0
. this decode READ 32 of the 32 bits; 0 were not needed to name the
instruction
Every line is arithmetic on the bits and every line is checkable by hand with the constants beside it. That is the whole difference between a decoder and a disassembler: llvm-objdump-21 printed and w0, w0, #0xf0f0f0f0 for the same word in one line and cannot tell you how, and the twelve bits it used are not readable in its output at all. Concept 3 is entirely about those twelve bits.
And the derivation is printed by the recorded run, not only by --why on a command line. That is a rule rather than a preference: check_quotes.py reads only a64dec.out, so a figure this page quotes has to be in the file the harness can see. A page that quotes a derivation no harness can find is a page whose numbers are only checkable by the machine that wrote them.
One more thing this page can now state that the previous course could not. Register 31 means different things in different groups, and this is not a subtle point: in add and sub it is the stack pointer, in the logical groups it is the zero register, in a load's data slot it is the zero register while in a load's base slot it is the stack pointer, and in cbz's register slot it is the zero register. The rows above show mov sp, x0 as add sp, x0, #0 and mov w0, w1 as orr w0, wzr, w1 — both use register 31, and in the first it is the stack pointer and in the second it is zero. A decoder that decides this once gets a plausible answer in the common case and a wrong one exactly where a compiler is most likely to emit the instruction. That is retraction R15 and it was found by a cross-check, not by reading a manual.
llvm-objdump-21 --triple=aarch64 -d any AArch64 object and count the bytes on each line. (Expect exactly seven distinct instruction lengths on the x86-64 side of the same C and exactly one on the AArch64 side, and expect the x86 distribution to be dominated by one and three bytes. The AArch64 row is a single bar at 100% and that is the whole visual argument for the concept.)mov w0, w1 and orr w0, wzr, w1, then orr w0, w1, wzr. (Expect the first pair to be identical bytes and the third to be a different word — and expect the assembler to accept all three without complaint, because orr w0, w1, wzr is a perfectly good instruction that is not the one you meant. This is the general form of the mvn row on this page: a wrong operand order in an alias is a plausible wrong instruction, not a diagnostic.)a64dec.py, change decode_text to advance by 3 instead of 4. (Expect the section 2 table to report chain closed NO and a non-zero %4, the section 12 cross-check to collapse, and the poisoned number to move. This is the completion criterion in miniature: the only claim in this course you can break with a one-character edit, and breaking it is how you learn that the claim was a measurement.)Backwards, isa-length derived the variable-length property this page is the negation of, and the ELF course's section-header concept is where the reader that walks these 868 words comes from — struct.unpack_from at 0x28 and the section array at 0x3a, with no library. elf-section-vs-segment is why the artifact finds .text by NAME and by the execute flag rather than by index.
Forwards, the field map is where bits[28:25] is measured, the immediates is where those twelve unreadable bits are unpacked, the conditions is where the cset row at the bottom of the alias table becomes a whole concept, and the cross-check is where the register-31 bug from this page is found.