Part 2 - MIPS

Posted on Mar 22, 2023
(Last updated: May 26, 2024)

In this part we’ll properly cover the MIPS architecture in depth.

Parameter transferring

When calling a function in C code, what is happening at the assembler level is, calling a subroutine (usually).

What we need to do before a subroutine call, is the following:

  1. Place the input parameters into their respective registers
  2. Call the function/subroutine
  3. Allocate memory for eventual saved and local variables
  4. Execute the function/procedure
  5. Place the result in a register
  6. Reset the stack, if needed
  7. Return to where the function/subroutine got called.

Register conventions

Since we now need to store our input parameters into register before we call a function, we need to create a convention to where we expect things to be.

MIPS has this following convention:

  • $a_0 - a_3$: Input parameters

  • $v_0, v_1$: Result values

  • $t_0 - t_9$: Temporary variables

  • $s_0 - s_7$: “Saved” registers

    • Must be saved and reset by each respective subroutine.
  • $gp$: global pointer

  • $sp$: stack pointer

  • $fp$: frame pointer

  • $ra$: return address

We discussed a lot of subroutines, but how do we actually call one?

jal Label will jump to a label (starting point of a subroutine).

To return from a subroutine call jr $ra.

A very simple function like:

int example(int g, int h, int i, int j) {
    int f;
    f = (g + h) - (i + j);
    return f;
}

Given that the arguments are in $a_0, \ldots, a_3$, f is in $s_0$ (therefore we must save $s_0$ to the stack before!).

The result will be stored in $v_0$

MIPS Code:

example:
    addi $sp, $sp, -4   # Allocate space for one variable, s_0
    sw   $s0, 0($sp)    # Save s_0

    add  $t0, $a0, $a1  # (g + h)
    add  $t1, $a2, $a3  # (i + j)
    sub  $s0, $t0, $t1  # f = (g + h) - (i + j)a

    add  $v0, $s0, $zero # Store value

    lw   $s0, 0($sp) # Reset s_0 value
    addi $sp, $sp, 4 # Reset stack pointer

    jr   $ra         # Return

Let’s now take a recursive

int fact(int n) {
    if(n < 1) {
        return 1;
    } else {
        return n * fact(n - 1);
    }
}

input parameter n in $a_0$, result in $v_0$

MIPS:

fact:
    addi $sp, $sp, -8   # Adjust stack so we can store 2 variables
    sw   $ra, 4($sp)    # Save return address
    sw   $a0, 0($sp)    # Save input register

    slti $t0, $a0, 1    # test for n < 1
    beq  $t0, $zero, L1
    addi $v0, $zero, 1  # If n < 1, result = 1

    addi $sp, $sp, 8    # Pop stack
    jr   $ra            # Return

L1: addi $a0, $a0, -1   # Decrement n
    jal  fact           # Recursive call

    lw   $a0, 0($sp)    # restore orignal n
    lw   $ra  4($sp)    # and return address

    addi $sp, $sp, 8    # Pop stack
    mul  $v0, $a0, $v0  # n * fact(n - 1)
    jr   $ra            # return

Less than a word

Sometimes we want to only access a byte or half-word:

lb rt, offset(rs)
lh rt, offset(rs)

# Unsigned
lbu rt, offset(rs)
lhu rt, offset(rs)

# Save
sb rt, offset(rs)
sh rt, offset(rs)

Indexing vs Pointers

  • Vector indexing requires:

    • Multiplication of the index with length

    • Summation of this product to the vectors start address

  • Pointer corresponds directly to a memory address

    • Therefore, it’s faster

Common mistakes

Common mistakes when writing programs in MIPS are:

  • Incrementing with 1 and not 4!

  • MIPS uses byte addressing