AArch64 Playground
4.18 · Memory: loads, stores, and addressing modes

Memory: loads, stores, and addressing modes

Memory is one long row of bytes, and each byte has a number called its address. On AArch64 an address is 64 bits wide. Each running program gets its own private set of numbers, called virtual addresses, and the operating system maps them onto the real memory, so two programs can use the same address without getting in each other's way.

AArch64 is a load/store design: arithmetic instructions work only on registers. To change a value in memory, a program loads it into a register, works on it there, and stores the result back. The address always goes inside square brackets, so ldr w10, [x9] reads "load w10 from the address held in x9".

Four sizes, two ways to widen

A load or store moves 1, 2, 4 or 8 bytes. The size comes from the mnemonic's last letter (b for a byte, h for a halfword) or, for 4 and 8 bytes, from the register: a w register moves 4 bytes and an x register moves 8.

bytesstoreload, zero-extendedload, sign-extended
1strb wldrb wldrsb w or ldrsb x
2strh wldrh wldrsh w or ldrsh x
4str wldr wldrsw x
8str xldr xnone needed: 8 bytes fill the register

A store of fewer bytes than the register holds writes only the register's low bytes. A load of fewer bytes has to decide what goes in the register's upper bits:

  • Zero extension fills them with 0s. Use it for a value that is never negative, such as a character code or a count.
  • Sign extension fills them with copies of the value's top bit, so a negative value stays negative in the wider register. The s in ldrsb, ldrsh and ldrsw asks for it.

The program below stores 0xf0 as a byte, 0xfffe as a halfword and -10 as a word, then loads each one back both ways. It prints:

byte 0xf0:        ldrb = 240, ldrsb = -16halfword 0xfffe:  ldrh = 65534, ldrsh = -2word 0xfffffff6:  ldr  = 4294967286, ldrsw = -10
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

The byte 0xf0 has its top bit set. Zero-extended it is 240; sign-extended it is -16. The halfword works the same way: 65534 or -2. For the word, ldr w1 writes a w register, and writing a w register always clears bits 32 to 63 of the matching x register, so %ld sees 4294967286. ldrsw x2 copies the sign bit up into those bits, so %ld sees -10.

pitfall

The register picks the size of an ldr or str, not the label. ldr x1, [x9] on a .word reads the 4 bytes of the word and the 4 bytes after it, and prints nonsense as soon as something else is stored there. Match the register to the directive: .byte with ldrb or ldrsb, .hword with ldrh or ldrsh, .word with a w register or ldrsw, .dword with an x register.

Offsets and index registers

The part inside the brackets is the addressing mode: how the processor works out the address. These forms leave the base register unchanged:

formaddress used
[x9]x9
[x9, 8]x9 + 8
[x9, x10]x9 + x10
[x9, x10, LSL 2]x9 + x10 * 4
[x9, w10, SXTW 2]x9 + w10 * 4, with w10 read as signed
[x9, w10, UXTW 2]x9 + w10 * 4, with w10 read as unsigned

In an array, element i sits at base + i * size, so the shift after LSL, SXTW or UXTW should match the element size: 0 for bytes, 1 for halfwords, 2 for words, 3 for doublewords. SXTW sign-extends a 32-bit index to 64 bits before adding it, which lets an index kept in a w register be negative. UXTW zero-extends it instead.

The program below adds up the same five-word array three ways: with a constant offset written out for each element, with a 64-bit index scaled by LSL 2, and with a 32-bit index widened and scaled by SXTW 2, the form course programs use most. It prints:

constant offsets  total = 75lsl 2 index       total = 75sxtw 2 index      total = 75
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

Pointers that move themselves

Two more forms change the base register as part of the access, so a pointer can walk along an array without a separate add:

formnamewhat happens
[x9, 4]!pre-indexadd 4 to x9 first, then use the new x9 as the address
[x9], 4post-indexuse x9 as the address, then add 4 to x9

You have seen both already. stp fp, lr, [sp, -16]! is pre-index: it moves sp down, then stores. ldp fp, lr, [sp], 16 is post-index: it loads, then moves sp back up.

The program below sums the array once with each form. Each loop counts down with subs, which sets Z when the count reaches 0, so b.ne needs no cmp. It also prints how far past the start of the array each pointer finished:

post-index  total = 75, pointer ends at arr + 20pre-index   total = 75, pointer ends at arr + 16
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

Both totals match, but the pointers stop in different places. Post-index stepped after its last load, so it finished 20 bytes in, one word past the end of the array. Pre-index stepped before each load, so it started one word before the array and finished 16 bytes in, on the last element.

Byte order

A word takes 4 bytes, so storing one fills 4 addresses. AArch64 on Linux is little-endian: the byte at the lowest address holds the least significant 8 bits of the value, the "little end". The program below stores 0x11223344 and reads its bytes back one at a time with ldrb. It prints:

0x11223344 in memory, lowest address first: 44 33 22 11
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 4try it: run it, or step one instruction at a timeOpen in playground

The byte order only shows when a program reads a value at a different size from the one it stored. Store a word and load a word, and the bytes come back in the right order.

note

Keep each value at an address its own size divides evenly: a word at a multiple of 4, a doubleword at a multiple of 8. That is why .balign 4 goes before a .word that follows strings, whose lengths can be anything. Linux still lets an ordinary load or store use a badly aligned address, only more slowly, but sp is strict: using sp for a memory access while it is not a multiple of 16 stops the program. The next lesson keeps sp aligned.

Check yourself

  1. A byte in memory holds 0x80. What does ldrb w1 put in w1, and what does ldrsb w1 put there, both printed with %d?
  2. x9 holds the address of an array of words and w10 holds 3. Which address does ldr w11, [x9, w10, SXTW 2] read?
  3. x9 holds 1000. Which address does ldr w11, [x9], 4 read, and what does x9 hold afterward? Answer the same for ldr w11, [x9, 4]! starting from 1000 again.
  4. str w10, [x9] stores 0x0a0b0c0d. Which byte sits at the address in x9?

answers

show answers
  1. 128 and -128. The top bit of 0x80 is 1, so sign extension fills the upper bits with 1s.
  2. x9 + 12, the element at index 3.
  3. Post-index reads address 1000 and leaves 1004 in x9. Pre-index changes x9 to 1004 first and reads address 1004.
  4. 0x0d, the least significant byte, because the machine is little-endian.

Practice