The purpose of this web page is to clarify exactly what is going on with the MIPS calling convention. In particular, we are going to answer the following questions:
The stack pointer is $sp or $29 on the MIPS platform. It is used to allocate space for local variables and sometimes for arguments. These cases are described in the cases below. One important fact to keep in mind is that the stack pointer is governed entirely by convention and not hardware. In other words, when we discuss the rules of the stack pointer, it is your responsibility as a programmer to observe the rules. You may break the rules. If you do break the rules, stuff may crash, other parts of the program may get unexpected information, and you will introduce bugs that are tough to find.
The frame pointer is much like the stack pointer. The big difference is that we do not use it much in this class. A natural question is, why don't we use it? Well, it is typically used to point to local variables in a function. However, when we write assembly in this class, we typically allocate memory in the .data section. This is analogous to static memory in C. If we were allocating local variables, we may find it convenient to use the frame pointer for accessing local variables and the stack pointer for dealing with arguments, saving temp registers, etc.
Despite the fact that we don't use the frame pointer much, you should know a few things about it. First of all, because local variables in C functions are typically allocated on the stack, you can expect the frame pointer to be somewhere near the stack pointer. Also, when you dump code from MIPSgcc as we did in lab 4, you will see the frame pointer and the stack pointer. If you are analyzing the output of MIPSgcc, We suggest tracking what the stack pointer AND the frame pointer do. You will see that they serve very similar purposes.
int twice(int arg);One MIPS implementation of this could be:
twice: add $v0, $a0, $a0 # add the input arg to itself jr $ra # return to caller
Note that we did not use the stack. This is totally acceptable. For simple tasks, don't mess with the stack. Also, don't worry what the caller is doing with the stack. The bottom line is that in this case you just don't care.
You should use your new-found knowlege from lab 4 to experiment
with this. But just for you, here is an example:
Say we have the following simple C code:
int twice(int arg1) { return arg1+arg1; } void main() { int a=1; twice(a); }
Now we run it through MIPSgcc and we get the following. Note that we changed the registers from numbers to letters and deleted some irrelevent code for clarity. Comments added:
0 .text 1 twice: 2 sll $v0,$a0,1 #double arg 3 j $ra #return to caller 4 5 main: 6 subu $sp,$sp,24 #get some stack space 7 sw $ra,16($sp) #push the return address 8 li $a0,1 #put value of a in arg register 9 jal twice #jump to function 10 lw $ra,16($sp) #restore return address 11 addu $sp,$sp,24 #restore stack pointer 12 j $ra #return to caller
So, this looks like we would expect. The function twice is pretty simple. The compiler chose to use the sll instruction instead of the add instruction. However, the agument appears in $a0 and the return value goes in $v0. The stack doesn't get touched.
Now let's discuss what is going on with the stack pointer in main. We know we have to save the return address, but why does the compiler take 24 bytes of stack space for this simple task? There are a few reasons. First of all, by convention, callers needs to allocate "shadow" space in memory for the argument registers $a0-$a3. The reason for this is that C requires every variable to have an address and registers do not have addresses. Imagine in our code if the function "twice" asked for the address of its argument using the '&' operator. There would have to be a place in memory whose address can be returned. You may argue that this is pointless because the space is left empty. Well, the point is that the space is there if needed. In actuality, if "twice" asks for the address of its argument for some reason, it (not main) is responsible for initializing the space to the proper value. Furthermore, the address will be calculated from the stack pointer.
So, we have accounted for 16 bytes of stack space so far. Why 24 bytes? Well, we need to have a spot to store $ra, of course. That brings us up to 20 bytes. The reason that we have those last 4 bytes is that the official MIPS calling convention requires that stack frames are a multiple of 8 bytes. This is done to simplify hardware for double-precision operations. The example given on the project 2 webpage violates this convention. Apologies.
NOTE 1: We deleted a lot of the output that MIPSgcc output to simplify this example.
NOTE 2: We compiled with the following command line:
MIPSgcc -O2 -S -fno-delayed-branch simple.c
The -O2 turns on gcc optimizer on. If you turn it to a higher level (i.e., -O3) the output output breaks some of the conventions that we are discussing in the interest of saving instructions. However, it does so in a way that does not effect the results. If you turn the optimizer off by eliminating the -O argument all together, the code that gets output is VERY verbose. It observes all sorts of conventions that we are not discussing. In particular, it uses the frame pointer extensively and maintains all variables in memory.
After the last example, this one should be pretty clear. Consider the following C code:
int foo(int arg1, int arg2, int arg3, int arg4, int arg5, int arg6) { return arg1+arg2+arg3+arg4+arg5+arg6; } void main() { int a=1,b=2,c=3,d=4,e=5,f=6; foo(a, b, c, d, e, f); }
Here is what MIPSgcc spits out. Note the added comments and the changed register names from numbers to letters:
1 .text 2 foo: 3 addu $v0,$a0,$a1 #$v0=arg1+arg2 4 addu $v0,$v0,$a2 #$v0=arg1+arg2+arg3 5 addu $v0,$v0,$a3 #$v0=arg1+arg2+arg3+arg4 6 lw $v1,16($sp) #$v1=arg6 7 lw $a0,20($sp) #$a0=arg5 8 addu $v0,$v0,$v1 #$v0=arg1+arg2+arg3+arg4+arg5 9 addu $v0,$v0,$a0 #$v0=arg1+arg2+arg3+arg4+arg5+arg6 10 j $ra #return to caller 11 12 main: 13 subu $sp,$sp,32 #get some stack space 14 sw $ra,24($sp) #store the return address 15 li $v0,5 #put value of e in $v0 16 li $v1,6 #put value of f in $v1 17 li $a0,1 #set arg1 to foo equal to 1 18 li $a1,2 #set arg2 to foo equal to 2 19 li $a2,3 #set arg3 to foo equal to 3 20 li $a3,4 #set arg4 to foo equal to 4 21 sw $v0,16($sp) #save arg5 to stack 22 sw $v1,20($sp) #save arg6 to stack 23 jal foo #jump to foo 24 lw $ra,24($sp) #restore return address 25 addu $sp,$sp,32 #restore stack pointer 26 j $ra #jump back to caller
After main calls foo, this is what the stack looks like:
|<---- 4 bytes ---->| +-------------------+ | | +-------------------+ | empty padding | +-------------------+ | original $ra | +-------------------+ | arg6=6 | +-------------------+ | arg5=5 | +-------------------+ |empty space for $a3| +-------------------+ |empty space for $a2| +-------------------+ |empty space for $a1| +-------------------+ $sp->|empty space for $a0| +-------------------+ | | +-------------------+ | | +-------------------+ | |
So, we have the empty spaces at 0($sp), 4($sp, 8($sp), and 12($sp) for the arguments as required by the MIPS calling convention. We also have the padding at 28($sp) to keep the 8 byte alignment. The original $ra appears at 24($sp). Finally, arg5 and arg6 appear at 16($sp) and 20($sp) respectively. This is where foo gets them from.
NOTE: Lab 05 did an example where a subfunction took its fifth argument from 20($sp). THIS IS AN ERROR!! A typical MIPS function would behave like the one that we describe here. That is, it would take its argument from 16($sp).
In project 2, sprintf takes a variable number of arguments. If this number exceeds 4, the useful information will be on the stack starting at 16($sp). Your code must detect the number of arguments based on the number of '%' characters in the format string and get them from the proper place.