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.
| Register | Job |
|---|---|
x0 to x7 | Pass arguments into a function; x0 also carries the result back |
x8 | Holds the address where a function should write a large result |
x9 to x15 | Temporaries: yours to use, but any call may change them |
x16, x17 | Used by the linker (the tool that joins your code to the C library) on the way into a called function: avoid them |
x18 | Reserved for the operating system: avoid it |
x19 to x28 | Saved registers: a function that changes one must restore it before returning, so a value kept here survives a call |
x29, named fp | The frame pointer: marks the current function's area of the stack |
x30, named lr | The 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 as | Base | Prefix | Value |
|---|---|---|---|
42 | decimal | none | 42 |
0x2a | hexadecimal | 0x | 42 |
0b101010 | binary | 0b | 42 |
052 | octal | a leading 0 | 42 |
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.
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
xregister, two in awregister), like0x70000 - 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.
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.
pitfall
Common mistakes from this lesson, each with a broken program and its fix that you can run:
Check yourself
- After
mov x19, -1and thenmov w19, 7, what doesx19hold? - Which registers keep their values across
bl printf? - What does
mov w20, 017put inw20? - Can
mov x21, 0x30000be one instruction? What aboutmov x21, 0x30001?
answers
show answers
- 7, because writing
w19clears the upper half ofx19. x19tox28, plusfpandsp;x0tox18may change.- 15, because the leading zero makes it octal.
- Yes: it is one halfword in bits 16 to 31. No: it needs a
movzand amovk.
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
movzandmovk. - Basic quiz: ARMv8 assembly: the register roles, aliases, and exception levels.