sections in this module City College of San Francisco - CS270
Computer Architecture

Module: MIPS-III (Procedures)
module list

Introduction to Procedures

Note: the term function and procedure are used interchangeably in this discussion. The traditional distinction is that a procedure doesnt return a value while a function does. We will view the terms as synonymous.

In this section we will start to discuss procedures. We begin with the simplest case - a leaf function. A leaf function is a function that does not call anyone else - it simply does its work and returns. If the leaf function is simple, it does not need to use the stack. It often does not need to touch the stack at all.

Returning from a function

The simplest thing to discuss about a function is how to return. When a function is called, the address of the instruction following the call is automatically placed in a special register - the return result register, $ra.

We have been using $ra to return from main() recently, and this is no different - just end the function with the return statement

jr $ra

Returning a function result

If your function returns a result, the result is placed in another special register - the return result register, $v0. Simply set $v0 to the function result and return. There are actually two function result registers, $v0 and $v1, but $v1 is seldom used.

Function arguments

On most machines, when a function is called, the arguments to the function are placed on the stack by the calling function. (Remember, we will refer to the calling function as the caller.) After the function call, the receiving function (the callee) takes the arguments off the stack as they are needed. Let's look at what this means for a leaf function that takes four arguments:

int foo (int a, int b, int c, int d)

higher address









lower address
other data for the caller






<----- $sp at entry to foo
fourth argument (d)  12($sp)
third argument (c) 8($sp)
second argument (b) 4($sp)
first argument (a) 0($sp)

Thus, at entry to foo, foo's first argument (a) would be located at 0($sp), and it's second argument at 4($sp). On most machines, then, foo can extract the arguments from the stack using their offsets whenever it likes. The stack location for the argument is called its home location. In fact, if foo wants to store a modified value for b on the stack later, it can use the home location allocated for it by its caller (4($sp)).

MIPS keeps this general framework. There is always space allocated for arguments on the stack when a function is called. However, MIPS does an optimization here. Since most functions use all of their arguments, it is redundant for the caller to place them on the stack and then have the callee take them off again. So MIPS reserves four registers for the first four arguments and leaves the arguments in those registers! Thus, foo can use the pre-loaded register $a0 for the first argument (a), up to register $a3 for the fourth argument (d). These values are not placed on the stack by the caller, but, since there is a home location allocated for them, foo (the callee) can store them if it needs to.

Note that this creates a bit of a quandry - not all functions use four arguments: some use more and many use less. The convention on MIPS is this - the $a0-$a3 registers are always reserved for the first four arguments, and space is reserved to home them on the stack. Later arguments (starting with the fifth one) are placed on the stack in the correct relative position just like most architectures.

A first example

Let's look at an example of a simple function foo:

int foo(int a, int b, int c, int d, int e) {
a += b;
a += c;
a += d;
a += e;
blahblah;
return (a);
}

Here we will pretend that blahblah is something that requires us to save argument a in its home location (for practice). Here is how the code would look:

# int foo(int a, int b, int c, int d, int e) {
foo:
# a += b;
add  $a0,$a0,$a1
# a += c;
add  $a0,$a0,$a2
# a += d;
add  $a0,$a0,$a3
# a += e;
lw  $t0,16($sp)
add  $a0,$a0,$t0
# home a
sw   $a0,0($sp)
# blahblah;
# return (a);
# since we homed it we reload it
lw   $v0,0($sp)
jr   $ra
}

As you see, the first four arguments were in registers (and not on the stack - although space was allocated for them). The fifth (and later) arguments were on the stack, and we had to load them.

On other machines, only the amount of space required for arguments is allocated on the stack when functions are called. On MIPS, space for a minimum of four arguments is allocated on the stack whenever a function is called. (Later, we shall see that this actually simplifies our work.)

A second example

We have seen how to use arrays that are global. What if an array is passed as an argument. This is actually very simple - when an array is passed as an argument, the address of its first argument is passed. Thus, there is no difference between these two functions

int sumarr(int arr[], int nelems);

int sumarr(int *iptr, int nelems);

In both cases, the first argument is a pointer to the first array element. Let's go ahead and code one of these functions to sum an array, as its name implies:

int sumarr(int arr[], int nelems) {
int sum = 0;
int i;

for (i=0;i<nelems;i++ ) {
sum += arr[i];
}
return (sum);
}

First, convert it. Here we have adopted the rule of prefixing our local labels with L and adopting a standard naming convention (although our label names get long):

int sumarr(int arr[], int nelems) {
int sum = 0;
int i = 0;
Lsumarrloop: if (i >= nelems) goto Lsumarrloopend;
sum += arr[i];
i++;
goto Lsumarrloop;
Lsumarrloopend:
 
return (sum);
}

Now lets add the MIPS code:

#int sumarr(int arr[], int nelems) {
    .globl sumarr
sumarr:
#int sum = 0;
    li   $v0,0
#int i = 0;
    li   $t0,0
#Lsumarrloop: if (i >= nelems) goto Lsumarrloopend;
Lsumarrloop:
    bge  $t0,$a1,Lsumarrloopend
#sum += arr[i];
    sll   $t1,$t0,2
    add   $t2,$a0,$t1
    lw    $t3,0($t2)
    add   $v0,$v0,$t3
#i++;
    add   $t0,$t0,1
#goto Lsumarrloop;
    b     Lsumarrloop
#Lsumarrloopend:

Lsumarrloopend:

#return (sum);
    jr    $ra
}

As you can see, the register $a0 was already initialized with the address of the start of the array - we just used it in each iteration. Note that this code could be optimized, but the indexing operation was left in for clarity.

Use of registers

Registers are global - there is only one set of registers. If you have a register in-use when you call a function, and that function uses that register, it overwrites your value.

Part of the procedure calling convention tells us what to do about preserving our registers. We will see that in the next section. Here we just introduce the simplest rule:

Whatever function is executing owns these registers: the a registers, the t registers, the v registers, and $ra. This means when you call a function, you must assume that all of those registers has been destroyed. It also means our leaf function could freely use those registers, as you saw in the example above.

In the next section we will discuss the general situation - when a function calls another. If that happened with our function foo, foo would have to allocate room on the stack (create a stack frame) to hold data which could be destroyed by the function it calls (it's return address, for example). We will see how that works in the next module.  Note that our function main(), which calls foo() is no longer a leaf function. It now needs a stack frame, as we will discuss in the next section.

Character strings in functions

In a previous section we discussed how an assignment of a character string (a C-string) to a character pointer results in an initialization of the string and the assignment of the address of the string to the pointer variable. In that example, the character pointer was global.

Globals, of course, are rarely used in modern programming languages. Instead you would have a local variable that is of type char * that is initialized to a string such as at the beginning of this function:

void foo (void) {
char * message = "hello there";

The only difference here is that there is not a global variable to hold the pointer. Instead, the local variable is initialized at runtime to the address of the string, which would be generated on MIPS using a .asciiz directive as in the previous example.

Character buffers in functions

Until now, we have been using global static character buffers to hold user input. It is a very bad idea (and is disallowed in most programming languages of today) to allow a function to modify global data. Once we start using the stack for local variables in the next module, we will allocate room for character buffers there instead. You will begin to see this in the sample files for the exercise sets and we will discuss it further in the next section. After that it will be class policy to not allow any function to modify global data.

Prev This page was made entirely with free software on linux:  
Kompozer, the Mozilla Project
and Openoffice.org    
Next

Copyright 2015 Greg Boyd - All Rights Reserved.