MOA • #21 • 24-3-1998

Procedure Calls in MIPS

The MIPS CPU contains 32 general purpose 32-bit registers: $0 - $31. Register $0 contains the hardwired value 0.

The recommended (i.e., not enforced by hardware) usage of these registers is as given in Table 2 of the document SPIM S20: A MIPS R2000 Simulator by J. Larus.

Apart from its generic $n name, a register has also a more descriptive name according to its intended usage. The following ones are useful in procedure calls.

Like what we have described before using a hypothetical procedure call model, a stack frame is created when a procedure (caller) calls another one (callee).

The stack frame contains data that is partly the caller's (return address, saved registers) and partly the callee's (passed arguments, local variables). But in general we call this stack frame the callee's stack frame.

The following figure shows the layout of a stack frame. The frame pointer ($fp) points at the first word in the frame, and the stack pointer ($sp) points at the first free word above the frame.

The first four arguments are passed in registers ($a0-$a3), and hence the first word of the stack frame stores the fifth argument if there is one.

To load the fifth argument into a register, the callee procedure can issue an instruction such as

lw $t0, 0($fp)
[This seems to violate the typical use of a stack (i.e., accessing only the top element). Well, the stack is used here mainly to maintain the LIFO behavior of procedure calls.]

Let's see what the caller has to do, and then the callee, in a procedure call.

The caller:

  1. Pass arguments. The first four using registers $a0 - $a3; the remaining pushed on the stack.
  2. Save those caller-saved registers that need to be saved on the stack.
  3. Execute a jal instruction which saves the return address in $ra (which will be saved by the callee on the stack if the callee will call another procedure -- i.e., a "non-leaf" procedure) and jumps to the procedure.

The callee:

  1. Allocate memory for the frame.
  2. Save callee-saved registers and optionally $fp and $ra in the frame. ($fp and $ra are saved if the callee is a non-leaf procedure).
  3. Establish the frame pointer so that it points at the first word of the frame.
  4. Start running.
  5. On return:
    1. If this is a function that has a value to return, place the return value in register $v0.
    2. Restore all callee-saved registers.
    3. Pop the stack frame by subtracting the frame size from $sp.
    4. Return by jumping to the address in register $ra.

Here is an example of procedure call: a program to calculate the factorial of the integer 10. The C++ version looks like the following.

#include <iostream.h>

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

void main()
{
  cout << "The factorial of 10 is " << fact(10) << endl;
}
Here is the assembly language program (runnable in SPIM):
      .globl main

      .data
Msg:  .ascii "the factorial of 10 is "

      .text

#################### Main program ####################

main: li    $a0, 10          # Put argument (10) in $a0
      jal   Fact             # The call - Fact(10)
      move  $t0, $v0         # Save result in $a1

      li    $v0, 4           # Print Msg
      la    $a0, Msg
      syscall
      li    $v0, 1           # Print result
      move  $a0, $t0
      syscall
      li    $v0, 10          # exit
      syscall

#################### Function Fact(n) ####################

Fact: subu  $sp, $sp, 32     # Create 32-byte stack frame
      sw    $ra, 20($sp)     # Save return address
      sw    $fp, 16($sp)     # Save frame pointer
      addu  $fp, $sp, 32     # Set up frame pointer
      sw    $a0, 0($fp)      # Save argument (n)
      bgtz  $a0, More        # Branch if n > 0
      li    $v0, 1           # else return 1
      j     Ret              # Jump to return code

More: subu  $a0, $a0, 1      # Compute n-1
      jal   Fact             # Fact(n-1)
      lw    $v1, 0($fp)      # Retrieve saved n
      mul   $v0, $v0, $v1    # Fact(n-1) * n

Ret:  lw    $ra, 20($sp)     # Restore $ra
      lw    $fp, 16($sp)     # Restore $fp
      addu  $sp, $sp, 32     # Pop stack
      j     $ra              # Return to caller
Some notes are in order: The call stack after Fact(10) has called Fact(9):