MIPS Programming

This is your crash course in assembler programming; you will teach yourself how to program in assembler for the MIPS processor. You will learn how to use the instruction set summary to write a program which runs in the MIPS simulator, running in "No Pipe" mode. To write the program, you will need to understand iteration, subroutine calls and parameter passing, recursion, storage and accessing of word and byte arrays, how basic high-level constructs map into assembler code (such as "if - then - else"), some of the differences between signed and unsigned integer arithmetic and comparisons, and many other things as well.

A basic rule is: try to be mechanical (that is, don't be "tricky") when you translate high-level code into assembler code.

For example, the high-level code (shown here in pseudocode)

  WHILE  [condition]  DO

should have this structure in assembler:

  top_of_while:  [compute condition]
                 [if condition is false, branch to "while_escape"]
                 [unconditional branch to "top_of_while"]

Test-last loops are even simpler:

  UNTIL [condition];

would have this structure in assembler:

  top_of_repeat: [body]
                 [compute condition]
                 [if condition is false, branch to "top_of_repeat"]

Conditional constructs also require a label for the branch target:

  IF [condition] THEN

would have this structure in assembler:

                 [compute condition]
                 [if condition is false, branch to "if_bottom"]

If-then-else is a little more complicated:

  IF [condition] THEN

and has this structure in assembler:

                 [compute condition]
                 [if condition is false, branch to "do_else"]
                 [unconditional branch to "if_bottom"]
  do_else:       [else_body]

"For"-loops are very similar to the examples given above.

Simple assignment statements involve evaluating the right-hand side until you obtain a simple result, and then storing that result into the address of the location designated by the left-hand side. For example

  A := expression;

has this structure in assembler:

                 [compute expression into a simple result]
                 [obtain address of location "A"]
                 [store the result into that address]

As you can see from the examples above, you need to use "branch" and "conditional branch" instructions to establish the control structure of your code, and invent "labels" so that your branch instructions can state where they want to branch to. But if you follow the guidelines above for writing structured assembler code, it becomes easy to nest structures inside of each other, and that way you can begin to write very complicated code very quickly. Your biggest problem will be in inventing new "labels", so that all labels have unique names.

Subroutine Calls and Parameters

There are two extremes in how to pass parameters, and how to allocate local variables within subroutine activations. The first way is: pass the parameters in registers (the MIPS convention says, registers $a0 through $a3 can be used to pass parameters, that is, at most four parameters), let the subroutine return its result (if the subroutine is a function) in registers (the MIPS convention is $v0 and $v1), and allocate local variables by assigning each local variable to a register (for example if we have a local integer variable K, then we could simply say (for example) "let register $t0 be integer K."

The advantage of using registers like this, for (almost) everything, is to avoid having to access the primary memory (using memory instructions such as lw, sw etc) which is slower than using just registers. The disadvantage is that there aren't very many registers. What happens if we have 5 parameters ? What happens if we have 50 local variables ?

The other extreme is to only use registers as "scratchpads," that is, to compute temporary results, but to pass parameters, return result, and allocate local variables in the primary memory. A special part of the primary memory is used for this, called the stack. The top-of-stack is identified by reserved register $sp, which points to (that is, which contains the address of) that word in memory which is the topmost word of the stack.

p>To "push" a new word onto the stack, $sp is decremented to make room on the stack, and then the new data value is written into the memory location pointed to by the new contents of $sp. That is, our stack "grows" towards lower addresses. To "pop" from the stack, do the reverse: first read the data, then increment the $sp. There is a single rule for working with the stack: never attempt to access data at a negative offset from register $sp. Consider all those memory locations as "volatile." For example, if you write to -4($sp), and then immediately attempt to read from -4($sp), you may read back something else than what you wrote ! The reason for this is exceptions and interrupts which you will learn about later in the course.

D0013E Calling Convention

The following subroutine convention will be used in this course, and it is a compromise between the two extremes discussed above.

By following this convention, the code you write will be naturally recursive (that is, subroutines can call themselves and not become confused). The "frame pointer" ($fp) may appear unnecessary for programs as small as in the one below, but we want you to learn how to use it. Consider the following main program, which calls subroutine "sum":

  # the main program: 
  main:  li  $a0, 5           # set up parameter := 5
         bal sum              # call:  sum( 5 )
	                      # result in $v0

Now we show how "sum" would look, first without a frame pointer, and afterwards with a frame pointer.

============ "sum" without frame pointer ===============
===== code is kind of "tricky" and stack-delicate. =====

#  compute  sum = n + (n-1)+(n-2)+...+ 1
#  call only for n  0.

# sum(n: int): int       (direct-recursive)
#   if n = 1
#     then return( 1 )
#     else return( n + sum(n-1) );
sum:   addiu $sp, $sp, -4	# push the return addr...
      sw    $31, 0($sp)	# ...onto the stack.
      addiu $sp, $sp, -4      # now push the incoming...
      sw    $a0, 0($sp)	# ...param "n"
if:    ori   $t0, $zero,  1	# $t0 := 1
      bne   $a0, $t0,  else	# n  1, branch to "else"
then:  ori   $v0, $zero,  1	# return value := 1,
      b     exit		# to exit sequence.
else:  addiu $a0, $a0, -1	# compute n-1 (destroys $a0).
      bal   sum		# $v0 := sum( n-1 ).
			# call also wipes out our $31.
      lw    $t0, 0($sp)	# fetch our incoming value "n".
      addu  $v0, $v0, $t0	# $v0 := n + sum( n-1 ).
exit:  lw    $31, 4($sp)	# restore return address.
      addiu $sp, $sp, 8	# stack like we found it.
      jr    $31		# return
========== end "sum" without frame pointer =============

However, when writing large subroutines, having many parameters, many local variables, and which may even change $sp during its execution (for example, when using the stack to evaluate a complicated push-down expression that does not fit in the registers) it is difficult to do everything $sp-relative.

For that reason we will use a second register $fp (frame pointer) to "mark" a place on the stack. $fp is guaranteed never to change during this activation, so we can rely on it to generate addresses into our activation record:

Here's an example of how to use the frame pointer. The main program looks exactly the same as above.

============== "sum" with frame pointer ================
===== example of a subroutine with frame pointer.  =====
===== easier to restore and manage the stack.      =====
===== little bit less efficient code.              =====

# ==================================
#  compute  sum = n + (n-1)+(n-2)+...+ 1
#  call only for n  0.

# sum( n: int ): int       (direct-recursive)
#   x: int;                (example of local variable)
#   if n = 1
#     then return( 1 )
#     else return( n + sum(n-1) );

sum:   addiu $sp, $sp, -4
      sw    $31, 0($sp)	# push the return addr
      addiu $sp, $sp, -4
      sw    $fp, 0($sp)	# push the old frame pointer
      move  $fp, $sp		# establish new frame pointer.
      addiu $sp, $sp, -8	# make room for 2 full
			# word local variables

  # everything above is called the "entry sequence".
  # the stack now looks like this.
  #    +-------------------+
  #    | uninit, for "x"   | -8($fp) <= $sp points here
  #    +-------------------+
  #    | uninit, for "n"   | -4($fp)
  #    +-------------------+
  #    | old frame pointer |  0($fp) <= $fp points here
  #    +-------------------+
  #    |  our return addr  |  4($fp)
  #  --+-------------------+--
  #    |                   |
  #    |  caller's stack   |
  #    |                   |
  # (we don't need "x" below, just given as an example.)
  # now this activation can access its activation record

save:  sw    $a0, -4($fp)	# save "n" into local var
if:    ori   $t0, $zero, 1	# $t0 := 1
      bne   $a0, $t0,  else	# n  1, jump to "else"
then:  ori   $v0, $zero, 1	# n = 1:  return value := 1,
      b     exit		# escape to exit sequence.
else:  addiu $a0, $a0, -1	# compute n-1 (destroys $a0).
      bal   sum		# $v0 := sum( n-1 ).
      lw    $t0, -4($fp)	# fetch value "n".
      addu  $v0, $v0, $t0	# $v0 := n + sum( n-1 ).
  # the exit sequence reverses the entry sequence:
exit:  move  $sp, $fp		# all local var's gone: easy !
      lw    $fp, 0($sp)	# restore old $fp
      addiu $sp, $sp, 4	# and pop that word,
      lw    $31, 0($sp)	# restore return address,
      addiu $sp, $sp, 4	# and pop that word.
      jr    $31		# return
=========== end "sum" with frame pointer ===============


The simulator starts executing with the first instruction that you write, so if you want to write your subroutines first, you will have to "jump over" them...

       j  main          # ...like this.  To my main program.

 sum:  # for example, subroutine "sum" above.
       jr $ra           # end of code for "sum"

 main:                  # my main program, that calls "sum"
       li  $a0, 5       #  compute sum(5)
       bal sum          # "sum" returns answer in "$v0"