Functions

Functions are a way to write a self-contained, modular sections of code that can be reused anywhere, without requiring any context of what’s happening outside of the function. (for example, not needing to know what registers are already being outside of the function). This of course can be extremely useful.

In higher level languages, we can write code in a function like normal, and the language handles the magic of making that code work in a modular way.

However, in assembly, there are no such safeguards. There’s technically nothing stopping a function from corrupting every single register, and/or the entire stack, thus breaking the program. So… we create functions by following a strict set of conventions, that every MIPS programmer agrees to adhere to.

Using The Stack

It’s very useful to be able to temporarily store things in memory, when we need to use registers for other things, but we need to make sure that functions don’t overwrite each other’s data. To solve this we have the stack. This is memory for functions to use, which follows FIFO.

At the start of a function, it will allocate itself some room at the “top” of the stack (though, in this case the stack builds downwards, so it’s actually like the bottom of the stack). And then at the end of the function, that memory is deallocated, aka removed from the stack. That is what the standard function prologue and epilogue are for.

By convention, the $sp register (”stack pointer”) always points to the bottom of the stack. So we always know where the next free memory is (right below it).

The $fp register (”frame pointer”) points to the start of the stack frame for the current function. the stack frame is like the section on the stack that belongs to a specific function.

# Standard Function Prologue

# Allocate 6 words of space on the stack, by decrementing $sp -24 bytes
# note that addiu just means "addi unsigned". it works basically the same as addi.
addiu  $sp, $sp, -24  

# we don't know where $fp is now, and need to restore it after the function is done, 
# so, we save it to one of the newly allocated spots on the stack
sw     $fp, 0($sp)

# $ra (return address) contains the address of the code we need to jump to after the function is done.
# we save it to the stack as well incase we need to call another function inside this function.
sw     $ra, 4($sp)

# update $fp to the current start of the stack frame (the first slot allocated to this function)
addiu  $fp, $sp, 20

# load the value of $ra back from where we stored it on the stack
lw     $ra, 4($sp)

# load the value of $fp back from where we stored it on the stack
lw     $fp, 0($sp)

# deallocate the stack space by incrementing $sp
addiu  $sp, $sp, 24

# jump back to where function was called from
jr     $ra

Using Registers

Another important convention system is the way we use registers.

There are a limited number of them, and inevitably functions are going to use the same ones. One solution would be to save every single register to memory at the start, and restore them at the end. But this would be incredibly inefficient. So we do something a bit different.

$tx Registers

functions are allowed to use these whenever they want. But this means that if you want to call a function, you CANNOT assume that any of your $tx registers will be the same value after the function has returned. So, if you have a value in a $tx register that you want to keep, you have to allocate an extra slot on the stack, save your $tx register value there, call the function, and then load the value and deallocate the space again after the function has returned. This is annoying, but by following it, we give functions the freedom to use those registers however it wants, which makes it less of a hassle to code them.

# say we have a function foo() that calls another function bar()
foo:
    # standard prologue
    addiu $sp, $sp, -24
    sw    $fp, 0($sp)
    sw    $ra, 4($sp)
    addiu $fp, $sp, 20

    # this register value is important for some reason and we want to keep it
    addi  $t0, $zero, 111
    
    # allocate a new slot by decrementing $sp -4 bytes (1 words)
    addiu $sp, $sp, -4

    # store the values in these new slots
    sw    $t0, 0($sp)

    # call function bar()
    jal   bar

    #restore the value we stored previously
    lw    $t0, 0($sp)

    # deallocate the extra slot
    addiu $sp, $sp, 4

    # standard epilogue
    lw    $ra, 4($sp)
    lw    $fp, 0($sp)
    addiu $sp, $sp, 24
    jr    $ra

$sx Registers

These basically work in the opposite way. functions are NOT allowed to corrupt the values in these registers. So if they want to use an $sx register, the function using it needs to store the value to the stack, and restore it before returning. This way, the function that calls another function MAY assume that any $sx registers it was using before the function call are still the same.

# say we are in the function bar(), that has just been called by the function foo()
bar:
    # standard prologue
    addiu $sp, $sp, -24
    sw    $fp, 0($sp)
    sw    $ra, 4($sp)
    addiu $fp, $sp, 20

	  # bar wants to use the $s0 register, but it isn't allowed to freely "corrupt" it,
	  # so, it needs to save $s0 to the stack and restore it later.

    addiu $sp, $sp, -4    # allocate a new slot by decrementing $sp -4 bytes (1 words)
    sw    $s0, 0($sp)     # store the value in the new slots
    
    addi  $s0, $zero, 123 # now, it can freely overwrite $s0
    
    
    lw    $s0, 0($sp)     # before the epilogue, restore $s0 to what it was originally.
    addiu $sp, $sp, 4     # deallocate the extra slot

    # standard epilogue
    lw    $ra, 4($sp)
    lw    $fp, 0($sp)
    addiu $sp, $sp, 24
    jr    $ra

Setting Arguments In Functions