A CPU instruction set gives you arithmetic, loads, stores and branches — but nowhere in the
The heart of a call is a question: after the function finishes, where do we resume? RISC-V answers with
the jump-and-link instruction, jal. It does two things at once: it jumps to the
function, and it saves the address of the following instruction — the return address — into a
register, by convention ra (which is x1). To return, the callee just jumps back to
whatever is in ra, using jalr x0, 0(ra) (aliased ret).
For a leaf function like square — one that calls nothing else — that is the entire story:
no memory needed. The trouble starts when a function must call another function, because the second
jal would overwrite ra, erasing the way home. The fix is the stack.
Each active function call gets a slab of memory called a stack frame (or activation record).
The stack pointer sp marks the current top; a call pushes a new frame by moving
sp down (stacks grow toward lower addresses), and a return pops it by moving
sp back up. Inside a frame the function keeps everything it must remember across an inner call: the
saved return address, saved registers, and its local variables — each at a fixed
sp
A non-leaf function's job at entry (its prologue) is to carve out this frame and stash anything precious; at exit (its epilogue) it restores those values and gives the space back:
Here is the ABI's cleverest idea. Registers are shared by everyone, so when A calls B, some register B clobbers might hold a value A still needs. Who is responsible for saving it? Saving every register on every call would be wasteful. So the ABI splits the 32 registers into two camps by convention:
| Class | RISC-V names | Rule | Preserved across a call? |
|---|---|---|---|
| Caller-saved (temporaries) | t0–t6, a0–a7 | callee may freely clobber; caller must save if it cares | No |
| Callee-saved (saved) | s0–s11, sp | callee must restore before returning | Yes |
| Arguments / return | a0–a7 (a0/a1 also return) | first 8 args here; results in a0/a1 | No (they carry the payload) |
| Return address | ra | set by jal; save it before an inner call | caller-saved by convention |
The names encode the deal. If you keep a value in a temporary and then make a call, expect it to
be gone — save it yourself (you are the caller). If you keep it in a saved register, you may
trust it survives — but if you want to use an s register you must save the previous owner's
copy first (you are now the callee). Every register is somebody's responsibility, and no value is ever silently
lost.
a0–a7; return values come back
in a0/a1;t*, a*) may be clobbered by the callee —
the caller saves them if needed;s*, sp) must be restored by the callee
before it returns;ra, set by jal and spilled to the
stack before any nested call.
Recursion needs no special hardware — it falls out for free. Each recursive call pushes its own frame,
with its own saved ra and its own locals, so a hundred nested calls simply means a
hundred frames stacked in memory. When each returns, it pops its frame and restores the caller's state exactly.
The simulation below models the stack explicitly for
Every recursive call pushes a frame and moves sp down; nothing pushes it back up until a call
returns. Infinite (or just too-deep) recursion therefore marches sp relentlessly downward until it
runs off the end of the region the OS reserved for the stack — often bumping into a deliberately unmapped
guard page. The load or store into that forbidden page faults, the OS sees it, and your program dies with
"stack overflow" (the website was named after the error). It is the physical shadow of an unbounded recursion:
each missing base case is another 16 bytes closer to the cliff. This is also why deep recursion is dangerous in
systems code and why compilers prize tail-call optimisation, which reuses one frame instead of pushing a
new one.
Two symmetric mistakes. A leaf function (calls nobody) does not need to save
ra or build a frame at all — doing so is pure overhead, and good compilers skip it. But a
non-leaf function that forgets to save ra before its inner jal is a
disaster: the inner call overwrites ra, so when the outer function runs ret it jumps to
the wrong place and the program derails. The rule is crisp: save ra to the stack if and only if
you make a call. Mixing this up is the single most common bug in hand-written assembly.
The calling convention is not a law of physics — the hardware would happily let you pass arguments in any registers you liked. Its value is that everyone agrees on the same one. That agreement is what lets separately compiled objects link together, lets your code call the C standard library, lets a debugger unwind the stack to show you a backtrace, and lets one language call another. The ABI is the ISA contract extended into a society of cooperating code. With calls in hand, the final lesson of this module widens the ISA in a different direction — exposing data parallelism through vector and SIMD instructions.