05. MIPS Functions
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.
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.
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.
flowchart TD
HIGH["Higher addresses"]
MAIN["main frame"]
F["f frame"]
G["g frame"]
H["h frame<br/>$sp points here"]
LOW["Lower addresses"]
HIGH --> MAIN
MAIN -->|"main calls f"| F
F -->|"f calls g"| G
G -->|"g calls h"| H
H --> LOW
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.
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.
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.
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.
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.
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.
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 callCommon Mistakes#
- Calling a function with
band expecting$rato be set. - Executing
jalin a non-leaf function without saving the incoming$ra. - Assuming
$t,$a, or$v0survives a call. - Modifying an
$sregister without saving and restoring its incoming value. - Restoring registers in the wrong order after
push. - Returning along one branch which bypasses the epilogue.
- Leaving
$spchanged when the function returns. - Performing a syscall after computing a return value in
$v0without preserving that return value. - Forgetting that recursive calls require the same preservation rules as any other call.