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
x0x30 31 general-purpose 64-bit registers
w0w30 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
WarningThe 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 plain mov #imm usually assembles to): a 16-bit immediate placed at bit 0/16/32/48, rest zeroed.
  • MOVK keeps everything else — chain up to four for any 64-bit constant.
  • MOVN moves 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 with MOVZ/MOVK instead.
  • Addresses of your data: the ADRP + @PAGEOFF pair 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 #4

Shifts 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 either

CBZ/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], #-1

6 · 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 / ccmp or two branches, or fully branchless: compute the modified value, CSEL on the range condition.
  • Digits from a number (Exercise 3.4): udiv q, n, ten then msub r, q, ten, n gives quotient and remainder (A64 has no combined divmod; MSUB closes the gap — Module 5 §1).
  • Bit counting (Exercise 3.5): shift-and-test is O(width); Kernighan’s x &= x−1 is O(set bits); the SWAR ladder (masks 0x5555…, 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; ldrb from 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.