You have met
An addressing mode is a rule for computing the effective address (EA) — the actual memory location an instruction reads or writes — from the bits in the instruction plus the current register values. RISC keeps essentially four, and a load or store uses only the base+displacement one:
| Mode | Operand is… | Effective address | RISC-V example |
|---|---|---|---|
| Register | a value in a register | — (no memory) | add x3, x1, x2 |
| Immediate | a constant in the instruction | — (no memory) | addi x3, x1, 42 |
| Base + displacement | memory at register + constant | lw x3, 8(x1) | |
| PC-relative | a target near the current instruction | beq x1, x2, label |
That's essentially the whole set for RISC-V. Notice base+displacement is the only mode that reaches data memory —
and even PC-relative is really just base+displacement with the PC as the base. CISC ISAs like x86 pile on more —
indexed, scaled-indexed [base + index*scale + disp], indirect through memory — but RISC bets that the
compiler can synthesise those from the primitives, and it can.
The single mode
a[i]: put the array's base in a register, and the byte offset
p->field: the base register holds the pointer p;
the field's fixed offset within the struct is the displacement. lw x5, 12(x10) loads the field at
byte 12 of the struct that x10 points to.sp, and each local
sits at a known displacement from it — lw x5, -8(sp).One mode, three pillars of real programs. That coverage is precisely why RISC designers felt safe throwing the fancier modes away.
Let's make the arithmetic concrete. Say an int array of 4-byte elements starts at address
x then an
8-byte y starts at
Branches and jumps use PC-relative addressing: the target is
imm is a signed offset stored in the
instruction. Two big wins fall out of this choice. First, most branches go somewhere nearby (the top or
bottom of a loop), so a small signed immediate reaches them — no need to spend 32 bits on an absolute target.
Second, and more deeply, PC-relative code is position-independent: because every branch target
is expressed as a distance from the current instruction, the whole block of code can be loaded at any address in
memory and still works untouched. That is exactly what a modern OS needs to load shared libraries and to
randomise addresses for security (ASLR).
Old CISC machines (and the DEC VAX especially) had glorious modes like auto-increment: a single
instruction would load through a pointer and bump the pointer to the next element, perfect for
*p++. RISC deleted it. So how does a RISC loop stride through an array? It just spends one extra,
dirt-cheap instruction: lw x5, 0(x10) to load, then addi x10, x10, 4 to advance the
pointer. Two simple, fixed-cost instructions instead of one clever, variable-cost one — the same RISC bargain you
keep meeting. And because they pipeline so well, the "extra" instruction is nearly free, while the decoder stays
blissfully simple. The compiler even hoists the increment out of the way with loop optimisations.
A classic bug: writing lw x5, 3(x10) expecting the fourth int of an array. Memory is
lw x5, 12(x10). Loading at offset 3 would read straddling bytes of the wrong (and
possibly misaligned) element. Always multiply the index by the element size to get the byte displacement — that
multiply is where the "scale" in x86's scaled-index mode comes from, and it's the step you must do by hand on
RISC.
Addressing modes are a microcosm of the whole RISC/CISC argument. CISC offered a rich menu, hoping to match every access pattern with a bespoke mode; RISC offered a tiny kit and trusted the compiler to assemble the rest from base+displacement plus ordinary arithmetic. Fewer modes means a simpler, faster decoder and a shorter effective address computation — one add — that fits neatly in a pipeline stage. The patterns that matter (arrays, structs, stack locals, nearby branches) are all covered, and the ISA stays lean. Next we use base+displacement in earnest: building the stack frames that make procedure calls work.