Stackframes for saving local stuff belonging to a method

There is a scheme for this described beginning on page A-23 of the textbook, but it is far too complicated for what we need. Here is a simpler method that will work for us. The concepts are the same and Figures A-11 and A-12 should still be useful in understanding what is going on.

Each invocation of a method gets its own stackframe. That is the secret that lets a method be recursive. Each time the method calls itself, a new stackframe is created for that invocation. Figure A-12 shows the stack after the factorial function has called itself a few times.

In our scheme, we will save any registers that we plan to use at the beginning of the method, and restore them at the end. They well be saved in the stackframe, of course. One register that will always be saved is $31 aka $ra the register that holds the return address for jal instructions. We don't actually have to save this register unless we plan to call another method, but it's easier just to do it every time and not have to worry about forgetting when it's really important.

The format of a typical method

At the beginning of the method we place code to create the stackframe and copy the registers we need to save into it. At the end of the method we place code to copy the values in the stackframe back into the registers they came from and then destroy the stackframe. In between, we put the code that accomplishes the real business of the method.

create stackframe
copy registers to stackframe

do the method's business

copy registers back from stackframe
destroy stackframe
return to caller

In the process of writing the code for a method we have to figure out which registers need to be saved. Any register that contains an important value and that we will write something else in needs to be saved. The first obvious one is the return address in $ra. Others can be the argument registers ($a0 .. $a3), the return-value registers ($v0, $v1), and any of the $s registers that we plan to use. The reason for this is that the $s registers may contain values that are important to method invocations prior to us on the call stack, and we need to return without disrupting the environments of those method invocations (viz. their stackframes). Once we know which registers we will save, we know how much space to allocate for our stackframe: 4 bytes for each register. Since the stack grows downward, we subtract the size of the new stackframe from the stack pointer ($sp). This has the effect of adding a new word to the stack for each register that we need to save. Copying registers to the stackframe is a register-to-memory operation, so we use store instructions for that. Copying from the stackframe back to the registers at the end is done by loads. We destroy the stackframe by adding to the stack pointer the same number we subtracted from it to create the stackframe. This restores the stack to what it was before we got called, so the environment of our calling method has been restored. Of course, the return is accomplished by the jr $ra we have always used for that.

An example

Let's do an example in which we will save $ra, $s0, and $a0. That's 3 registers, or 12 bytes. Here's the code:

subu $sp, $sp, 12           # create the stackframe with a size of 3 words

sw    $ra, 8($sp)             # copy $ra into the top word in the stackframe

sw    $s0, 4($sp)             # copy $s0 into the middleword in the stackframe

sw    $a0, 0($sp)             # copy $a0 into the bottomword in the stackframe

 

In here we do our stuff

 

lw    $ra, 8($sp)                   # copy $ra back from the stackframe

lw    $s0, 4($sp)                   # copy $s0 back from the stackframe

lw    $a0, 0($sp)                   # copy $a0 back from the stackframe

addu $sp, $sp, 12                 # destroy the stackframe

jr      $ra                               # return to the caller

Notes on the code

It should be obvious that the subu has to come first in its block of code at the top, and that the addu must come last in its block of code at the bottom (i.e. right before the return). The order of the stores in their block doesn't matter, nor does the order of the loads in their block. Instead of reversing the order of the loads at the bottom, I have made it the same as in the top block of stores. This makes it easy to write the bottom block using copy and paste. Just paste a copy of the top block at the bottom and change all the sw's to lw's. Then paste the subu after that and change it to an addu. Next add the jr $ra at the very bottom. Finally, go back to the middle, and put in the business code there.

What it looks like in memory

pointers address base/displacement what's in that location
original sp

etc.

0x7FFFFFA8

0($sp)

stuff already on the stack

 (end of the previous stackframe)

 

0x7FFFFFA4 8($sp) saved $ra

 

0x7FFFFFA0 4($sp) saved $s0
new sp 0x7FFFFF9C 0($sp) saved $a0

 

0x7FFFFF98

etc

free space that will go to succeeding stackframes

Our new stackframe is made up of the words at addresses 0x7FFFFFA4, 0x7FFFFFA0, and 0x7FFFFF9C (the ones colored pink).

Another example on the website

The file stackframe.s in the example code section of the class website is a minimal example. The main program calls a method named "body" from a loop. The body method makes no method calls and needs no other temporary storage, so it does not need to create any stackframes. The main program needs to save its $ra so that it can return to the system at the end. This requires it to create a stackframe whose size is 4 bytes.