AArch64 Registers, Instructions, and Addressing
ARM64 is the common platform and operating-system label for the 64-bit Arm execution environment; AArch64 is Arm’s architectural name for that state. Documentation, compiler targets, and package names use both. This lesson uses AArch64 when describing the instruction set and ARM64 when describing a machine or platform. It concerns code you write, own, and may inspect—not copied binaries or unknown input.
The integer register view
AArch64 supplies thirty-one general-purpose integer registers named x0 through x30. An x name denotes the full 64-bit register. The matching w0 through w30 names select its low 32 bits. Reading a w register reads those low bits; importantly, writing one clears the upper 32 bits of the corresponding x register. That makes add w0, w1, w2 a 32-bit calculation whose result is zero-extended in x0, not a partial update.
| Name | Usual meaning | Important caution |
|---|---|---|
x0–x7 | Arguments and results | Usually caller-clobbered |
x8 | Indirect result location in the ABI | Do not assume it is a ninth ordinary argument |
x9–x15 | Temporary registers | May change across a call |
x19–x28 | Callee-saved registers | A function using them must restore them |
x29, x30 | Frame pointer (FP), link register (LR) | Roles are ABI conventions, not magic hardware types |
Register number 31 is context-sensitive. In many data-processing instructions it is spelled xzr or wzr, the zero register: reads produce zero and writes are discarded. In stack-addressing and selected arithmetic forms it denotes sp, the stack pointer. An assembler rejects combinations the encoding does not permit, so spell the intended name rather than assuming x31 is interchangeable. There is no general instruction that treats SP as an ordinary scratch register.
Flags, instruction width, and byte order
The condition bits live in NZCV: negative, zero, carry, and signed overflow. Instructions ending in S, such as adds and subs, update them. cmp a, b is an alias for a flag-setting subtraction whose value is discarded; tst similarly aliases a flag-setting AND. A following conditional branch consumes the flags, so keep flag-producing and flag-consuming instructions adjacent unless the intervening instruction is known not to alter them. Carry and overflow have different unsigned and signed meanings.
Unlike variable-length x86 instructions, every AArch64 instruction is exactly 32 bits (four bytes) and instructions are four-byte aligned. Fixed width makes instruction boundaries straightforward, but it does not make arbitrary bytes safe or meaningful to execute. Current mainstream AArch64 systems are little-endian: a 32-bit word is stored least-significant byte first. Endianness changes memory representation, not the register value written in source. Protocols and file formats may require an explicitly defined byte order; use loads, stores, and byte-reversal instructions only after reading that format’s specification.
Useful instruction families
| Purpose | Examples | Notes |
|---|---|---|
| Move/construct | mov, movz, movk | mov is often an alias; wide moves construct selected constants. |
| Arithmetic | add, sub, madd | Choose w or x deliberately. |
| Logical | and, orr, eor | Not every numerical literal encodes as a logical immediate. |
| Memory | ldr, str, ldrb, ldrsw | Load/store architecture: arithmetic operands are registers. |
| Address creation | adr, adrp | Often paired with an add or load relocation. |
An immediate is a value embedded in an instruction encoding, not an unlimited literal. add x0, x0, #12 is directly encodable, while a large arbitrary 64-bit constant commonly needs movz plus one or more movk instructions, a literal load, or compiler-selected materialization. Prefer the assembler and linker’s relocation syntax for addresses; manually calculating addresses defeats position-independent code.
Loads, stores, and addresses
AArch64 is load/store: add x0, x1, x2 never fetches from an address held in x1. Use ldr to bring memory into a register and str to write it back. Load mnemonic and destination size express extension behavior: ldrb w0, [x1] zero-extends a byte; ldrsb x0, [x1] sign-extends one. A plain 64-bit ldr x0 requires an appropriately aligned, valid eight-byte object under the language and platform rules.
// Read table[index] when each element is an unsigned 32-bit word.
// x0 = base address, w1 = index; x2 is a temporary.
uxtw x2, w1 // define unsigned index width
ldr w3, [x0, x2, lsl #2] // address = base + index * 4
add w0, w3, #1 // 32-bit result, upper x0 bits become zero
Common forms are [xN] (base), [xN, #offset] (base plus immediate), and [xN, xM, lsl #scale] (base plus scaled index). Pre-indexed [sp, #-16]! updates the base before access; post-indexed [sp], #16 updates it after access. They are useful for disciplined stack adjustment but hide state changes, so annotate them. The permitted offset range and scaling depend on the particular load/store encoding, element size, and signedness.
Widths, extension, and aliases
Width is an algorithmic decision. A pointer and a size_t are ordinarily 64-bit on AArch64 LP64 systems, while an int is ordinarily 32-bit. Passing a negative 32-bit quantity to a 64-bit computation requires a sign extension; passing an unsigned one requires zero extension. Instructions such as sxtw x2, w1 and uxtw x2, w1 make that choice explicit. Using the wrong extension produces particularly confusing bugs for values whose top 32-bit bit is set.
The assembler offers aliases to make common encodings readable. mov x0, x1 can assemble as an OR with the zero register; cmp and cmn are forms of flag-setting subtract/add; neg is subtraction from zero. An alias is not a separate hardware capability. When comparing listings from tools, recognize the underlying encoding before concluding that source and disassembly disagree.
Address versus value
Keep addresses and loaded values separate in annotations. If x0 points to a structure, add x1, x0, #8 computes another address and does not read field bytes. ldr w2, [x0, #8] reads a value. This distinction also explains why a load may fault while an address calculation usually does not. In compiler-generated code, an address may be held temporarily across several instructions; do not rename it “the field value” until a load has occurred.
At the source-language boundary, alignment, lifetime, object bounds, and aliasing still govern validity. Assembly lets an author state memory operations directly; it does not grant permission to access an object after its lifetime or outside its allocation. For a review, document each base register’s object, each maximum offset, access width, and whether another pointer may overlap it. This simple ledger catches more errors than memorizing every mnemonic.
When reading a debugger display, also distinguish architectural state from source variables. A compiler can keep one variable in different registers at different points, reuse a register after a value dies, or omit a variable entirely under optimization. Debug information and the instruction stream together provide stronger evidence than a single register snapshot.
For each experiment, record the target, assembler version, source, and exact object inspected. Change one operand width or addressing form at a time. This turns a listing into reproducible evidence and avoids attributing a difference caused by optimization or a different target to an instruction-set rule.
A final review question is simple: for every instruction, can you state its input registers, output registers, width, and any memory effect? If not, pause and consult the instruction reference before extending the routine.
PC-relative data safely
adr forms an address near the current instruction. adrp forms the page address, then an add :lo12:symbol can add its in-page offset. Toolchains emit relocations so the linker can place the result. This concise ELF-style example shows the idea; let the target assembler choose the supported relocation spelling.
.text
.global increment
increment:
ldr w1, [x0] // caller supplies a valid int pointer
add w0, w1, #1
ret
This function has no I/O, does not allocate memory, and touches only its caller-provided object. It is a good supervised stepping exercise in a debugger with a test program you compiled. Do not turn it into an exercise in probing arbitrary addresses: a bad address may fault, corrupt data, or expose information.
Checks and exercises
- For each
wwrite in a short listing, state the resulting upper half of the matchingxregister. - Translate
ldr w5, [x4, #20]into an address calculation and identify the loaded byte count. - Write a pure routine that returns
a + bfromw0andw1; compare its object-file disassembly with the source. - Change the table example to signed 16-bit elements and explain which load and index extension you selected before assembling it.
For board-specific, supplemental practice, see the existing ARMv8 Khadas/Vim3 introduction and the ARM64 GDB lesson. Their hardware and command details are supplemental, not portable ABI rules.
dispelled