JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Addressing Modes and Load-Store

Every load and store has to answer one question: which byte of memory? Meet the small menu of addressing modes a clean ISA offers, and the load-store rule that keeps every slow memory touch corralled into just two instruction kinds.

The one question every memory access must answer

We met the memory model as one long flat array of bytes, each with its own numeric address. A load or store reaches into that array, but first it has to compute which address — and the menu of ways the ISA lets it compute that address is its set of addressing modes. This sounds like a footnote and is actually one of the most consequential choices in an ISA: it decides how compact and regular the instruction stream can be, and how fast the hardware can decode it. Where guide 1 sketched the idea, here we open it up.

A useful way to read any addressing mode is as a tiny formula for the effective address — the final byte number the access actually lands on. "Add the contents of a register to a small constant" is one such formula; "take where we are now and step forward" is another. A CISC ISA might offer a dozen elaborate formulas, including ones that read a register, scale it, add a second register, and add a constant, all in one mode. RISC-V deliberately offers only a tiny handful, because every extra mode is more work the decoder must do on every instruction, whether or not it uses it.

Base-plus-offset: the workhorse mode

RISC-V's everyday addressing mode for data is base-plus-offset (also called base-plus-displacement). You write it as `offset(rs1)`, and the effective address is simply the value held in register rs1 plus the constant offset. The register holds the base — often the start of an array, a struct, or the current stack frame — and the constant offset picks a specific slot relative to that base. Because the offset is a small constant baked into the instruction, it is an immediate, and the ISA spends only a few bits on it (twelve in RISC-V), which comfortably covers the modest distances real code needs.

This single mode is astonishingly expressive once you see how compilers lean on it. An array element `a[i]` becomes: compute the byte position of `a[i]` into a register (that becomes the base), then load with offset 0. A field three slots into a struct is just `12(rs1)` if each slot is four bytes. A local variable lives at a fixed offset from the stack pointer, so `8(sp)` reads it directly. One formula — register plus constant — quietly covers arrays, structs, and local variables alike. That is the payoff of choosing the right small set instead of a sprawling one.

# Loading a[i] (4-byte ints) where a's start is in x10, i is in x11.

  slli  x12, x11, 2      # x12 = i * 4      (shift left by 2 = times 4)
  add   x12, x10, x12    # x12 = &a[0] + i*4   <- this register is the BASE
  lw    x13, 0(x12)      # x13 = memory[x12 + 0]   <- base-plus-offset, offset 0

# A struct field: if 'p' (in x14) points at a struct and the field
# we want sits 12 bytes in, no extra add is needed at all:

  lw    x15, 12(x14)     # x15 = memory[x14 + 12]   <- the offset does the work
Base-plus-offset at work. The register supplies a computed base; the immediate offset reaches a specific slot. The same mode serves arrays and struct fields.

Reaching code: PC-relative addressing

Loads and stores reach data; branches and jumps reach code, and they use a closely related mode called PC-relative addressing. Instead of naming an absolute target address, a branch carries an offset that is added to the current program counter to produce the destination. "If x11 is not zero, jump 40 bytes back from here" is how a loop closes. The mode is the same flavour of formula — a base (here the PC) plus a constant offset — applied to the instruction stream rather than the data array.

PC-relative addressing pays a quiet but real dividend: it makes code position-independent. Because every branch target is described as a distance from the current instruction rather than a fixed address, the whole block of code can be loaded anywhere in memory and still work — every internal jump still lands correctly. That is exactly what an operating system needs when it places programs and shared libraries at whatever addresses are free. The same simple base-plus-offset idea, pointed at the PC, buys relocatable code almost for free.

The load-store rule and why it earns its keep

All of this lives under one defining decision: RISC-V is a load-store architecture. The rule is blunt and worth saying again precisely — arithmetic and logic instructions may name only registers as operands, never memory. The single way data crosses between memory and the register world is through the two memory instructions: a load copies one item from memory into a register, a store copies a register's value out to memory. There is no "add a memory location to a register in one instruction" the way a CISC machine allows.

Why accept this apparent inconvenience? Because it pays off three ways. First, regularity: every instruction is the same length and most have the same register-only shape, so the decoder and pipeline stay simple and fast — that is the gift the architecture hands the microarchitecture. Second, it confines the slow, variable-latency operation — touching memory — to two clearly marked instruction kinds, instead of letting it hide inside an arithmetic instruction. Third, with values pulled into the register file up front, a compiler can reuse a loaded value many times without re-reading memory, which is one of the most effective optimizations there is.

What an address actually lands on: bytes, alignment, endianness

An effective address is a byte number, but loads and stores come in widths — load a byte, a halfword, a word, a doubleword. Two honest wrinkles follow. Data alignment: hardware prefers (and some ISAs require) that a 4-byte load sit at an address that is a multiple of 4, an 8-byte load at a multiple of 8, and so on. A naturally aligned access fits inside one memory transfer and one cache line; a misaligned one may straddle two, costing extra work or, on strict machines, faulting outright. Compilers pad structs precisely to keep fields aligned.

Endianness: when a multi-byte number sits in byte-addressed memory, which byte comes at the lowest address — the most significant or the least? A big-endian machine stores the most significant byte first; a little-endian machine stores the least significant first. Neither is wrong, but the memory model must pin one down, because a value written by one convention and read by the other comes out scrambled. This matters the moment bytes leave the chip — across a network, or in a file shared between differently-built machines. It is invisible until it suddenly is not.

One more honest detail about how the offset reaches its full width. A RISC-V offset is only 12 bits, but an address is 32 or 64 bits wide. Before it is added to the base, the offset is sign-extended — its top bit is copied leftward to fill the wider field — so that a small negative offset like -8 still works correctly, letting `-8(sp)` reach a slot just below the stack pointer. The same one trick that makes a short immediate stand in for a full-width number, met in guide 3, is exactly what makes offsets reach both up and down from a base.

The cost of a load, and the hazard waiting downstream

There is one last truth the ISA's clean picture hides: a load is not free, and it is not even reliably fast. A load whose data is already sitting in cache returns in a handful of cycles; one that misses all the way to main memory can cost hundreds. The addressing mode computes which byte, but how long fetching it takes depends entirely on the memory hierarchy underneath — a microarchitecture story, not an ISA one. This is why arranging data so accesses hit nearby, recently-used locations (good locality) can make identical code run many times faster.

Even when a load hits cache, it sets a subtle trap downstream. In a pipelined chip the loaded value is not ready the instant the load issues; it arrives a stage later. If the very next instruction tries to use that value, the pipeline must wait — this is the load-use hazard, and it is the reason a load is often best placed a little earlier than the instruction that needs its result. Compilers reorder code precisely to slip useful work into that gap.