Registers are fast but few — only 32. Real programs need to store far more, and to call functions that call other functions. For that we need memory, the stack, and a shared rulebook called the calling convention. This is the last ISA concept before Day 6's assembly, and it's the one that makes "functions" actually work.
Forget anything complicated. Memory is one enormous array of bytes, and every byte has a number — its address. In RV32, addresses are 32 bits, so there are up to 4 GB of byte-slots (2³² of them). To use memory you give an address and either read or write — exactly the load/store instructions from Day 4.
Memory isn't a free-for-all; a program divides its address space into regions. Roughly, from low addresses up:
The stack is a special memory region used like a stack of plates: last in, first out. The register sp (x2) always points to the top of the stack. Two surprises for beginners:
sp smaller.sp, then store; pop by loading, then adding back.Picture a plate dispenser: you press the top plate down to add one (push), and the newest plate is the first you take back (pop). The stack is the same — and sp is your finger marking where the top plate is. Add a plate → sp moves; take it back → sp returns.
# Push two registers, then pop them back (stack grows DOWN)
addi sp, sp, -8 # make room for 2 words (push)
sw ra, 4(sp) # save ra on the stack
sw s0, 0(sp) # save s0 on the stack
# ... do work that may clobber ra and s0 ...
lw s0, 0(sp) # restore s0 (pop)
lw ra, 4(sp) # restore ra
addi sp, sp, 8 # free the space
Here's the problem it solves. From Day 4, jal calls a function and saves the return address in ra. But if that function calls another function, the second jal overwrites ra — and now the first function has forgotten where to return! The fix: each function saves ra on the stack before making nested calls, then restores it. The stack lets calls nest arbitrarily deep, each remembering its own return address.
For functions written by different people (or compilers) to work together, everyone must agree on how arguments are passed, where results go, and which registers must be preserved. That agreement is the calling convention (RISC-V's is similar to ARM's AAPCS). Using the ABI names from Day 2:
| Register(s) | Role | Who preserves it |
|---|---|---|
| a0–a7 | function arguments; a0 (& a1) = return value | caller-saved |
| ra | return address | caller-saved (save before nested calls!) |
| t0–t6 | temporaries (scratch) | caller-saved |
| s0–s11 | saved registers (locals) | callee-saved |
| sp | stack pointer | callee-saved (must be restored) |
Putting it together: a function that calls another function follows a standard pattern — a prologue (save ra and any s-registers), the body, and an epilogue (restore, return):
# int outer(int n) { return inner(n) + 1; }
outer:
addi sp, sp, -16 # PROLOGUE: make a stack frame
sw ra, 12(sp) # save return address (we'll call -> ra clobbered)
sw s0, 8(sp) # save a callee-saved reg we want to use
mv s0, a0 # keep n in s0 across the call
# a0 already holds n -> call inner(n)
jal ra, inner # inner's result comes back in a0
addi a0, a0, 1 # return value = inner(n) + 1
lw s0, 8(sp) # EPILOGUE: restore
lw ra, 12(sp) # restore return address
addi sp, sp, 16 # pop the frame
ret # jalr x0, 0(ra) -> return to caller
The frame on the stack (16 bytes here) is this function's private scratch space. Skip saving ra and a non-leaf function returns to the wrong place — the single most common beginner bug. (A leaf function that calls nothing can skip the frame and just ret.)
Memory is a byte-addressable array (little-endian, aligned), split into text/data/heap/stack. The stack (top tracked by sp, grows down) is push/pop scratch space built from sw/lw. The calling convention sets the rules: a0–a7 args, a0 return, ra return address, s0–s11 callee-saved — and any non-leaf function must save ra on the stack in its prologue and restore it in the epilogue.
sp marks the top; push = addi sp,sp,-N+sw, pop = lw+addi sp,sp,N.ret.sp point to?ra?A byte-addressable array (32-bit addresses → 4 GB), little-endian, with words usually aligned to 4-byte boundaries.
A LIFO memory region for function scratch space; sp points to the top and it grows downward. Push/pop are done with sw/lw + adjusting sp.
The agreed rules: a0–a7 args, a0/a1 return, ra return address, sp stack, and caller- vs callee-saved registers.
Start-of-function code that saves ra and callee-saved regs on the stack, and end-of-function code that restores them and returns.