“ARM has 16 registers” is a useful sentence only after adding “in AArch32.” It becomes actively confusing on a modern 64-bit phone or server, where the register names, widths, calling convention, and treatment of the program counter differ. I find it clearer to learn the two execution states side by side, then layer the ABI on top.

AArch32 at a glance

  • R0–R12 are visible integer registers in ordinary AArch32 code.

  • R13 is conventionally and architecturally usable as the stack pointer, SP.

  • R14 is the link register, LR, commonly holding a subroutine return address.

  • R15 is the program counter, PC, with instruction-dependent read semantics.

  • CPSR holds condition flags, execution state, masks, and control state; application code commonly refers to its application-visible flags as APSR.

  • Some privileged modes bank selected registers in classic A/R profiles, so “16 physical registers total” is also inaccurate.

AArch64 is not a wider spelling of R0–R15

  • X0–X30 are 31 general-purpose 64-bit registers.

  • W0–W30 name the low 32 bits of those same registers.

  • Writing a W register zeroes the upper 32 bits of its corresponding X register.

  • SP is separate from the numbered general-purpose register bank.

  • X30 is conventionally the link register, but it remains a general-purpose register.

  • PC is not a general-purpose register that ordinary A64 instructions name like X31.

  • PSTATE contains processor state; condition flags are accessed through NZCV.

Register widths change instruction behavior

widths.s (AArch64)asm
mov  x0, #-1        // X0 = 0xffffffffffffffff
mov  w0, #7         // X0 becomes 0x0000000000000007
add  w1, w0, #5     // 32-bit result; write zero-extends into X1
add  x2, x0, #5     // 64-bit result

The register spelling selects the operation size

  • Wn and Xn are views of one architectural register, not independent storage.

  • A 32-bit general-register write clears the upper half; it does not preserve stale bits.

  • Immediate syntax is assembler notation and may expand into one or more actual instructions.

  • Signedness usually comes from the instruction and later interpretation, not from a permanent register type.

What a function call does to LR and PC

call.s (AArch64)asm
bl   calculate      // X30 receives the return address
// execution resumes here after calculate returns
 
calculate:
    add  x0, x0, x1
    ret                 // normally branches to X30
  • BL changes control flow and writes the return address to X30.

  • RET defaults to the address in X30, though A64 permits another register operand.

  • A leaf function may return without saving X30.

  • A non-leaf function normally preserves its incoming return address before another BL overwrites X30.

  • Security extensions such as pointer authentication can alter real prologues and epilogues.

AAPCS64 assigns roles across function boundaries

  • X0–X7 carry the first general-register arguments and return values use X0 and, where required, X1.

  • X8 is an indirect-result location register in the base procedure-call standard.

  • X9–X15 are temporary caller-saved registers.

  • X16 and X17 are intra-procedure-call scratch registers that veneers and PLT code may use.

  • X18 is platform-specific and portable code should avoid assuming it is free.

  • X19–X28 are callee-saved.

  • X29 is conventionally the frame pointer and X30 the link register.

  • The stack pointer must remain 16-byte aligned at public interfaces and whenever memory is accessed through SP.

Caller-saved and callee-saved are obligations

Caller-saved means a called function may overwrite the register, so the caller saves any live value it needs afterward. Callee-saved means a function that changes the register must restore its incoming value before returning. Neither phrase says that hardware performs the save automatically.

AArch32 calling conventions use different assignments

  • In the base AAPCS32, R0–R3 carry arguments, results, and scratch values.

  • R4–R8, R10, and R11 are normally callee-saved; roles for R9 and frame-pointer use can vary by platform.

  • R12 is an intra-procedure-call scratch register.

  • SP, LR, and PC are aliases for R13, R14, and R15.

  • Floating-point argument rules depend on the selected ABI variant, so hard-float and soft-float objects must not be mixed casually.

Flags live in dedicated status state

compare.s (AArch64)asm
cmp   x0, x1       // alias of SUBS; updates N, Z, C, V
csel  x2, x0, x1, hi

Condition flags describe the latest flag-setting operation

  • N records a negative-sign result, Z a zero result, C carry/no-borrow semantics, and V signed overflow.

  • CMP discards the arithmetic result and keeps the flags.

  • HI is an unsigned comparison condition; signed comparisons use conditions such as GT or LT.

  • Not every arithmetic instruction updates flags—A64 distinguishes forms such as ADD and ADDS.

Floating-point, SIMD, SVE, and SME add more register state

  • AArch64 has 32 128-bit SIMD/floating-point registers viewed as V0–V31, with scalar B, H, S, D, and vector Q views.

  • AAPCS64 uses V0–V7 for many floating-point/vector arguments and results; preservation rules apply to portions of V8–V15.

  • SVE adds scalable vector Z registers, predicate P registers, and FFR on implementations that support it.

  • SME adds streaming and matrix state such as ZA and, in newer extensions, ZT0.

  • Always match assembly and context-switch code to the implemented architectural extensions and current ABI.

System registers and exception levels are another namespace

Registers such as SCTLR_EL1, TTBR0_EL1, ESR_EL1, and ELR_EL1 configure or report privileged architectural state. Their _ELn suffix names an exception level. User-space code at EL0 cannot freely read or write privileged state; an illegal access traps rather than behaving like an ordinary X register.

Observe registers with GDB

AArch64 target or emulatorbash
gcc -g -O0 demo.c -o demo
gdb ./demo
(gdb) break main
(gdb) run
(gdb) info registers
(gdb) disassemble /m main
...
x0  ...
x29 ...
x30 ...
sp  ...
pc  ...
cpsr ...

Debugger names reflect the target architecture

  • -g preserves debug information and -O0 reduces—but does not eliminate—source/register surprises.

  • info registers shows the stopped thread’s current architectural state.

  • GDB may display AArch64 status as cpsr for debugger compatibility even though AArch64 architecture describes PSTATE fields.

  • At higher optimization levels, variables can move, share registers, or be optimized away.

  • Cross-debugging requires a GDB build and remote target description that agree on architecture and extensions.

Common misconceptions to retire

  • Every ARM processor has 16 registers: true only as a simplified AArch32 core-register view.

  • LR always contains my return address: another nested call can overwrite it unless software preserves it.

  • PC contains the next instruction in a universal way: AArch32 PC reads are pipeline/context sensitive, while AArch64 does not expose PC as a normal GPR.

  • R0 or X0 has a fixed purpose: ABI roles apply at interfaces; within a function these registers hold temporary values.

  • A register is signed or unsigned: bits have no permanent signedness; instructions and types provide interpretation.

  • The compiler always maintains a frame pointer: ABI/platform policy and compiler options permit omission in many builds.

Primary Arm references