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.
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.
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.
sp to where it started, and restore ra before ret if you called anything. A mismatched stack is the classic way assembly programs crash.