05. MIPS Functions
Functions in MIPS#
A C function call hides several coordinated actions:
- evaluate the arguments;
- transfer control to the function;
- create the function's local state;
- perform its body;
- produce a return value;
- resume at the instruction after the call.
MIPS does not have one instruction which performs all of this. Instead, a calling convention assigns responsibilities to registers, jal and jr handle the transfer of control, and the stack preserves state which cannot safely remain in registers.
A function call involves two pieces of code. The caller is the function making the call; the callee is the function being called. Because they may be written separately, they need a shared agreement about where arguments go, where a result appears, and which registers may be changed. That agreement is the calling convention.
Think of it as a hand-off contract. The caller agrees to place the first arguments in $a0–$a3. The callee agrees to place a register-sized result in $v0. Temporary registers may be overwritten, while saved registers must be returned with their original values. These names do not make the registers physically different; they describe the promises functions make about them.
This note builds the mechanism in stages: first a leaf function, then arguments and a return value, then a non-leaf call, preserved values, a concrete stack frame, multiple calls and finally recursion. At each stage the same question drives the translation: which values must still exist after the next jal?
Calling and Returning#
jal function means jump and link. It stores the address of the following instruction in $ra, then continues at function.
jal product # call product
# execution resumes here after product returnsThe function returns with:
jr $raflowchart TD
C["Caller executes<br/>jal product"]
R["$ra receives the<br/>resume address"]
F["Control enters<br/>product"]
J["product executes<br/>jr $ra"]
N["Caller resumes after<br/>the jal"]
C --> R --> F --> J --> N
A plain b function or j function is not a normal function call because it does not record where execution should return.
Arguments and Return Values#
The first four register-sized arguments are placed in $a0–$a3. A register-sized result is returned in $v0.
int product(int x, int y) {
return x * y;
}
int result = product(6, 7); li $a0, 6 # first argument
li $a1, 7 # second argument
jal product
move $t0, $v0 # int result = product(6, 7);
product:
mul $v0, $a0, $a1 # return x * y;
jr $raThe callee receives bits, not C type information. Correct interpretation comes from the function contract and instruction choice.
Note
- More than four arguments
A full ABI uses the stack for additional arguments and for values which do not fit in registers. The Week 3 course material treats this as out of scope except for possible challenge work. Concentrate first on $a0–$a3 and $v0.
The Calling Convention#
The calling convention prevents separate functions from silently destroying each other's state.
| Register group | Meaning | Preserved across a call? |
|---|---|---|
$a0–$a3 |
arguments | no |
$v0 |
return value | no |
$t0–$t9 |
temporaries | no |
$s0–$s7 |
saved values | yes, by the callee |
$sp |
stack pointer | yes |
$fp |
frame pointer | yes |
$ra |
current return address | must be saved if the function will overwrite it |
“Preserved” does not mean a function may never use an $s register. It means that if the function changes one, it must save the incoming value and restore that exact value before returning.
Caller-Saved and Callee-Saved#
$t Registers: Caller-Saved#
A called function may overwrite every $t register. Therefore, if the caller needs a value after jal, it must not leave the only copy in $t0–$t9.
li $t0, 10
jal mystery
# $t0 cannot be assumed to still contain 10Treat callees as black boxes. Strict course tests may deliberately clobber temporary registers to expose code which relies on an implementation accident.
$s Registers: Callee-Saved#
Use an $s register for a value which must survive a function call:
move $s0, $a0 # preserve original argument
jal other_function
# $s0 is still available hereHowever, the current function has now modified $s0, so it owes its caller the original $s0. It must save that value in its prologue and restore it in its epilogue.
Why $ra Must Be Saved#
Every jal writes a new return address into $ra. Consider main calling f, then f calling g:
main --jal f--> $ra says "return to main"
f --jal g--> $ra now says "return to f"
If f did not save its incoming $ra, it has lost the address needed to return to main. This often produces a loop around the end of f rather than a clean return.
A function which calls another function is a non-leaf function and must preserve $ra. A function which makes no calls is a leaf function; because it never executes jal, it normally does not need to save $ra.
Practice: Quick Checks#
- What two actions does
jal functionperform, and how does the callee return? - Where do the first four register-sized arguments arrive, and where is a register-sized result returned?
- Why does a leaf function normally not need to save
$ra, while a non-leaf function does?
Note
- Quick Check Answers
jalplaces the resume address in$raand jumps tofunction; the callee returns withjr $ra.- Arguments arrive in
$a0–$a3, and the result is returned in$v0. - A leaf executes no
jal, so its incoming$rais not overwritten. Every nestedjalreplaces$ra, so a non-leaf must preserve the address needed to return to its own caller.
The Stack#
The stack is a region of memory managed in last-in, first-out order. $sp points to its current top, and the stack grows towards lower addresses.

The diagram is a snapshot while h is active. As execution descends into deeper nested calls (main -> f -> g -> h), new stack frames are allocated at progressively lower memory addresses by subtracting from $sp. As each function returns (h -> g -> f -> main), its frame is deallocated by adding back to $sp, restoring the caller's stack frame.

A function may allocate stack space by subtracting from $sp. It must restore $sp to its incoming value before returning.
Saving Registers: The Explicit Way#
Suppose a function needs to preserve $ra, $s0, and $s1. It can reserve one 12-byte block:
function:
function__prologue:
addi $sp, $sp, -12
sw $s0, 0($sp)
sw $s1, 4($sp)
sw $ra, 8($sp)
function__body:
# function work
function__epilogue:
lw $s0, 0($sp)
lw $s1, 4($sp)
lw $ra, 8($sp)
addi $sp, $sp, 12
jr $raThe offsets remain stable because $sp is not changed inside the frame. Saving one word at a time is also valid, provided allocation and deallocation are exact opposites.
Concrete Stack Snapshot#
Assume $sp was 0x7FFFFF00 on entry. Subtracting 12 changes it to 0x7FFFFEF4, and the three stores create this frame:
| Address | Offset from new $sp |
Saved value |
|---|---|---|
0x7FFFFEFC |
8 | incoming $ra |
0x7FFFFEF8 |
4 | incoming $s1 |
0x7FFFFEF4 |
0 | incoming $s0 ← new $sp |
The table above is the stack snapshot: the current $sp points at saved $s0; saved $s1 and $ra sit at successively higher addresses; and the incoming $sp marks the first byte just above this frame. Reading the rows from bottom to top therefore means moving towards higher addresses.
The epilogue loads from the same addresses before adding 12. After that addition, $sp is exactly 0x7FFFFF00 again. A stack diagram is most useful when it records actual addresses, offsets and ownership rather than merely saying that values are “on the stack”.
Mipsy's Stack Helpers#
COMP1521's mipsy provides pseudo-instructions:
push $ra
pop $raConceptually:
# push $ra
addi $sp, $sp, -4
sw $ra, 0($sp)
# pop $ra
lw $ra, 0($sp)
addi $sp, $sp, 4Popping must reverse pushing. If the prologue pushes $ra, $s0, then $s1, the epilogue pops $s1, $s0, then $ra.
Warning
push, pop, begin, and end are mipsy conveniences, not portable MIPS instructions. They are appropriate for COMP1521 code but should not be expected in arbitrary MIPS toolchains.
Prologues and Epilogues#
The prologue establishes a function's stack state. The epilogue restores it and returns.
The course's full skeleton is:
function:
function__prologue:
begin
push $ra
push $s0
push $s1
function__body:
# body
function__epilogue:
pop $s1
pop $s0
pop $ra
end
jr $raOnly save registers the function actually needs. A small leaf function can be much simpler:
product:
mul $v0, $a0, $a1
jr $raThere is no virtue in adding an empty prologue to every leaf function, but there is a serious bug in omitting required preservation from a non-leaf function.
Worked Decision: Who Saves a Live Value?#
Start with the lifetime of the value: will it still be needed after a call?
int f(int x) {
int doubled = x * 2;
int y = g();
return doubled + y;
}doubled must survive the call to g. There are two valid strategies:
- keep it in a temporary register and have
f, the caller ofg, save it in its stack frame beforejal g; - keep it in an
$sregister, while ensuringfsaves the incoming value of that$sregister for its own caller and restores it before returning.
The second strategy is often clearer in course translations:
f:
f__prologue:
begin
push $ra
push $s0
f__body:
mul $s0, $a0, 2 # doubled = x * 2; must survive jal
jal g
add $v0, $s0, $v0 # return doubled + y;
f__epilogue:
pop $s0
pop $ra
end
jr $raThere are two different preservation promises here. Saving $ra protects f's route back to its caller because jal g overwrites $ra. Saving $s0 protects the value that belonged to f's caller before f reused that register. The live $s0 holds doubled during the call to g; the stack copy is the older $s0 value which must be restored on exit.
Important
- Choose the preservation
A function calculates a value in $t0, calls helper, and then needs that value again. Why is merely hoping that helper leaves $t0 unchanged invalid? Give one caller-saved and one callee-saved solution.
Note
- Reasoned answer
$t0 is caller-saved, so helper is allowed to overwrite it. The caller-saved solution is to store the value in the caller's stack frame before jal helper and load it afterwards. The callee-saved solution is to use an $s register, while saving that register's incoming value in the prologue and restoring it in the epilogue. In either case, the code obeys the convention rather than relying on the current implementation of helper.
The Frame Pointer#
$sp may move during a function—for example, when a variable-sized local array is allocated. $fp provides a fixed reference point for the current stack frame, which is useful for stable offsets and debugging stack backtraces.
Mipsy's helpers perform the bookkeeping:
beginsaves the previous$fpand establishes a new frame pointer;endrestores$spto the frame boundary and restores the previous$fp.
This is why course skeletons place begin first and end immediately before returning. Frame pointers are optional in general machine code, but the helpers make stack mistakes considerably easier to diagnose.
Note
- The important distinction
$sp identifies the current top of allocated stack space and may move. $fp identifies a stable point associated with the current frame. A function must restore both according to the calling convention.
A Non-Leaf Function#
Consider:
int sum_product(int a, int b) {
return product(6, 7) + a + b;
}a and b are needed after product returns. They cannot remain solely in $a0 and $a1, because the call is allowed to overwrite argument registers. Preserve them in $s registers:
sum_product:
sum_product__prologue:
begin
push $ra
push $s0
push $s1
sum_product__body:
move $s0, $a0 # preserve a
move $s1, $a1 # preserve b
li $a0, 6
li $a1, 7
jal product # $v0 = product(6, 7)
add $v0, $v0, $s0
add $v0, $v0, $s1 # return product(...) + a + b;
sum_product__epilogue:
pop $s1
pop $s0
pop $ra
end
jr $raThis demonstrates both sides of the convention: sum_product relies on product preserving $s0 and $s1, while also restoring the incoming values before returning to its own caller.
Note
Checkpoint
- Arguments enter through
$a0–$a3, and a register-sized result leaves through$v0. - Caller-saved values must be protected by the caller; callee-saved values must be restored by the callee.
- A non-leaf function saves
$ra, allocates its frame once, and restores the frame exactly before returning.
Multiple Calls: Preserve What the Next Call Will Clobber#
Consider a function which calls product twice:
int sum_two_products(int a, int b, int c, int d) {
return product(a, b) + product(c, d);
}The second call may overwrite $a0, $a1, $v0 and every $t register. The function must therefore preserve c, d and the first product somewhere call-safe:
sum_two_products:
sum_two_products__prologue:
begin
push $ra
push $s0
push $s1
push $s2
push $s3
push $s4
sum_two_products__body:
move $s0, $a0 # a
move $s1, $a1 # b
move $s2, $a2 # c
move $s3, $a3 # d
move $a0, $s0
move $a1, $s1
jal product
move $s4, $v0 # first product must survive next jal
move $a0, $s2
move $a1, $s3
jal product
add $v0, $s4, $v0 # return first + second
sum_two_products__epilogue:
pop $s4
pop $s3
pop $s2
pop $s1
pop $s0
pop $ra
end
jr $raImmediately before the second jal, the important live values are:
| Value still needed | Location | Why it survives product |
|---|---|---|
c |
$s2 and then $a0 |
$s2 is callee-saved |
d |
$s3 and then $a1 |
$s3 is callee-saved |
| first product | $s4 |
$v0 will hold the second result |
| return to this function's caller | saved $ra in the frame |
each jal overwrites register $ra |
This is the general multiple-call method: list what remains live across the next call, then give every such value a call-safe home.
Practice: Writing / Translation#
- Translate this leaf function:
int difference(int x, int y) { return x - y; }. - Write the prologue and epilogue for a non-leaf function which modifies
$s0and$s1. Use mipsy's helpers. - A function computes a value in
$v0and must print a newline with a syscall before returning that value. Show one correct way to preserve the result.
Note
- Writing / Translation Answers
4.
difference:
sub $v0, $a0, $a1
jr $rafunction__prologue:
begin
push $ra
push $s0
push $s1
# body
function__epilogue:
pop $s1
pop $s0
pop $ra
end
jr $ra- Move the result to a preserved register such as a correctly saved
$s0, perform the syscall—which overwrites$v0with its service number—then move$s0back into$v0before the epilogue.
Recursion#
Recursion is not a special calling mechanism. A recursive function simply calls the same function label with jal. The stack allows every active invocation to retain its own return address and saved values.
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}factorial:
factorial__prologue:
begin
push $ra
push $s0
factorial__body:
move $s0, $a0 # preserve n
ble $s0, 1, factorial__base
addi $a0, $s0, -1
jal factorial
mul $v0, $s0, $v0
b factorial__epilogue
factorial__base:
li $v0, 1
factorial__epilogue:
pop $s0
pop $ra
end
jr $raEach call has its own saved $s0 and $ra. When the base case returns, frames unwind in reverse order.
Snapshot of factorial(4)#
At the deepest point, just before the base case returns, four invocations are active:
| Frame, high to low address | Saved $s0 |
Saved $ra returns to |
|---|---|---|
factorial(4) |
4 | its original caller |
factorial(3) |
3 | multiply inside factorial(4) |
factorial(2) |
2 | multiply inside factorial(3) |
factorial(1) |
1 | multiply inside factorial(2) |
The base frame sets $v0 = 1. Unwinding then produces:
factorial(1) returns 1
factorial(2) restores n = 2, returns 2 * 1 = 2
factorial(3) restores n = 3, returns 3 * 2 = 6
factorial(4) restores n = 4, returns 4 * 6 = 24
Every frame runs the same instructions, but each owns a different saved n and return address. That per-call ownership is the entire reason recursion works.
Because stack capacity is finite, unbounded recursion eventually causes a stack overflow. The underlying problem is not that recursion is inherently invalid; it is that active calls continue allocating frames without reaching a base case and returning.
Practice: Tracing / Calculation#
- A caller stores 19 only in
$t0, callsmystery, then uses$t0. What assumption is wrong, and what are two valid repairs? - A function enters with
$sp = 0x7FFFFF00, subtracts 16 and saves$s0,$s1,$s2,$raat offsets 0, 4, 8, 12. Give the four addresses and the final value of$spafter a correct epilogue. - During
factorial(4), what values are saved in the four active copies of$s0, and in what order are they used while returning?
Note
- Tracing / Calculation Answers
7. $t0 is caller-saved and may be clobbered. The caller can save it to its stack frame before jal, or keep the live value in an $s register whose incoming value is correctly preserved.
8. The new $sp is 0x7FFFFEF0. The saves are at 0x7FFFFEF0, 0x7FFFFEF4, 0x7FFFFEF8 and 0x7FFFFEFC. After restoration and adding 16, $sp is again 0x7FFFFF00.
9. The frames save 4, 3, 2 and 1 from oldest to newest. They are restored in reverse call order: 1 returns the base result, then 2, 3 and 4 multiply while unwinding.
Practice: Challenge#
- Sketch the frame contents and control flow for a recursive
sum_to(n)which returns 0 forn <= 0and otherwise returnsn + sum_to(n - 1). Which two values must each non-base invocation preserve?
Note
- Challenge Answer
Each invocation preserves its incoming n and its incoming $ra, commonly in a saved $s0 plus the stack frame. The recursive branch passes n - 1, calls sum_to, restores its own n, adds it to the returned $v0, then follows the shared epilogue. For sum_to(3), active frames save 3, 2, 1 and 0; unwinding returns 0, 1, 3 and finally 6.
Local Arrays on the Stack#
A local array cannot fit in one register. A fixed ten-element integer array requires 40 bytes:
addi $sp, $sp, -40 # int squares[10];
# use $sp as the base address while this allocation remains fixed
...
addi $sp, $sp, 40 # release the arrayFor element squares[i], calculate i * 4 and add it to the array base exactly as in MIPS Data and Memory. If the function already has saved registers or changes $sp again, plan the frame carefully or use $fp as the stable reference.
Worked Example: A Complete Fixed Local Array#
This leaf function copies its four arguments into a local array, then returns their sum:
int sum_four(int a, int b, int c, int d) {
int values[4] = {a, b, c, d};
return values[0] + values[1] + values[2] + values[3];
}sum_four:
addi $sp, $sp, -16 # allocate int values[4]
sw $a0, 0($sp) # values[0] = a;
sw $a1, 4($sp) # values[1] = b;
sw $a2, 8($sp) # values[2] = c;
sw $a3, 12($sp) # values[3] = d;
lw $t0, 0($sp)
lw $t1, 4($sp)
add $v0, $t0, $t1
lw $t0, 8($sp)
add $v0, $v0, $t0
lw $t0, 12($sp)
add $v0, $v0, $t0
addi $sp, $sp, 16 # release exactly this frame
jr $raBecause the array base remains equal to $sp throughout the body, its four elements stay at offsets 0, 4, 8, and 12. The function is a leaf, so it does not overwrite $ra; its only preservation duty here is returning $sp to its incoming value. If a later version called another function, it would also need a home for the incoming $ra and any live temporary values.
Practice: Local Stack Storage#
- Suppose
sum_fourenters with$sp = 0x7FFFFF00. Give the new$sp, the address of every array element, and the value of$spimmediately beforejr $ra.
Note
- Local Stack Storage Answer
Subtracting 16 gives 0x7FFFFEF0. The four elements begin at 0x7FFFFEF0, 0x7FFFFEF4, 0x7FFFFEF8, and 0x7FFFFEFC. Adding 16 in the epilogue restores $sp to 0x7FFFFF00 before the return.
Function Translation Checklist#
Before writing the body, answer:
- Which arguments arrive in which
$aregisters? - What must be returned in
$v0? - Is this a leaf or non-leaf function?
- Which values must survive a
jal? - Which
$sregisters will be modified? - Does
$raneed saving? - Does the function allocate stack storage?
- Does the epilogue exactly reverse the prologue?
Then annotate the function:
# Arguments:
# - $a0: int n
# Returns:
# - $v0: result
# Locals:
# - $s0: n, preserved across recursive call