HomeRISC-V from ScratchDay 5
DAY 5 · THE RISC-V ISA

Memory, the Stack & Calling Convention

By EcrioniX · Updated Jun 7, 2026

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.

1. Memory is just a giant numbered shelf

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.

Two details that bite beginners

2. The memory map

Memory isn't a free-for-all; a program divides its address space into regions. Roughly, from low addresses up:

A program's memory map high address Stackgrows DOWN ↓ (sp) ↓ ↑ Heap (grows up ↑) Data (globals) Text (your code) low address stack & heap grow toward each other
Figure — Code and globals at the bottom; the heap grows up, the stack grows down from the top.

3. The stack — a pile you push and pop

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:

💡 A spring-loaded plate dispenser

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-pop.s — manual stack operations
# 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

4. Why the stack exists: nested function calls

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.

5. The calling convention (the shared rulebook)

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)RoleWho preserves it
a0–a7function arguments; a0 (& a1) = return valuecaller-saved
rareturn addresscaller-saved (save before nested calls!)
t0–t6temporaries (scratch)caller-saved
s0–s11saved registers (locals)callee-saved
spstack pointercallee-saved (must be restored)

6. A real function: prologue & epilogue

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):

func.s — a function that calls another
# 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.)

✅ Day 5 in one line

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.

🎯 Day 5 takeaways

Quick check

  1. Which direction does the stack grow, and what does sp point to?
  2. How do you "push" a register if there's no push instruction?
  3. Why must a function that calls another function save ra?
  4. Which registers carry arguments and the return value?

FAQ

How is memory organised?

A byte-addressable array (32-bit addresses → 4 GB), little-endian, with words usually aligned to 4-byte boundaries.

What is the stack?

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.

What is the calling convention?

The agreed rules: a0–a7 args, a0/a1 return, ra return address, sp stack, and caller- vs callee-saved registers.

What is a prologue/epilogue?

Start-of-function code that saves ra and callee-saved regs on the stack, and end-of-function code that restores them and returns.

Previous
← Day 4: The RV32I instruction set

← Back to the full roadmap  ·  Open the Verilog simulator →