The Stack: Saving Values Across a Call
intermediateRISCVLesson 2 of 3
addi sp, sp, -16 # make room: grow the stack downsw ra, 12(sp) # save return addresssw s0, 8(sp) # save callee-saved s0addi s0, zero, 42 # ... do work, clobbering s0lw s0, 8(sp) # restore s0lw ra, 12(sp) # restore return addressaddi sp, sp, 16 # release the frame
registers
memory / stack
pc & flags
Registers are fast but few. The moment you call another function, you face a problem: it might overwrite the registers you were using. The stack is the shared scratch space that solves this.
The shape of a stack frame
The stack pointer sp marks the top of the stack, and on RISC-V the stack
grows downward — toward lower addresses. So you reserve space by
subtracting:
addi sp, sp, -16 # allocate 16 bytes
... # use the frame
addi sp, sp, 16 # free it — must balance exactly
sw (store word) writes a register to memory; lw (load word) reads it back:
sw ra, 12(sp) # mem[sp + 12] = ra
lw ra, 12(sp) # ra = mem[sp + 12]
Caller-saved vs callee-saved
RISC-V’s calling convention splits registers into two camps:
| Kind | Examples | Rule |
|---|---|---|
| callee-saved | s0–s11, sp, ra |
If you use one, you must restore it before returning. |
| caller-saved | t0–t6, a0–a7 |
Free to clobber; the caller saves them if it cares. |
This lesson’s trace saves ra and s0 because they’re callee-saved — watch the
memory panel light up as each is pushed, then the registers panel as each
is restored. By the final step, sp is back exactly where it began: a balanced
frame is the contract.
Try this
- What breaks if you forget the final
addi sp, sp, 16? (The stack “leaks” — every call driftsspfurther down.) - Why save
raat all? (A nested call would overwrite it, and you’d lose your way home.)