Procedures and Functions

Quick terminology:
function, procedure, subroutine are all used (incorrectly) as synonyms in this set of notes.

Why have functions?

Assembly languages typically provide little or no support for function implementation.

So, we get to build a mechanism for implementing functions out of what we already know.

The steps in the invocation and execution of a function:

  1. save return address
  2. function call
  3. execute function
  4. return

What is the return address? the instruction following call

What is the function call? jump or branch to first instruction in the function

What is a return? jump or branch to return address

Here is a not-quite-right MAL implementation of procedure call:


	   la  $8, rtn1
	   b proc1                 # one procedure call
     rtn1: # next instruction here
	    .
	    .
	    .
	   la  $8, rtn2
	   b proc1                 # another call location
     rtn2: # next instruction here

	    .
	    .
	    .

   proc1:    # 1st instruction of procedure here
	     .
	     .
	     .
	     jr $8

jr (jump register) is a new instruction -- it does an unconditional branch (jump, actually) to the address contained in the register specified.

The MIPS R2000 architecture (MAL) provides a convenient instruction for calls.


	    jal  procname

jal does 2 things:

  1. it places the address of the instruction following it into register $31. (The choice of $31 is fixed on this architecture.)
  2. it branches (jumps) to the address given by the label given as the operand (procname in this example).

The MIPS architecture is defined such that we can use pre-defined aliases instead of register numbers within our code. We will only use some of these aliases this semester. This alias is an alternative name that is designed to remind the code writer about the implied duties of the register. Use of an alias makes the assembly language code easier to understand, as we use specific registers for specific variables (or values). The alias for $31 is $ra. ra stands for return address.

The example, re-written:


             jal proc1   # use of $ra is implied
	     .
	     .
	     .
	     jal proc1
	     .
	     .
	     .


   proc1:    # 1st instruction of procedure here
	     .
	     .
	     .
	     jr $ra   # $ra is the alias for $31

One problem with this scheme. What happens if a procedure calls itself (recursion), or if a procedure calls another procedure (nesting) using jal?

Here is sample code for this problematic case (due to a nested call):


             jal proc1
	     .
	     .
	     .


   proc1:    .            # proc1 definition here
	     .
	     jal proc2
	     .
	     .
	     jr  $ra


   proc2:    .            # proc2 definition here
	     .
	     .
	     jr  $ra

The value in register $ra gets overwritten with each jal instruction. Return addresses are lost. This is an unrecoverable error!

What is needed to handle this problem is to have a way to save return addresses as they are generated. For a recursive subroutine, it is not known ahead of time how many times the subroutine will be called. This data is generated dynamically; while the program is running.

These return addresses will need to be used (for returning) in the reverse order that they are saved.

The best way to save dynamically generated data that is needed in the reverse order it is generated is on a stack.

As already defined in the class, the data can be defined as either

In this case, it is the amount of memory needed to hold items on the stack that cannot be determined until run time. This data (for return addresses) is dynamically generated.

The System Stack

A stack is so frequently used in implementing procedure call/return, that many (most?) computer systems predefine a stack, called the system stack, or simply, the stack.

The size of the system stack is very large. In theory, it should be infinitely large. In practice, it must have a size limit.

on the MIPS architecture:

 address  |         |
    0     | your    |
          | program |
          | here    |
          |         |
          |         |
          |         |
          |         |
          |         |
          | system  |  / \
  very    | stack   |   |  grows towards smaller addresses
 large    | here    |   |
 addresses

A little terminology:

Some people say that this stack grows down in memory. This means that the stack grows towards smaller memory addresses. Their diagram would show address 0 at the bottom (unlike my diagram).

down and up are vague terms, unless you know what the diagram looks like.

The MIPS system stack is defined to grow towards smaller addresses, and the stack pointer points to an empty location (the next available) at the top of the stack. The stack pointer is register $29. Its alias is $sp. It gets a value before program execution begins, because the operating system sets a proper value.

Code fragment for push, in MAL:


    sw   $?, ($sp)       # the ? is replaced by whatever register
    sub  $sp, $sp, 4     # contains the data to be pushed.

OR

    sub  $sp, $sp, 4     # a "better" implementation, since it allocates
    sw   $?, 4($sp)      # the space before using the space.

Code fragment for pop, in MAL:


    add $sp, $sp, 4    # the ? is replaced by a register number
    lw  $?, ($sp)

OR

    lw  $?, 4($sp)       # a "better" implementation, since it copies
    add $sp, $sp, 4      # the data out before deallocating the space

NOTE: If $sp is used for any other purpose, then the value of the stack pointer is lost.

An example of using the system stack to save return addresses:



     jal proc1              # one call location
     .
     .
     .
     jal proc1              # another call location
     .
     .
     .

proc1: 
       sub $sp, $sp, 4     # save return address
       sw  $ra, 4($sp)

      .
      .
      .
       jal proc2           # this would overwrite the return
                           # address, if it had not been saved.
      .
      .
      .

       lw  $ra, 4($sp)      # restore return address
       add $sp, $sp, 4
       jr  $ra

about Stack Frames (Activation Records)

From a compiler's point of view, there are a bunch of things that should go on the stack relating to procedure call and return. They include:

Each procedure has different requirements for numbers of parameters, their size, and how many registers (which ones) will need to be saved on the stack. So, the compiler composes a stack frame or activation record or AR that is specific to a procedure. The size of an AR is determined and known at compile time, so the size of an AR is static, and can be used in the assembly language source code.

Space for an AR gets allocated on the stack each time a procedure is called (invoked), and taken off the stack each time a return occurs. These ARs are pushed/popped dynamically (while the program is running).

An initial example showing the steps for the administration of ARs, but not all the code:


main:
    allocate main's AR
    jal  A
    jal  B
    .
    .
    deallocate main's AR
    done


A:  allocate A's AR        # AR stands for Activation Record
    jal C
    jal D
    deallocate A's AR
    jr $ra

B:  allocate B's AR
    jal D
    deallocate B's AR
    jr $ra

C:  allocate C's AR
    jal E
    deallocate C's AR
    jr $ra

D:  allocate D's AR
    deallocate D's AR
    jr $ra

E:  allocate E's AR
    deallocate E's AR
    jr $ra

Here is the call tree for this little example.

       main
       /   \
      A     B
     / \    |
    C   D   D
    |
    E

The code (skeleton) for one of these procedures:


A:  sub $sp, $sp, 20     # allocate frame for A, AR is 5 words
    sw  $ra, 20($sp)     # save A's return address

    jal C
    jal D

    lw  $ra, 20($sp)     # restore A's return address
    add $sp, $sp, 20     # remove A's frame from stack
    jr $ra               # return from A

Some notes on this:

     $sp --> |            |     address 0 at top of diagram
 (after A    |------------| \
  allocates  |            |  \
  A's AR )   |------------|   |
             |            |   |
             |------------|   |-- A's frame
             |            |   |   (A allocates, and then deallocates this space)
             |------------|   |
             |            |   |
             |------------|  /
             |   $ra      | /
             |------------|
             |            |
             |------------|

Parameter Passing

Use parameter and argument as synonyms for this discussion.

Just as there is little to no support for implementing procedures in many assembly languages, there is little to no support for passing parameters to those procedures.

Here's the important ideas to remember about parameter passing: Remember that when it comes to the implementation,

A note on parameter passing: A HLL specifies rules for passing parameters. There are basically 2 types of parameters. Note that a language can offer only one or both types.

There are many ways of implementing these 2 variable types. If call by value is the only parameter type allowed, how can we implement a reference type parameter? Pass the address of the variable as the parameter. Then access to the variable is made through its address.

This discussion now turns to where the parameters are located. There are 3 basic schemes for the locations of parameters.

  1. in a register
  2. on the stack
  3. in a pre-allocated, set-aside memory location.

1. Parameters passed in registers

The parent puts the parameter(s) into specific registers, and the child uses them.

An initial example that does not have the administrative code dealing with activation records:


	     .
	     .
	     .
	     move  $4, $20      # parent puts the parameter into $4
	     jal   decrement
	     move  $20, $4      # recopy parameter out to its correct place
	     .
	     .
	     .


             # the parameter is in $4
decrement:   add  $4, $4, -1
             jr $ra

Notes:

-- This is a trivial example, since the procedure is 1 line long.

-- Why not just use $20 within the procedure?

  1. convention -- parameters are passed in specific registers.
  2. the same procedure could be used to decrement the value in other registers -- just copy the value to register $4 first, and copy it out afterwards.

2. Historically more significant mechanism: pass parameters on the stack

Place the parameters to a procedure (function) on the stack. The parameters go into the AR allocated by the parent, into the stack space closest to what will be the child's AR.

       # portions of parent's code
       sub $sp, $sp, 12  # allocate parent's AR, parent only needs 3 words
       sw  $ra, 12($sp)  # save return address
       sw  $9, 4($sp)    # place parameter 1 into allocated space
       sw  $18, 8($sp)   # place parameter 2 into allocated space
       jal child
       .
       .
       .
child:
       sub $sp, $sp, 20  # allocate AR for child, assume child needs 5 words
       sw  $ra, 20($sp)  # save return address
       lw  $10, 24($sp)  # retrieve parameter 1 for use
       lw  $11, 28($sp)  # retrieve parameter 2
       .
       .
                         # use parameters in procedure calculations
       .
       .
       lw  $ra, 20($sp)  # restore return address
       add $sp, $sp, 20  # remove AR of child
       jr  $ra

Here is a diagram of what the stack looks like while child is executing.

          $sp --> |            |     address 0 at top of diagram
 (after child     |------------| \
     allocates    |            |  \
   child's AR )   |------------|   |
                  |            |   |
                  |------------|   |-- child's frame
                  |            |   |   (child allocates, and then deallocates this space)
                  |------------|   |
                  |            |   |
                  |------------|  /
                  |   $ra      | /
                  |------------|
                  |   P1       |  \
                  |------------|   \
                  |   P2       |   |-- parent's frame
                  |------------|   |
                  |   $ra      |  /
                  |------------|
                  |            |
                  |------------|

The parent:

The child:

3. Pass Parameters in specific, set aside memory locations

This way of passing parameters does not work for recursive calls. It is therefore not implemented.

An example of code (that must not be recursive) may appear as:

       # portions of parent's code
  .data
P1_for_child:  .word
P2_for_child:  .word
  .text
       .
       .
       sw  $9, P1_for_child    # place parameter 1 into allocated space
       sw  $18, P2_for_child   # place parameter 2 into allocated space
       jal child
       .
       .
       .
child:
       sub $sp, $sp, 4         # allocate AR for child, assume child needs 1 word
       sw  $ra, 4($sp)         # save return address
       lw  $10, P1_for_child   # retrieve parameter 1 for use
       lw  $11, P2_for_child   # retrieve parameter 2
       .
       .
                         # use parameters in procedure calculations
       .
       .
       lw  $ra, 4($sp)         # restore return address
       add $sp, $sp, 4         # remove AR of child
       jr  $ra

Passing Parameters: the 354 Way (Spring 2008)

Pass all parameters via the stack.

Reuse the stack space allocated for parameters, if a parent calls more than one child. The amount of stack space allocated for parameters is the maximum number of parameters passed to any one of the children.

Summary of the general ideas:

  1. use registers
  2. use some registers, and place the rest on the stack
  3. put all parameters on the stack (an unsophisticated compiler might do this)
  4. put parameters in memory set aside for them

How to Not Overwrite Register Values

An example that shows a problem:

  A calls B
  B calls C

  the code for A is in a separate file from B, C
  the code for B is in a separate file from C, A
  the code for C is in a separate file from A, B

  Code in A gets compiled separately from that of B, C
  Code in B gets compiled separately from that of C, A
  Code in C gets compiled separately from that of A, B

Suppose the code in A implements the statement


      X = Y + Z

The compiler must assign variables X, Y, and Z (assumed to be local variables for this example) to reside in registers. Which registers does it choose?

The compiler will have a list of registers that are currently unused, and choose the first 3 that it finds in the list. For this example, use $8, $9, $10.

Some of the code for A looks something like:


     A:   

         add  $8, $9, $10


	 jal  B

         # assume more code here that needs the current values
	 # in registers $8, $9, and $10

	 jr   $ra

The example continues:

Suppose the code in B implements the statement


      I = J * K

The compiler must assign variables I, J, and K (assumed to be local variables for this example) to reside in registers. Again, which registers does it choose?

Remember that since this code is compiled separately, the compiler has no knowledge of the code generated for A.

So, some of the code for B looks something like:


     B:   

         mul  $8, $9, $10

	 jal  C
  
	 jr   $ra

Notice that the compiler had chosen registers that overlap. This code cannot work correctly due to the overlap. The values in registers will get improperly overwritten.

There are 2 standard solutions to this problem. Both utilize space in an activation record to save/restore registers.

One solution: caller (parent) saves registers. A is the parent; B is the child.

The activation record for A:


                     (smaller addresses at top of this diagram)

       |           | <-- $sp
       -------------
       |  P1 (to B)| ------
       -------------      |
       |  P2 (to B)|      |
       -------------      |-- space allocated by A
       |  P3 (to B)|      |      (7 words; 28 bytes)
       -------------      |
       |   $8      |      |
       -------------      |
       |   $9      |      |
       -------------      |
       |   $10     |      |
       -------------      |
       |   $ra     | ------
       -------------


         # a code fragment of parts of A
  A:     sub  $sp, $sp, 28 
         sw   $ra, 28($sp)

         add  $8, $9, $10

	 # A is the parent of B
	 # A saves $8, $9, and $10 so B can overwrite the values,
	 #   should it choose to do so.
	 sw   $8, 16($sp)
	 sw   $9, 20($sp)
	 sw   $10, 24($sp)

	 jal  B

         # Since there is more code here that needs the saved values
	 # in registers $8, $9, and $10, restore them
	 lw   $8, 16($sp)
	 lw   $9, 20($sp)
	 lw   $10, 24($sp)
	 # now the values in $8, $9, and $10 can be used

         lw   $ra, 28($sp)
         add  $sp, $sp, 28 
	 jr   $ra
  

  B:     # much of B's code is omitted!   

         mul  $8, $9, $10   # As the child, B overwrites these values
                            # as it likes
	 jal  C
  
	 jr   $ra

The other solution: callee (child) saves registers. Again, A is the parent; B is the child.

Alternatively, it could be set up that the callee (child) saves/restores the values, so as to not mess up its caller's (parent's) register values:

                     (smaller addresses at top of this diagram)

       |           |
       -------------
       |           | <-- $sp
       -------------
       | P1 (to C) | ------
       -------------      |-- space allocated by B
       | P2 (to C) |      |    (6 words; 24 bytes)
       -------------      |
       |   $8      |      |
       -------------      |
       |   $9      |      |
       -------------      |
       |   $10     |      |
       -------------      |
       |   $ra     | ------
       -------------
       | P1 (to B) | ------
       -------------      |-- space allocated by A
       | P2 (to B) |      |
       -------------      |
       | P3 (to B) |      |
       -------------      |
       |   $ra     | ------
       -------------
       |           |

   A:    # much of A's code omitted.  

         # Consider A's role only as that of the parent in this example.

         add  $8, $9, $10


	 jal  B

         # assume more code here that needs the current values
	 # in registers $8, $9, and $10

	 jr   $ra


     # a code fragment of parts of B
   B:    sub  $sp, $sp, 24 
         sw   $ra, 24($sp)
	 # B saves $8, $9, and $10 so it cannot possibly mess up
	 # A's use of these registers.
	 sw   $8, 12($sp)
	 sw   $9, 16($sp)
	 sw   $10, 20($sp)

         mul  $8, $9, $10

         jal  C
  
	 lw   $8, 12($sp)  # restore the registers
	 lw   $9, 16($sp)
	 lw   $10, 20($sp)
         lw   $ra, 24($sp)
	 add  $sp, $sp, 24
	 jr   $ra

Each of these solutions does solve the problem. An implementation would need to make sure that the compiler always used just one method (for all time).

Some important notes:

How to Not Overwrite Register Values: the 354 Way (Spring 2008)

Use all of registers $8-$25 as general purpose registers. All of them are to be used as callee (child) saved.

In MIPS terminology, these are known as s registers, where s stands for saved. This irrelevant detail is presented in case Karen misses changing further notes that do use the aliases for $s registers, which look like $s0, $s1, etc. Also, the simulator likely references $s registers.

Functions

Functions are just procedures that return a value.

For Java programmers, functions are methods. For C programmers, procedures are functions that return a void type.

A function sets a return value, and this return value is available (set) after the function returns. So, a return value is similar to a parameter, only the flow of information is the reverse.

The location of a function return value follows the same rules as the location of parameters. We could place a function return value

  1. in a register.
  2. on the stack. The caller would allocate space for both outgoing parameters AND return values in its activation record. The function places the return value into this allocated space. The caller then has the return value in this allocated space.
  3. in a set-aside memory location (not on the stack). Just like for parameters, this location for a return value will not work for a recursive function.

The MIPS architecture specifies that a return value is placed into a register. In fact, there are two registers set aside for this purpose:

$2 is also called $v0 and

$3 is also called $v1

An example code fragment:


             jal  function1      # y = function1();
             sw   $v0, y
             .
             .
             .
 function1:  # the function does calculations
             .
             .
             .
	     # and then sets the return value
	     move $v0, $13       # the return value was in $13
	     jr   $ra

General Layout of a Function or Procedure

What the mechanisms should look like from the compiler's point of view:

A small portion of what the parent has:


   call setup
   call
   return cleanup

The child looks like:


child:      prologue

            calculations

            epilogue

When a child function is also a parent, then the call set up, call, and return cleanup are simply a portion of the calculations.

An Important Detail when Using the 354 Simulator

The I/O instructions putc, puts, and getc are implemented as functions that happen to be located within the operating system. They are not actual instructions on a MIPS R2000 processor. (In general, NO modern architecture has explicit input/output instructions.)

Parameters get passed to the operating system, and return values get set by the OS code's functions implementing putc, puts, and getc.

The simulator follows (mostly) the MIPS R2000 conventions that are given in a separate set of notes, but are not covered in 354! Here is a simplified version of what happens, so that you are aware:


Copyright © Karen Miller, 2006, 2007, 2008