ARM64 Memory — Load-Store Architecture and Addressing
In this tutorial, you will learn about ARM64 Memory. We cover key concepts, practical examples, and best practices to help you master this topic.
ARM64 memory access is restricted to ldr and str instructions with multiple addressing modes including base register, offset, pre-index, and post-index for flexible data access.
What You'll Learn
- Load/store instruction variants
- Addressing modes
- Stack operations with stp/ldp
- Atomic memory operations
- Barriers and ordering
Why It Matters
ARM64's load-store architecture means every memory access must be explicit. Doda Browser uses ARM64 SIMD loads for efficient image decoding on mobile devices.
Real-World Use
Data structure access, buffer manipulation, stack frame management, concurrent data structures, and memory-mapped I/O in Embedded Systems.
flowchart LR
A["ARM64 Memory"] --> B["ldr/str"]
A --> C["Addressing Modes"]
A --> D["Stack"]
B --> E["Load from mem"]
B --> F["Store to mem"]
C --> G["Base + offset"]
C --> H["Pre-index"]
C --> I["Post-index"]
style A fill:#2563eb,stroke:#2563eb,color:#fff
style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
style E fill:#dbeafe,stroke:#2563eb,color:#1e40af
Load/Store Variants
// Size suffixes
ldrb w0, [x1] // load byte (zero-extend)
ldrh w0, [x1] // load half-word (zero-extend)
ldr w0, [x1] // load word (32-bit)
ldr x0, [x1] // load double-word (64-bit)
// Sign-extending loads
ldrsb x0, [x1] // load signed byte
ldrsh x0, [x1] // load signed half-word
ldrsw x0, [x1] // load signed word -> sign extend to 64
// Store variants
strb w0, [x1] // store byte
strh w0, [x1] // store half-word
str w0, [x1] // store word (32-bit)
str x0, [x1] // store double-word (64-bit)
Base Register Only
// Simple register indirect
ldr x0, [x1] // x0 = *x1
str x0, [x1] // *x1 = x0
// Pair operations
ldp x0, x1, [x2] // x0 = *x2, x1 = *(x2+8)
stp x0, x1, [x2] // *x2 = x0, *(x2+8) = x1
Base + Offset
// Register + unsigned immediate (12-bit, scaled)
ldr x0, [x1, #16] // x0 = *(x1 + 16)
ldr x0, [x1, #255] // max unsigned offset
str x0, [x1, #32] // *(x1 + 32) = x0
// Register + register offset
ldr x0, [x1, x2] // x0 = *(x1 + x2)
ldr x0, [x1, x2, lsl #3] // x0 = *(x1 + x2 * 8)
// Negative offset (must use register)
sub x2, xzr, #16
ldr x0, [x1, x2] // x0 = *(x1 - 16)
Pre-Index and Post-Index
// Pre-index: update base, then load/store
ldr x0, [x1, #16]! // x1 += 16, then x0 = *x1
str x0, [x1, #16]! // x1 += 16, then *x1 = x0
// Post-index: load/store, then update base
ldr x0, [x1], #16 // x0 = *x1, then x1 += 16
str x0, [x1], #16 // *x1 = x0, then x1 += 16
// Useful for stack and array traversal
ldp x29, x30, [sp], #16 // pop pair from stack
Literal Loads
// Load from PC-relative literal pool
ldr x0, =0x1234567890ABCDEF // assembler creates literal
ldr x0, =label // load address of label
ldr x0, [pc, #offset] // manually PC-relative
The assembler places the constant in a literal pool near the instruction.
Stack Frame Convention
function:
// Prologue
stp x29, x30, [sp, #-32]! // save fp, lr
str x19, [sp, #16] // save callee-saved
add x29, sp, #0 // set frame pointer
// Body
// Local variables at [sp, #0..15]
// Epilogue
ldr x19, [sp, #16] // restore callee-saved
ldp x29, x30, [sp], #32 // restore fp, lr, deallocate
ret
Atomic Operations
// Atomic compare-and-swap (ARMv8.1+)
cas x0, x1, [x2] // if *x2 == x0, *x2 = x1; x0 = old *x2
// Atomic add
ldadd x0, x1, [x2] // x1 = *x2; *x2 += x0
// Atomic bit operations
ldset x0, x1, [x2] // x1 = *x2; *x2 |= x0
ldclr x0, x1, [x2] // x1 = *x2; *x2 &= ~x0
ldeor x0, x1, [x2] // x1 = *x2; *x2 ^= x0
Memory Barriers
// Full memory barrier
dmb sy // data memory barrier (full system)
// Store buffer drain
dsb sy // data synchronization barrier
// Instruction synchronization
isb // instruction synchronization barrier
// Acquire/release semantics
ldar x0, [x1] // acquire load
stlr x0, [x1] // release store
ldaxr x0, [x1] // acquire exclusive load (for LL/SC)
stlxr w2, x0, [x1] // release exclusive store
Common Mistakes
1. Alignment restrictions
ARM64 requires aligned access for ldr/str. Unaligned access works but is slower and may fault for device memory.
2. Using post-index when pre-index needed
Pre-index updates before access. Post-index updates after. Mixing them corrupts the base register.
3. Register offset scaling
ARM64 scales register offsets by the access size automatically. ldr x0, [x1, x2] accesses x1 + x2 (not auto-scaled). Add lsl #3 for qword scaling.
4. Forgetting explicit stack adjustments
ARM64 has no push/pop. Every stack operation needs explicit stp/ldp with pre/post-index.
5. Missing acquire/release on shared data
ARM64 has relaxed memory ordering. Use ldar/stlr for lock-free data structures or pair with dmb.
Practice Questions
1. What does ldr x0, [x1, #32]! do?
First adds 32 to x1 (pre-index), then loads x0 from the new address in x1.
2. How do you implement a stack push in ARM64?
stp x0, x1, [sp, #-16]! stores a register pair and decrements SP by 16.
3. What is the difference between ldrb and ldrsb?
ldrb loads a byte and zero-extends to 32/64 bits. ldrsb loads a byte and sign-extends.
4. How do you atomically increment a counter in ARM64?
ldadd x1, xzr, [x0] atomically adds x1 to *x0 and discards the old value.
Challenge: Write a memcpy implementation for ARM64 using ldp/stp.
Solution
// memcpy(dest, src, count)
// x0 = dest, x1 = src, x2 = count (bytes)
memcpy:
// Use pairs for 16-byte copies
lsr x3, x2, #4 // number of 16-byte blocks
lsl x3, x3, #4
sub x4, x2, x3 // remaining bytes
loop_pairs:
cbz x3, loop_byte
ldp x5, x6, [x1], #16
stp x5, x6, [x0], #16
sub x3, x3, #16
b loop_pairs
loop_byte:
cbz x4, done
ldrb w5, [x1], #1
strb w5, [x0], #1
sub x4, x4, #1
b loop_byte
done:
ret
FAQ
{{< faq question="Does ARM64 support unaligned memory access?" >}} Yes, for normal memory. Device memory (MMIO) requires aligned access. There is a small performance penalty for crossing cache lines. {{< /faq >}}
{{< faq question="What is the exclusive monitor in ARM64?" >}}
A hardware mechanism for atomic operations using ldxr (load exclusive) and stxr (store exclusive). Used for LL/SC (load-link/store-conditional).
{{< /faq >}}
{{< faq question="How does ARM memory ordering differ from x86?" >}} ARM64 has relaxed ordering by default. x86 is TSO (total store order). ARM needs explicit barriers or acquire/release instructions. {{< /faq >}}
{{< faq question="What is the adrp instruction used for?" >}
adrp loads the page address (4KB aligned) of a label relative to PC. Combined with add :lo12: for full address.
{{< /faq >}}
{{< faq question="Can I access memory with more than 32KB offset?" >}}
The unscaled offset is 9 bits (signed, -256 to +255). Scaled offset for ldr x0, [x1, #imm] is 12 bits (scaled by 8 = 0-32768).
{{< /faq >}}
Mini Project
Write an ARM64 function that reverses a byte array in place using ldr/str.
// void reverse_bytes(uint8_t* buf, int len)
// x0 = buf, x1 = len
reverse_bytes:
sub x2, x1, #1 // last = len - 1
lsr x3, x1, #1 // half = len / 2
loop:
cbz x3, done
ldrb w4, [x0] // first byte
ldrb w5, [x0, x2] // last byte
strb w5, [x0] // swap
strb w4, [x0, x2]
add x0, x0, #1 // first++
sub x2, x2, #1 // last--
sub x3, x3, #1 // half--
b loop
done:
ret
What's Next
Now that you understand ARM64 memory, proceed to ARM64 branches.
| Topic | Description | Link |
|---|---|---|
| ARM branches | Conditional branches | {{< ref "24-arm-branches" >}} |
| ARM functions | Functions and calling convention | {{< ref "25-arm-functions" >}} |
| ARM intro | ARM64 architecture | {{< ref "21-arm-intro" >}} |
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro