COMP1521 956 words·5 min read

05. MIPS Functions

MIPS Functions#

Functions in MIPS#

A C function call hides several coordinated actions:

  1. evaluate the arguments;
  2. transfer control to the function;
  3. create the function's local state;
  4. perform its body;
  5. produce a return value;
  6. 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.

MIPS
	jal	product			# call product
	# execution resumes here after product returns

The function returns with:

MIPS
	jr	$ra

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.

C
int product(int x, int y) {
	return x * y;
}

int result = product(6, 7);
MIPS
	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	$ra

The 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.

MIPS
	li	$t0, 10
	jal	mystery
	# $t0 cannot be assumed to still contain 10

Treat 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:

MIPS
	move	$s0, $a0		# preserve original argument
	jal	other_function
	# $s0 is still available here

However, 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:

CODE
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.

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:

MIPS
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	$ra

The 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:

MIPS
	push	$ra
	pop	$ra

Conceptually:

MIPS
	# push $ra
	addi	$sp, $sp, -4
	sw	$ra, 0($sp)

	# pop $ra
	lw	$ra, 0($sp)
	addi	$sp, $sp, 4

Popping 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:

MIPS
function:
function__prologue:
	begin
	push	$ra
	push	$s0
	push	$s1

function__body:
	# body

function__epilogue:
	pop	$s1
	pop	$s0
	pop	$ra
	end
	jr	$ra

Only save registers the function actually needs. A small leaf function can be much simpler:

MIPS
product:
	mul	$v0, $a0, $a1
	jr	$ra

There 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:

  • begin saves the previous $fp and establishes a new frame pointer;
  • end restores $sp to 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:

C
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:

MIPS
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	$ra

This 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.

C
int factorial(int n) {
	if (n <= 1) return 1;
	return n * factorial(n - 1);
}
MIPS
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	$ra

Each 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:

MIPS
	addi	$sp, $sp, -40		# int squares[10];
	# use $sp as the base address while this allocation remains fixed
	...
	addi	$sp, $sp, 40		# release the array

For 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:

  1. Which arguments arrive in which $a registers?
  2. What must be returned in $v0?
  3. Is this a leaf or non-leaf function?
  4. Which values must survive a jal?
  5. Which $s registers will be modified?
  6. Does $ra need saving?
  7. Does the function allocate stack storage?
  8. Does the epilogue exactly reverse the prologue?

Then annotate the function:

MIPS
# Arguments:
# - $a0: int n
# Returns:
# - $v0: result
# Locals:
# - $s0: n, preserved across recursive call

Common Mistakes#

  • Calling a function with b and expecting $ra to be set.
  • Executing jal in a non-leaf function without saving the incoming $ra.
  • Assuming $t, $a, or $v0 survives a call.
  • Modifying an $s register without saving and restoring its incoming value.
  • Restoring registers in the wrong order after push.
  • Returning along one branch which bypasses the epilogue.
  • Leaving $sp changed when the function returns.
  • Performing a syscall after computing a return value in $v0 without preserving that return value.
  • Forgetting that recursive calls require the same preservation rules as any other call.

Sources and Further Reading#