Loads and stores are the only RISC-V instructions that touch memory. Every variable that does not fit in a register, every array element, every field of a structure and every device register is reached through them, and all of them compute their address the same simple way: a register plus a small constant. This article explains the load and store instructions, shows how the compiler turns C variables, arrays, pointers, structures and the stack into addresses, compares RISC-V's single addressing mode with the richer ones of other ISAs, and shows how often programs load and store and what a load costs when its result is needed right away.

14 min read | Beginner

What You'll Learn

  • Use the RV32I load and store instructions and their sizes and extensions
  • Explain base plus offset addressing and its range
  • Trace how globals, arrays, pointers, structures and the stack become addresses
  • Explain why a load followed immediately by its use costs a cycle, and how compilers avoid it

Only Loads and Stores Touch Memory

RISC-V is a load/store architecture: arithmetic instructions work only on registers, and memory is reached only through load and store instructions. To add a number in memory to a register, a program first loads it into a register, then adds. Registers and the Register File explains why: registers are fast and few, memory is large and slower, and keeping the two jobs apart makes every instruction simple.

RV32I has five loads and three stores:

InstructionSizeWhat it does
lw rd, off(rs1)word, 4 bytesload 32 bits into rd
lh / lhu rd, off(rs1)halfword, 2 bytesload 16 bits, sign-extend (lh) or zero-extend (lhu) to 32
lb / lbu rd, off(rs1)byteload 8 bits, sign-extend (lb) or zero-extend (lbu) to 32
sw rs2, off(rs1)wordstore all 32 bits of rs2
sh rs2, off(rs1)halfwordstore the low 16 bits of rs2
sb rs2, off(rs1)bytestore the low 8 bits of rs2

There is no lwu in RV32I, because a 32-bit load already fills the whole register. Stores need no signed and unsigned variants, because they only write bits and never widen anything. The byte order inside a word is little-endian, as Memory, Addresses and Endianness explains.

One Addressing Mode: Base Plus Offset

An addressing mode is a rule for computing the address of a memory access. RISC-V has exactly one: the effective address is the value of register rs1, the base, plus a 12-bit signed constant from the instruction, the offset:

address = rs1 + sign-extended offset        offset range: -2,048 to +2,047 bytes
lw a5, 8(a4)      reads the word at a4 + 8
sw a3, -4(a4)     writes a3 to the word at a4 - 4

An offset of 0 gives plain register-indirect addressing, the address is just the register. A base of x0, which is always zero, gives absolute addressing for the lowest and highest 2 KB of the address space. Anything else, a scaled index or a larger offset, is computed with ordinary arithmetic instructions first. The same adder in the execute stage that does an add computes every address, so loads and stores need no extra address hardware.

Quick Check

Q: Register a4 holds 0x0000_1000. Which addresses do lbu a5, 2047(a4) and lbu a5, -2048(a4) read, and can a single load reach a4 + 4096?

Show Answer

The first reads 0x1000 + 2047 = 0x17FF, the second 0x1000 - 2048 = 0x0800. a4 + 4096 is out of the offset range, so the program first needs an extra instruction, for example adding 4096 to a copy of a4 with lui and add, then a load with a small offset.

From C Variables to Addresses

A C programmer never writes addresses explicitly for ordinary variables, but every variable that lives in memory has one, and the compiler must produce it in a register before it can load or store. Small compiled programs show each common case.

Global Variables and Arrays

A global array such as static u32 a[1024]; gets a fixed address from the linker, here 0x58. The compiler builds that address with lui and addi, then uses it as the base:

  18:  000007b7   lui   a5,0x0
  1c:  05878713   addi  a4,a5,88        # a4 = 0x58, the address of a[0]

Larger programs often reserve a register, gp (the global pointer), to point into the middle of the global data, so that any global within 2 KB of it needs just one load with a gp-relative offset. These small programs are linked without that optimization, so every global address is built explicitly.

Walking an Array: Pointers Instead of Indexes

Element a[i] of a word array is at address a + 4 x i. A literal translation of a loop over i would compute that address every time: shift i left by 2, add the base, then load. Compilers usually do something cheaper: they keep a pointer that starts at a[0] and add 4 to it in each iteration, and stop when it reaches the end. A loop that copies memory works this way with two pointers:

  60:  00062683   lw    a3,0(a2)        # load src[i]
  64:  00460613   addi  a2,a2,4         # src pointer + 4
  68:  00470713   addi  a4,a4,4         # dst pointer + 4
  6c:  fed72e23   sw    a3,-4(a4)       # store to dst[i], the word before the new pointer
  70:  feb618e3   bne   a2,a1,60        # until src reaches its end

The store uses the offset -4 because the destination pointer has already been moved on. The offset field lets the compiler place the pointer update wherever it fits best, without an extra instruction to undo it. The loop variable i has disappeared entirely: the end test compares the pointer with the end address in a1.

Structures: Fields at Fixed Offsets

A C structure places its fields one after another at fixed offsets from its start. In a linked list, each node might be struct node { struct node *next; u32 val; }: next at offset 0, val at offset 4. Walking the list is the classic pointer chasing loop:

 15c:  0047a703   lw    a4,4(a5)        # a4 = p->val   (offset 4)
 160:  0007a783   lw    a5,0(a5)        # p = p->next   (offset 0)
 164:  00e50533   add   a0,a0,a4        # s += val
 168:  fe079ae3   bnez  a5,15c          # until p is NULL

The base register holds the address of the structure, and the offset selects the field. Every field access is one instruction, which is why base plus offset is such a good fit for C. Note that the second load overwrites the register it uses as its own base: the new p is loaded from the old p.

The Stack

Local variables that do not fit in registers, and registers that a function must preserve, go on the stack, a region of memory addressed through the stack pointer sp. The main function of a linked-list program starts by making room and saving registers:

  18:  fe010113   addi  sp,sp,-32       # allocate 32 bytes of stack
  20:  00812c23   sw    s0,24(sp)       # save s0 at sp + 24
  34:  00112e23   sw    ra,28(sp)       # save the return address at sp + 28
  ...
 174:  01c12083   lw    ra,28(sp)       # restore before returning
 18c:  02010113   addi  sp,sp,32        # free the stack space

Every stack access is base plus offset with sp as the base. How a Program Runs: Loops, Calls and the Stack explains why functions save registers and how the stack grows and shrinks.

Device Registers

Devices are reached the same way: build the device's address in a register, then load or store with an offset. A small test program might end with lui t0, 0xf0000 followed by sw a0, 0(t0), a store to a device register at 0xF000_0000 that reports its result.

Alignment in Practice

A word access is aligned when its address is a multiple of 4, a halfword access when its address is a multiple of 2. RISC-V lets each processor decide what to do with a misaligned access: handle it in hardware, possibly slowly with two memory accesses, or raise a misaligned-address exception and let software handle it. Small cores usually expect aligned accesses, because supporting misaligned ones means splitting an access across two memory words and joining the halves.

Compilers make sure ordinary C code never produces misaligned accesses. Every variable is placed at an address that is a multiple of its size, and inside structures the compiler inserts unused padding bytes where needed. In struct { u8 tag; u32 value; }, value cannot start at offset 1; the compiler puts it at offset 4, leaves bytes 1 to 3 unused, and the structure takes 8 bytes instead of 5. Reordering the fields from largest to smallest often removes padding, which matters for large arrays of structures.

Misaligned accesses appear when software reinterprets raw bytes, for example reading a 32-bit length field at byte offset 3 of a received network packet through a cast pointer. Portable code copies such fields byte by byte, or with memcpy, so that the compiler can generate accesses that are safe on any processor.

Richer Addressing Modes Elsewhere

Other ISAs offer more ways to form an address. x86 allows base + index x scale + displacement in a single instruction, so a[i] with i in a register is one access. Arm has modes that update the base register automatically after the access (post-increment), which turns the memcpy loop's pointer updates into part of the load and store. x86 even lets arithmetic instructions read memory directly, such as adding a memory word to a register in one instruction.

Richer modes mean fewer instructions, but more complex hardware: an address unit that shifts and adds three values, loads that also write a second register, instructions of variable length. RISC-V chose the opposite trade. Its one mode needs only the existing adder, every load writes exactly one register, and the compiler's pointer tricks recover most of the instruction count. The consequences of such choices are the subject of ISA Design Choices: RISC, CISC and Extensions.

Where Does This Matter?

Engineers reading a crash dump or a waveform often have only a faulting load's address, its base register and its offset. Recognizing "offset 4 from a pointer" as a structure field, or "a negative offset from sp" as a local variable, is how they find which line of C caused it. Verification engineers write targeted tests around the same corners: offsets of -2,048 and +2,047, base registers that wrap around, and loads that overwrite their own base.

How Often Programs Load and Store

In one test of six small programs, every executed load and store was counted. The share varied widely, from almost half for a sort that constantly moves data to about 3 percent for a checksum that keeps its working values in registers. Adding up an array came to a quarter, and copying memory to about a fifth. Across typical integer programs, loads and stores together are often a quarter to a third of all instructions, which is why the memory system matters so much for performance.

When the Loaded Value Is Needed Right Away

In a pipelined processor, a load delivers its data later than an arithmetic instruction delivers its result: the data only arrives after the memory access stage. If the very next instruction needs the loaded value, it must wait, usually one cycle in a five-stage pipeline. This one-cycle bubble is a load-use stall. The bubble sort's comparison is a textbook case:

  64:  0007a703   lw    a4,0(a5)        # a[j]
  68:  0047a683   lw    a3,4(a5)        # a[j+1]
  6c:  00e6f663   bgeu  a3,a4,78        # needs a3 immediately: one stall cycle

Sorting 64 values this way compares 63 + 62 + ... + 1 = 2,016 pairs, and one test confirmed exactly one stall per comparison. A pointer-chasing loop over a linked list, by contrast, had none, although each node's address comes from the previous load: the compiler placed the add between the load of next and the branch that tests it, and loaded val one instruction ahead of its use. Arranging instructions so that a loaded value is not needed immediately is called instruction scheduling, and compilers do it routinely. The Pipelining chapter explains the stall in detail.

Quick Check

Q: Why does the bubble sort stall on every comparison while the compiler avoided stalls in the linked-list loop?

Show Answer

In the sort, the comparison needs both loaded values, and there is no other useful work in that small loop body to place between the second load and the branch. In the list loop, the add of the previous value is independent work that the compiler could move between the load and its use.

Common Mistakes

Mistake #1: Using lb where lbu is meant

Bytes of text and raw data are unsigned. lb turns every byte above 0x7F into a negative number. Use lbu, and lhu for unsigned halfwords.

Mistake #2: Forgetting the offset is in bytes

Element i of a word array is at byte offset 4 x i, not i. Field offsets in structures are also in bytes, and include any padding the compiler adds for alignment.

Mistake #3: Assuming offsets are unlimited

The offset is 12 bits, so -2,048 to +2,047. A structure larger than 2 KB, or a stack frame larger than that, needs extra instructions to form addresses for its far fields.

Practice Exercise

Challenge: Addresses From C

A program declares struct pkt { u32 len; u16 id; u8 type; u8 flags; u32 data[4]; }, with no padding needed, and a pointer to one in register a0. (a) Give the byte offset of each field. (b) Write the single instruction that loads p->flags into a1, and the one that loads p->data[2] into a2. (c) Write the instruction that stores the low 16 bits of a3 into p->id. (d) How many bytes does the structure take?

Hint

Fields follow each other in declaration order. u32 is 4 bytes, u16 2, u8 1. data[2] is the third word of the array.

Solution

(a) len at 0, id at 4, type at 6, flags at 7, data at 8 (data[0] at 8, data[1] at 12, data[2] at 16, data[3] at 20). (b) lbu a1, 7(a0) (unsigned byte) and lw a2, 16(a0). (c) sh a3, 4(a0). (d) 8 + 4 x 4 = 24 bytes. Every field is reachable with one instruction because all offsets are far below 2,048.

Summary

  • RV32I has loads lb, lbu, lh, lhu, lw and stores sb, sh, sw; only they access memory.
  • Every address is a base register plus a 12-bit signed offset, -2,048 to +2,047 bytes, computed by the ordinary adder.
  • Compilers turn globals into lui/addi addresses, array loops into moving pointers, structure fields into offsets, and locals into sp-relative accesses.
  • Loads and stores ranged from about 3 percent to almost half of the executed instructions across six small programs.
  • A load whose value is used by the next instruction costs a stall cycle, and instruction scheduling can often remove it.
Did you find what you were looking for?

Quiz

No quiz questions available for this article.