CS61C Summer 2004

Digression on Stack Issues

Purpose

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:

  1. What is the deal with the stack pointer?
  2. What is the deal with the frame pointer?
  3. If I am writing a simple function with less than 4 arguments in assembly, what do I do with the stack?
  4. If I am writing a simple function in c with less than 4 arguments, what will the c compiler do?
  5. If I am writing a c function with lots of arguments, what goes on with the stack?
  6. What should I do on my project 2?

  1. What is the deal with the stack pointer?
  2. 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.

  3. What is the deal with the frame pointer?
  4. 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.

  5. If I am writing a simple function with less than 4 arguments in assembly, what do I do with the stack?
  6. Remember: the stack (and stack pointer) are available to you as a programmer and thier use is governed by convention. Therefore, if you don't need them, you need not use them. For example, consider the following C function prototype that takes an int arg and returns 2*arg:
    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.

  7. If I am writing a simple function in c with less than 4 arguments, what will the c compiler do?
  8. 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.

  9. If I am writing a c function with lots of arguments, what goes on with the stack?
  10. 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).

  11. What should I do on my project?
  12. 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.


CS61C Notes revised slightly from Brian Cavagnolo's version.