This post is an introductory tutorial on memory access for beginners learning ARM assembly. Using the ARMv4 architecture as a reference, we’ll explore how the CPU uses the ldr (load) and str (store) instructions to read and write data to memory.

In this post, you’ll learn:

  • Why values must be loaded into registers before computation
  • How ldr and str are structured and what they do
  • What the square brackets ([]) mean in addressing mode
  • How to inspect registers and memory states using QEMU and GDB

Key Takeaway:
ldr and str are the foundation of all ARM programs. Through this tutorial, you’ll clearly see how the CPU actually reads and writes data.

CPU ↔ Memory data flow with LDR/STR Shows LDR reading from Memory to Register and STR writing from Register to Memory. CPU Registers r0 r1 r2 Memory 0x1000 0x1004 0x1008 STR LDR
LDR moves data Memory → Register; STR moves data Register → Memory.

Why We Need to Access Memory

The ARM CPU’s data-processing instructions (mov, add, etc.) never access memory directly — they operate only on values stored in registers. Therefore, any value stored in memory must be loaded into a register before performing an operation.

ARMv4 provides a total of 16 general-purpose registers (r0r15). It’s impossible to store all program data in these registers alone. Thus, the CPU uses registers as temporary storage for computation, while the main data resides in memory.

Registers vs Memory roles Shows 16 general-purpose registers contrasted with large memory as main storage. Registers (16) r0 r1 r2 r3 r4 r5 r6 r7 r8 r9 r10 r11 r12 r13 r14 r15 Main Memory (Data store) Large, holds program data Registers used for compute
Registers are temporary for compute; memory is the main data store.

Assembly Instructions for Memory Access

There are two basic instructions for accessing memory:

  • ldr: loads data from memory into a register.
  • str: stores data from a register into memory.

Their basic syntax is:

ldr Rd, [Rn]
str Rd, [Rn]

Where:

  • Rd: destination register to store or retrieve data.
  • Rn: base register containing the memory address.

ldr reads the value stored at memory address Rn into register Rd, and str writes the value in Rd to the memory location pointed to by Rn.

Meaning of Brackets and Base Register

The value inside square brackets ([]) represents a memory address, and the expression [address] refers to the value stored at that address.

For example:

ldr r0, [r1]

This means “read the value stored at the memory address pointed to by r1, and put it into r0.”

Base register addressing Shows r1 holding an address 0x1000 and [r1] meaning value at memory[0x1000]. r1 0x00001000 Memory[0x1000] 0x000004D2 [r1] ≡ *(uint32_t*)0x1000
If r1 holds 0x1000, then [r1] means the value stored at that memory address.

How the CPU Reads and Writes Data via an Address

The CPU interprets the value stored in the base register (Rn) as a memory address. It then locates the memory cell corresponding to that address and either reads or writes the value stored there.

In other words:

  • ldr = read data from memory using the address.
  • str = write data to memory using the address.

Practice Example

The following examples demonstrate the basic behavior of ldr and str. You can verify the results using QEMU (versatilepb) with gdb-multiarch.

For compiling, running on QEMU, and setting up GDB, refer to this post.

  .text
  .global _start
_start:
  mov r0, #0x3       @ store decimal 3 into r0
  ldr r1, =0x1000    @ store address 0x1000 into r1
  str r0, [r1]       @ [0x1000] = 3
  b .

Here, the CPU interprets the value in r1 (0x1000) as a memory address and stores the value of r0 (3) into that memory location.

Next, we can load the value back from memory:

  .text
  .global _start
_start:
  ldr r1, =0x1000    @ store address 0x1000 into r1
  ldr r0, [r1]       @ load value from [0x1000] into r0
  b .

If memory address 0x1000 previously held 3, then after execution, r0 will contain 3.


In this post, we explored the most fundamental way the CPU interacts with memory using ldr and str. In the next post, we’ll cover more flexible addressing modes using offsets.