System Architecture

(INFT12-212 and 72-212)
Lab Notes for Week 4: Calling Functions, The Stack, Complex Data Structures

1  Introduction

This week we are looking at the mechanism used on the MIPS CPU to call functions, so that programs can be modularised as we can in high-level languages like C and Java. To understand how we do this, we also need to understand the stack frame which MIPS uses. Later on, we will look at recursion in assembly.

2  Instructions for Calling and Returning

MIPS provides two instructions for calling functions and returning from functions:
  1. jal label: Jump to the instruction at the named label, storing the address of the instruction following the jal in the $ra (return address) register.
  2. jr $ra: Jump back to the instruction at the address stored in the $ra (return address) register.
These are the only instructions we need to do function calls; however, things are not as simple as they seem.

2.1  Task 1

Download the program wk4jal_jr_instructions.asm and load it into MARS. This program starts in the main() program, which prints out one string. main() then calls the function() using the jal instruction, which prints out the second string. Once the function() returns using the jr instruction, the main() program prints out the first string again.
Assemble the program, and slowly single-step the program. Watch the first string get printed out with $a0 pointing at the first string. When the jal instruction is performed, notice the program jump to the function(), and also notice that $ra is set pointing at the instruction after the jal. Watch the second string bring printed. When the jr instruction is performed, see the program jump back to the main() program.
Question: why did the main() program print out the second string and not the first string again? After all, the only value that the main() program puts into $a0 is the address of string1.


The answer is that the function() overwrote the value in $a0 when it printed out the second string, so when we returned back to main(), what main() thinks is in the $a0 register is no longer there. The function() has tromped all over main()'s register value!

3  Functions: What We Need

Some of the following is abstracted from Appendix A of Patterson and Hennessy's book.
You can see that, if we are going to call and return from functions properly, we need to do some things other than jal & jr to get it to work. Here are the things we need for functions to work:
  1. The function caller needs to be able to provide a set of arguments, in a specific order, to the function. The function needs to be able to see these as local variables.
  2. The function needs to be able to create its own local variables which are different to those in the functions which called it.
  3. The function caller and the function itself are going to need to agree on how to store away register values, so that each will have access to the CPU's registers without having to worry about losing values.
  4. The function needs to be able to return a result to the function caller.
  5. We also want recursion: the ability to write functions which call themselves an arbitrary number of times.
In the following discussion, the function being called is going to be known as the callee.

3.1  Registers for Arguments and Result

On a CPU, there are two places which we could use to send in arguments to a function, and get back the result: registers or main memory. With the MIPS CPU, the registers $a0 to $a3 are used to send in up to 4 arguments to a function: if the function needs more, then the caller and callee must be written to store and collect the remaining arguments in main memory. The registers $v0 and $v1 are used to return a single result to the caller: both are used in combination if the result is a doubleword.

3.2  Who Deals Which Registers?

Of course, there are only 32 registers on the MIPS CPU, and both the function caller and the function will need to use some of them. Somebody is going to need to do some work in order to save the function caller's existing values, so that the function itself has some registers to use.
Here is the register saving convention used by all software written for the MIPS:

3.3  The Stack

So, the function caller stashes some registers away before it makes the function call. Likewise, the function callee stashes some registers away when it gets called. On the way back out, both the callee and caller restore the register values, so that when the function is done the caller has its registers back the way they were (except for the result of the function).
This is all fine and good if we only call one function at a time: we can choose a fixed set of memory locations and store the register values into these locations, then fetch them back later. But what about functions that call functions, and functions that perform recursion an arbitrary number of times? One fixed set of locations is not going to be enough.


This issue is solved using a stack. A stack is an area of memory of (essentially) arbitrary size which we can use to store as many register values as we like, as well as other data. We can push values onto the stack, which grows in size, and later we can pop values back off the stack in reverse order.
Figs/stack.png
Stack (from Wikipedia)
Conventionally, memory on many computers is laid out so that the code of a program is stored in low memory, followed by any global data which has been allocated to the program. The stack is created at the high-end of memory and as it grows, it grows downwards in memory locations. In other words, computer stacks are upside down: a push makes it grow at the bottom, not the top!
Figs/mem_layout.gif
This division of a program's memory into three distinct areas is not the only way to do things, but it does allow both the global data and the stack (where local data is stored) to expand with minimum risk of running into each other. Later, we will see how we can efficiently manage the memory allocated to all three.
Note the existence of the stack pointer, which points at the item most recently pushed onto the stack, and the item which is the first to be pulled. The MIPS register $sp is used as the stack pointer. The memory at addresses below the stack pointer is not yet allocated, but will become used once new data items are pushed onto the stack. When a program begins execution, the operating system will initialise the stack pointer to the highest address which is part of the stack.

3.4  Pushing and Popping

Pushing a data item onto the stack is easy: decrement $sp by the size of the item, and store the item on the stack using $sp indirect addressing. For example, to push the value in $t1 onto the stack:
        subu $sp, $sp, 4     # Decrement $sp by 4
        sw $t1, ($sp)        # and copy $t1 onto the stack

Popping a data item is the reverse. Let's pop the top word off the stack and bring it back into the $t3 register:
        lw $t3, ($sp)
        addu $sp, $sp, 4

Note: memory addresses start at 0 and go higher: there are no negative addresses. This explains why the addu and subu instructions are used.

3.5  Stack Frames

Even though I have shown you how to push and pop individual data items, in fact we will be growing and shrinking the stack in whole groups of data items. We know that each function has to keep track of: To do this, each function builds a stack frame on the stack which records all of these details. The stack frame has a generic format, although each frame's size will vary depending on how many arguments, saved registers and local variables are stored on it.
The simplest and smallest stack frame is shown in the diagram below; it is 24 bytes in size.
Figs/simplest_stack_frame.gif
The first thing we (as a function) must do when it is called is to move the stack pointer $sp down to make room on the stack for the frame. The next critical thing we need to do, then, is to save the return address currently in $ra. Why? Because if we ourselves now call another function, the return address that we need will be lost!
At the top of each function, therefore, should be these instructions or similar:
    function:  subu $sp, $sp, 24      # Make a stack frame of 24 bytes
               sw $ra, 20($sp)        # Save our $ra before we call another function
                                      # Note: we treat the stack frame like an array

And the last instructions in a function should undo the above two instructions:
               lw $ra, 20(sp)         # Reload our return address
               addu $sp, $sp, 24      # Destroy our stack frame
               jr $ra                 # and return out of this function

3.6  What About the Rest of the Stack Frame?

Good question! The minimal stack frame provides room for us, the recently-called function, to stash away copies of our arguments in $a0 to $a3, before we ourselves call a function. However, saving these registers is not mandatory: we only need to do it if we are calling another function.
Similarly, there is room to save our $fp register before we call a function which might destroy it.
Big note: in this subject we won't be using the $fp register at all, so there is no need for any function to save a copy of it on the stack frame.
Now, the stack frame shown above is simply the minimal, smallest stack frame. If we need to save other registers on the stack, then we will create a larger stack frame, and we get to choose where to put the extra values on the stack frame.
I've tried to find a definitive guide for what an extended MIPS stack frame should look like, but there does not seem to be any general agreement. So I recommend the following stack frame structure for this subject:
Figs/extended_stack_frame.gif
Obviously, you need to design your stack frame to hold those values that will get overwritten, but you don't need to make the stack frame any bigger than that.

3.7  Task 2

Download the program wk4stackframe1.asm and load it into MARS. This program does the same job as Task 1, but this time both the main() program and the function() create stack frames for themselves. This allows main() to keep the value in $a0 across the call to the function().
Run, and single-step, the program and make sure that you understand how the stack frame is working.

4  Local Variables

Functions also want to keep variables which are local, i.e. not globally visible and only within the scope of the function while it is running. If we used .data to store these variables, then the assembler would allocate space for them in the global data area, and everybody would be able to see them.
Instead, we can use the stack frame to store local variables. We need to:
  1. increase the size of the stack frame to provide enough space to hold the local variables
  2. decide where in this extended stack frame to place our local variables.

4.1  Task 3

Download the program wk4mostfreqchar.asm and load it into MARS. This program stores an array of characters on the stack frame as a local variable. In the C language, we would write this as the function definition:
    char mostfreqchar(void)
    {
      char line[200];    # Local array of 200 characters
       . . .
    }

In MIPS assembly, we need to allocate 200 more bytes on the stack frame, i.e. make it 224 bytes in length. We also need to decide where to store the line array. The simplest choice is to store it at offset 24 in the frame (i.e. just above the old $ra value), and extending for 200 bytes in length.
Read through the program and do these things:
  1. Understand the purpose of the function, what inputs it receives (none), what result it returns.
  2. Understand the high-level algorithm of the function. Note that it's often easier to use pointers into the array's contents rather than thinking in the array[index] mindset.
  3. See how the stack frame is larger than the minimal stack frame, and that we take a pointer to the base of the character array, by doing 24($sp). That's because the local variable is at a fixed offset from the base of the stack frame.
  4. Follow the assembly translation of the high-level algorithm. Writing this wasn't easy, mainly because you have to remember which register holds which variable. It's a good idea to scratch down on paper a table of register/variable equivalence.
  5. Note that I chose to use registers which are caller-saved registers, so that I didn't have to save any registers in the function except for the $ra. This includes the argument registers $a0 to $a3: they were free as I didn't get any arguments.
  6. Finally note that there is a fair bit of move $reg, $reg going on. That usually indicates that the allocation of registers could be improved, which would save having to copy values around here and there.

5  Recursion

We can now finally tackle recursion. For recursion to work, a function has to be able to call itself, and it also has to be able to keep its own local variables which will not be clobbered by other instances of the function.
For the local variables, we have to caller-save registers, callee-save registers, store locals in the stack frame, and/or all of the above.
Consider the prototypical recursive function, fact(X):
    int fact(int X) {
      int result;

      if (X==1) return(1);
      result= X * fact(X-1);
      return(X);
    }

Here, the X variable is both an argument to the function and a local variable in the function. Similarly, result is a local variable in the function. We don't have to worry too much about the value in result, as it is unknown before the recursive function call, and filled in by the recursive function call. However, the value in X has to be preserved across the function call, so we can multiply the result by X after the function call.

5.1  Task 4

Download the program wk4factorial.asm and load it into MARS. This program implements the recursive factorial function in MIPS assembly. Our argument (shown as X above) comes in as the $a0 register. Therefore, we need to preserve $a0 across the recursive call to the function. This is done by the sw $a0, 0($sp) instruction before the jal function call, and the lw $a0, 0,($sp) instruction after the return from the function.

5.2  Task 5

Using the wk4factorial.asm program as a template, write a MIPS assembly version of the recursive Fibonacci function. The definition of the function is:
  1. Fib(1) = 1
  2. Fib(2) = 1
  3. Fib(X) = Fib(X-1) + Fib(X-2)

6  Outlook for the Next Lab

In the next lab, we will look at exceptions, interrupts and exception handling on the MIPS CPU.


File translated from TEX by TTH, version 3.85.
On 25 Nov 2011, 11:15.