Functions & the stack

Functions & the stack

A function is a named, reusable block of instructions. You jump to it, it does its job, and it hands control back to wherever you called it from. The point is to write a piece of logic once and reuse it from many places, instead of copying the same lines all over your program.

Call a function with jal ("jump and link"): it jumps to the label and saves the return spot in ra. The function ends with ret, which jumps back to ra. In practice you'll usually write call label and ret. These are pseudo-instructions: friendly names the assembler expands into the real ones, jal ra, label and jalr zero, ra, 0.

Arguments go in a0, a1, …; the return value comes back in a0. This is the calling convention, the shared agreement that lets separate pieces of code work together.

A square() function
li a0, 6         # argument
jal square       # call; ra remembers where to come back

li a7, 1         # square left its result in a0
ecall
li a7, 10
ecall

square:
  mul a0, a0, a0   # a0 = a0 * a0
  ret              # jump back to ra

Trace the call. li a0, 6 puts the argument 6 in a0. jal square jumps to the square label and saves the return address in ra. Inside, mul a0, a0, a0 computes 6 * 6 = 36 and leaves it in a0, the return-value register. ret jumps back to the line right after the call, where li a7, 1 and ecall print 36, and the program exits.

The stack

Only a handful of registers exist, so functions that call other functions need scratch space. The stack is memory you borrow via the stack pointer sp: subtract to make room, store what you must keep, restore it before ret.

The stack operates as a 'Last-In, First-Out' (LIFO) structure, and by convention in RISC-V, it grows downwards in memory. When you subtract from sp, you are structurally claiming new, safe space. You must be extremely careful to retrieve your stored values in a strictly mirrored order so you do not corrupt the execution flow.

Save and restore ra across a call
addi sp, sp, -4   # make room for one word
sw   ra, 0(sp)    # save the return address
# ... call other functions here ...
lw   ra, 0(sp)    # restore it
addi sp, sp, 4    # give the room back
ret

Why bother? Every jal/call overwrites ra. If your function calls another function, that inner call clobbers your return address and your own ret would jump to the wrong place. So addi sp, sp, -4 claims 4 bytes on the stack, sw ra, 0(sp) stashes ra there, you make your inner calls, then lw ra, 0(sp) restores it and addi sp, sp, 4 releases the space before ret.

Always restore sp to where it started, and restore ra before ret if you called anything. A mismatched stack is the classic way assembly programs crash.