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.
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.
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
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.
| 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).
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.