AArch64 Playground
4.4 · Registers and immediates

Registers and immediates

prerequisite

Read this first:

A register is a small, fast storage slot inside the processor. Arithmetic instructions work on registers, not on memory, so a value has to be in a register before the processor can compute with it.

AArch64 gives you thirty-one general-purpose registers, x0 through x30, each holding 64 bits. The same storage has a 32-bit view, w0 through w30, that reads and writes the low half. Writing a w register also clears the upper 32 bits of the matching x register: after mov w19, 5, all of x19 holds 5, whatever was there before. A program spends most of its time moving values between these registers and computing on them, so the move instructions are worth knowing well.

What each register is for

Any of the thirty-one can hold any value, but the calling convention, the set of rules every function on the system agrees to follow, gives most of them a job. Those rules are what let your program call printf and still find its own values where it left them.

RegisterJob
x0 to x7Pass arguments into a function; x0 also carries the result back
x8Holds the address where a function should write a large result
x9 to x15Temporaries: yours to use, but any call may change them
x16, x17Used by the linker (the tool that joins your code to the C library) on the way into a called function: avoid them
x18Reserved for the operating system: avoid it
x19 to x28Saved registers: a function that changes one must restore it before returning, so a value kept here survives a call
x29, named fpThe frame pointer: marks the current function's area of the stack
x30, named lrThe link register: bl stores the return address here

The stack pointer, the zero register, and pc

sp is the stack pointer, which tracks the top of the stack; the stack lesson covers it. xzr, with its 32-bit view wzr, is the zero register: reading it always gives 0, and anything written to it is thrown away. Both are register number 31 in the machine code, and each instruction decides which of the two that number means.

pc, the program counter, holds the address of the instruction running now. No ordinary instruction names it as an operand; branch instructions are how a program changes it.

A separate bank of 32 floating-point registers, d0 to d31 (with s0 to s31 as their 32-bit views), holds numbers with fractions. The floating-point lesson covers them.

warning

Library functions such as printf and scanf are allowed to change any of x0 to x18, and they do. A value left in x9 before bl printf is gone afterward. Keep anything that must outlive a call in x19 to x28.

When a program in these first lessons keeps a value in x19 or above inside main, it does not save the old contents first. That is common practice for main, but a subroutine you write yourself must save and restore any of x19 to x28 it changes; the subroutine lessons show how.

Giving registers names

The programs on this site give registers readable names with m4, a text tool that runs over the file before the assembler sees it. define(value_r, x19) tells m4 to replace every later value_r with x19; the _r ending marks the name as a register. Every program here opens with two such lines that name x29 as fp and x30 as lr, so the frame pointer and the link register can be written by their jobs.

m4 replaces a defined name everywhere below its define, including inside strings, so pick names that never appear as words in your messages.

Immediates and number bases

An immediate is a constant written inside an instruction, like the 42 in mov w19, 42. The assembler packs it into the 32-bit instruction word itself, so the value arrives without a trip to memory.

The destination always comes first: mov w19, 42 means "w19 becomes 42", and add w20, w19, 8 means "w20 becomes w19 plus 8".

An immediate can be written in four bases, and its prefix says which:

Written asBasePrefixValue
42decimalnone42
0x2ahexadecimal0x42
0b101010binary0b42
052octala leading 042

pitfall

A leading zero means octal, never a padded decimal. 010 is eight, 017 is fifteen, and 09 does not assemble at all, because 9 is not an octal digit. Never pad a decimal number with zeros to line up a column. The base converter shows any value in all four bases.

The program below writes one value four ways and then falls into the trap. It prints 42 42 42 42 on the first line and 010 reads as 8 on the second.

The two lines at the top of main and the last three are the setup and return that every program here shares, and the next two lessons explain printf. For now, read w1 to w4 as the values to print, in order.

loading editor...

regfile

N clearZ clearC clearV clear

x0–x30 are the integer registers.

X0arg00x0000000000000000
X1arg10x0000000000000000
X2arg20x0000000000000000
X3arg30x0000000000000000
X4arg40x0000000000000000
X5arg50x0000000000000000
X6arg60x0000000000000000
X7arg70x0000000000000000
X8ind0x0000000000000000
X90x0000000000000000
X100x0000000000000000
X110x0000000000000000
X120x0000000000000000
X130x0000000000000000
X140x0000000000000000
X150x0000000000000000
X16ip00x0000000000000000
X17ip10x0000000000000000
X18pr0x0000000000000000
X190x0000000000000000
X200x0000000000000000
X210x0000000000000000
X220x0000000000000000
X230x0000000000000000
X240x0000000000000000
X250x0000000000000000
X260x0000000000000000
X270x0000000000000000
X280x0000000000000000
X29fp0x0000000000000000
X30lr0x0000000000000000
SP0x0000000080000000
PC0x0000000000400000
console

Output prints here as your program runs.

Press step or run under the editor, or feed stdin from the box below.

not assembled

example 1try it: run it, or step one instruction at a timeOpen in playground

What fits in one mov

Immediates are convenient but small: only so many bits remain in a 32-bit instruction word after the bits that name the operation and the registers, so one instruction cannot carry an arbitrary 64-bit constant.

mov is not a single instruction either. It is an alias, a friendly name the assembler turns into whichever real instruction does the job: mov x0, x1 becomes orr x0, xzr, x1 (an OR with zero), mov x0, sp becomes add x0, sp, 0, and mov x0, 42 becomes movz x0, 42. A disassembler, a tool that turns machine code back into assembly, may show you the real name.

So a mov with an immediate works only when the constant has one of three shapes:

  • a 16-bit value (0 to 65535) sitting in any one of the register's 16-bit slots (four in an x register, two in a w register), like 0x70000
  • such a value with every bit flipped, which covers small negative numbers like -5
  • a block of ones that repeats at a fixed spacing across the register, like 0x00ff00ff00ff00ff (the exact rule is fiddly, and the assembler checks it for you)

The program below uses each shape and prints 65535 458752 -5 ff00ff00ff00ff. Change 0x70000 to 0x70001 and run it again: the assembler refuses the line, because that value needs two slots.

loading editor...

regfile

N clearZ clearC clearV clear

x0–x30 are the integer registers.

X0arg00x0000000000000000
X1arg10x0000000000000000
X2arg20x0000000000000000
X3arg30x0000000000000000
X4arg40x0000000000000000
X5arg50x0000000000000000
X6arg60x0000000000000000
X7arg70x0000000000000000
X8ind0x0000000000000000
X90x0000000000000000
X100x0000000000000000
X110x0000000000000000
X120x0000000000000000
X130x0000000000000000
X140x0000000000000000
X150x0000000000000000
X16ip00x0000000000000000
X17ip10x0000000000000000
X18pr0x0000000000000000
X190x0000000000000000
X200x0000000000000000
X210x0000000000000000
X220x0000000000000000
X230x0000000000000000
X240x0000000000000000
X250x0000000000000000
X260x0000000000000000
X270x0000000000000000
X280x0000000000000000
X29fp0x0000000000000000
X30lr0x0000000000000000
SP0x0000000080000000
PC0x0000000000400000
console

Output prints here as your program runs.

Press step or run under the editor, or feed stdin from the box below.

not assembled

example 2try it: run it, or step one instruction at a timeOpen in playground

Building a wide constant

A constant with none of those shapes is built one halfword, 16 bits, at a time; a 64-bit register holds four halfwords. movz (move and zero) writes a halfword and zeroes the rest of the register, while movk (move and keep) writes a halfword and keeps everything else. A third, movn (move and NOT), places a halfword and then flips every bit of the register, so the other halfwords become all 1s. That is how the assembler builds the second shape above: mov w19, -5 becomes movn w19, 4, because flipping every bit of 4 gives -5.

Start with movz to set one halfword and clear the register, then use movk for each remaining halfword. A left shift picks the 16-bit slot the immediate lands in: no shift targets bits 0 to 15, lsl 16 targets bits 16 to 31, lsl 32 targets bits 32 to 47, and lsl 48 targets bits 48 to 63.

Once the constant is in place, ordinary arithmetic such as add with an immediate adjusts it without touching memory.

pitfall

A common slip is to reach for movk first. movk keeps the other halfwords, so if the register still holds an earlier value those bits survive into your constant: had value_r held -1 and the program below started with movk in place of movz, its hex output would read 0xffffffff00012730. Begin with movz to clear the register and set the first halfword, then layer movk on top. The other trap is range: a plain mov takes only the three shapes above, and any other constant has to be built with the movz and movk pair rather than forced into one instruction.

The program below builds 0x12710 from two halfwords, adds 0x20, and prints the result in decimal and in hex: value = 75568 (0x12730). The hex form shows each halfword directly. Change the halfwords and watch the printed value follow.

This main keeps its value in x19 without saving the old contents first, the habit the warning above describes.

loading editor...

regfile

N clearZ clearC clearV clear

x0–x30 are the integer registers.

X0arg00x0000000000000000
X1arg10x0000000000000000
X2arg20x0000000000000000
X3arg30x0000000000000000
X4arg40x0000000000000000
X5arg50x0000000000000000
X6arg60x0000000000000000
X7arg70x0000000000000000
X8ind0x0000000000000000
X90x0000000000000000
X100x0000000000000000
X110x0000000000000000
X120x0000000000000000
X130x0000000000000000
X140x0000000000000000
X150x0000000000000000
X16ip00x0000000000000000
X17ip10x0000000000000000
X18pr0x0000000000000000
X190x0000000000000000
X200x0000000000000000
X210x0000000000000000
X220x0000000000000000
X230x0000000000000000
X240x0000000000000000
X250x0000000000000000
X260x0000000000000000
X270x0000000000000000
X280x0000000000000000
X29fp0x0000000000000000
X30lr0x0000000000000000
SP0x0000000080000000
PC0x0000000000400000
console

Output prints here as your program runs.

Press step or run under the editor, or feed stdin from the box below.

not assembled

example 3try it: run it, or step one instruction at a timeOpen in playground

pitfall

Check yourself

  1. After mov x19, -1 and then mov w19, 7, what does x19 hold?
  2. Which registers keep their values across bl printf?
  3. What does mov w20, 017 put in w20?
  4. Can mov x21, 0x30000 be one instruction? What about mov x21, 0x30001?

answers

show answers
  1. 7, because writing w19 clears the upper half of x19.
  2. x19 to x28, plus fp and sp; x0 to x18 may change.
  3. 15, because the leading zero makes it octal.
  4. Yes: it is one halfword in bits 16 to 31. No: it needs a movz and a movk.

Practice

  • Warm up the registers: combine three registers with two instructions.
  • The vanishing value: fix a swap of two registers that loses one of the values.
  • Halfword by halfword: build a 64-bit constant 16 bits at a time with movz and movk.
  • Basic quiz: ARMv8 assembly: the register roles, aliases, and exception levels.