Module 3 Lessons — A64 Essentials: Data, Memory, and Control Flow
Back to the Course 3 syllabus · Practice: Module 3 exercises
This page is the working A64 reference for the hands-on half of the course: the programmer’s model, how constants and addresses get into registers, how flags and branches express control flow, and how loads and stores address memory. Everything runs on your Mac exactly as written (Mach-O syntax from Module 0 throughout).
1 · The programmer’s model
| Resource | Description |
|---|---|
x0–x30 |
31 general-purpose 64-bit registers |
w0–w30 |
The low 32 bits of the same registers — not separate hardware |
xzr / wzr |
The zero register: reads as 0, writes vanish (encoded as register 31 in most instructions) |
sp |
Stack pointer (register 31 in address contexts); must stay 16-byte aligned for SP-based access |
pc |
Not directly readable/writable — only branches, ADR/ADRP, and exceptions touch it |
x30 (lr) |
Link register — BL writes the return address here (Module 4’s subject) |
x29 (fp) |
Frame pointer by convention (Module 4) |
| NZCV | The four condition flags (Module 1 §1), living in cpsr as lldb shows it |
w-register rule
Writing a w register zeroes the upper 32 bits of the x register. mov w0, #-1 leaves x0 = 0x00000000FFFFFFFF — not 64-bit −1. There is no partial-register merging (and no false dependencies, which is why the rule exists). Choose w for 32-bit data, x for 64-bit and addresses; Exercise 3.1’s table drills the corner cases.
2 · Getting values into registers
Fixed 32-bit instructions can’t hold 64-bit constants, so A64 builds them:
mov x0, #0x1234 ; MOVZ: x0 = 0x1234 (rest zeroed)
movk x0, #0xABCD, lsl #16 ; keep other bits, insert halfword: x0 = 0xABCD1234
movn x0, #0 ; x0 = ~0 = 0xFFFF…FF (−1): "move NOT"MOVZ(what plainmov #immusually assembles to): a 16-bit immediate placed at bit 0/16/32/48, rest zeroed.MOVKkeeps everything else — chain up to four for any 64-bit constant.MOVNmoves the complement — small negative numbers in one instruction.- Logical instructions (
AND/ORR/EOR) accept a separate bitmask immediate class: repeating run-of-ones patterns (0xFF00FF00FF00FF00,0x7FFFFFFF…). The assembler validates; if a mask is rejected, it isn’t encodable — build it withMOVZ/MOVKinstead. - Addresses of your data: the
ADRP+@PAGEOFFpair from Module 0 §4 — that’s the constant-materialization story for symbols.
The free shift. Most data-processing instructions shift their second register operand at no cost (Module 1 §4’s barrel shifter, exposed in the encoding):
add x0, x1, x2, lsl #3 ; x0 = x1 + (x2 << 3) — array-index math in one shot
and x0, x0, x1, asr #4Shifts also exist standalone — LSL (logical left), LSR (logical right, zero-fill), ASR (arithmetic right, sign-fill: signed ÷2ⁿ), ROR (rotate) — plus the extend-and-shift forms (uxtb, sxtw, …) that widen a narrow register on the way into an address or an add (Module 1 §1’s sign extension, as syntax).
3 · Flags, compares, and conditional everything
Instructions ending in S set NZCV (ADDS, SUBS, ANDS); the compare family are their result-discarding aliases:
| Mnemonic | Is really | Asks |
|---|---|---|
CMP a, b |
SUBS zr, a, b |
How do a and b compare? |
CMN a, b |
ADDS zr, a, b |
Compare with negative |
TST a, b |
ANDS zr, a, b |
Any of these bits set? |
Condition codes read the flags with two vocabularies — signed and unsigned — and using the wrong column is the classic off-by-everything bug:
| Meaning | Unsigned | Signed |
|---|---|---|
| equal / not equal | EQ / NE |
same |
| below / at-or-above | LO (CC) / HS (CS) |
— |
| above / at-or-below | HI / LS |
— |
| less / greater-or-equal | — | LT / GE |
| greater / less-or-equal | — | GT / LE |
| negative / overflow set | MI / VS (and PL / VC) |
— |
Branching on conditions:
cmp x0, x1
b.ge else_part ; signed ≥
cbz x2, done ; compare-and-branch-if-zero: no flags needed
tbnz x3, #7, bit7_set ; test single bit, branch — no flags eitherCBZ/CBNZ and TBZ/TBNZ fuse the compare into the branch for the two most common tests (zero, single bit) — idiomatic loop exits.
The conditional-select family turns small ifs into straight-line data flow — no branch, no misprediction (Module 2 §4):
| Instruction | Effect |
|---|---|
CSEL xd, xa, xb, cond |
xd = cond ? xa : xb |
CSINC xd, xa, xb, cond |
xd = cond ? xa : xb+1 (alias CINC) |
CSINV / CSNEG |
select or invert/negate |
CSET xd, cond |
xd = cond ? 1 : 0 |
The min/max scan of Exercise 3.6 is two CMP+CSEL pairs per element; the saturating add of Exercise 1.5 is ADDS + CSEL-on-VS. Learn to reach for CSEL whenever both sides of an if are cheap.
Loop and if/else shapes — the two patterns that cover 90 % of structured control flow:
; while (x0 != 0) { body } ; if (x0 < x1) A else B (signed)
loop: cmp x0, x1
cbz x0, done b.ge elseB
; ... body ... ; ... A ...
b loop b endif
done: elseB:
; ... B ...
endif:4 · Data sections and alignment
.data
.p2align 3 ; 8-byte alignment for the array below
vals: .quad 1, 2, 3, 4 ; 64-bit values
msg: .asciz "hello" ; NUL-terminated string
buf: .space 64 ; 64 zero bytes
.text
.p2align 2 ; instructions are 4-byte aligned.byte/.hword/.word/.quad emit 1/2/4/8-byte data. Alignment matters twice: correctness conventions (keep n-byte data n-aligned as a habit — it’s mandatory on other ARM systems and free here) and the ADRP page math of Module 0 §4 gets you to any of it position-independently.
5 · Loads, stores, and addressing modes
A64 touches memory only through loads and stores; the register suffix sets the width, with explicit widening on narrow loads:
| Instruction | Width | Notes |
|---|---|---|
LDR x0/w0, […] |
8 / 4 bytes | w load zeroes the top half (§1) |
LDRB/LDRH w0, […] |
1 / 2 bytes | zero-extend into w0 |
LDRSB/LDRSH/LDRSW |
1 / 2 / 4 | sign-extend (into w or x) |
STR/STRB/STRH |
8·4 / 1 / 2 | stores narrow from the register’s low bits |
LDP / STP |
2 registers | pair load/store — the stack-frame workhorse (Module 4) |
Addressing modes — the catalog Exercise 3.3 makes you actually use:
| Mode | Syntax | Effect |
|---|---|---|
| Base | [x0] |
address = x0 |
| Immediate offset | [x0, #16] |
x0+16; x0 unchanged |
| Register offset | [x0, x1] |
x0+x1 |
| Scaled register | [x0, x1, lsl #3] |
x0 + x1·8 — array indexing in the address |
| Pre-index | [x0, #16]! |
x0 += 16 first, then access — note the ! |
| Post-index | [x0], #16 |
access at x0, then x0 += 16 |
Pre/post-index give you pointer-walking loops with zero separate add instructions:
; strlen-ish byte walk ; two pointers closing inward (Exercise 3.3)
loop: ; ldrb w2, [x0]
ldrb w1, [x0], #1 ; ldrb w3, [x1]
cbnz w1, loop ; strb w3, [x0], #1
; strb w2, [x1], #-16 · Endianness, observed once and settled forever
Both machines in this course are little-endian: the least-significant byte of a word sits at the lowest address.
x0 = 0x0102030405060708 → str x0, [x1]
address: x1+0 +1 +2 +3 +4 +5 +6 +7
byte: 08 07 06 05 04 03 02 01
Loads reassemble in the same order, so registers always look right; only byte-granular views (a uint8_t* walk, a hexdump, a wire protocol) expose the ordering — exactly Exercise 3.7. The REV family (REV, REV16, REV32) byte-swaps in one instruction when a big-endian protocol demands it (Module 8 §5’s “state the byte order explicitly,” instruction edition).
7 · Idiom kit for the exercises
Patterns that recur through Modules 3–5, worth recognizing as units:
- Range test + conditional modify (Exercise 3.2):
cmp/ccmpor two branches, or fully branchless: compute the modified value,CSELon the range condition. - Digits from a number (Exercise 3.4):
udiv q, n, tenthenmsub r, q, ten, ngives quotient and remainder (A64 has no combined divmod;MSUBcloses the gap — Module 5 §1). - Bit counting (Exercise 3.5): shift-and-test is O(width); Kernighan’s
x &= x−1is O(set bits); the SWAR ladder (masks0x5555…,0x3333…,0x0F0F…) is O(log width) with no branches at all — three algorithms, one spec, measurable differences. - Table-driven anything (Exercise 1.6, and every protocol FSM you’ll ever write): index = state·inputs + input;
ldrbfrom the table; scaled addressing does the multiply.
Where the exercises hook in
| Lesson section | Exercise |
|---|---|
| §1–2 registers, moves, shifts | 3.1 — MOV/shift predict-verify |
| §3 flags + CSEL, §7 idioms | 3.2 — case converter · 3.6 — min/max scan |
| §5 addressing modes | 3.3 — reverse in place |
| §7 divide/remainder idiom | 3.4 — integer → decimal ASCII |
| §7 bit algorithms | 3.5 — bit census |
| §6 endianness | 3.7 — endianness, observed |
Reference shelf: the Arm Architecture Reference Manual for A-profile is the authority when an instruction’s exact behavior matters (its per-instruction pages are surprisingly readable); H&H App. B maps A32 ↔︎ A64 mentally; Smith 2, 4, 5 and Plantz 9–13 (registers, first programs, instruction details, control flow) tell the same story book-paced.