CS 3723
 Programming Languages 
   MIPS Assembly Lang.  


History: The MIPS hardware development started in 1981 at Stanford University by John Hennessy and his slaves (= graduate students). It was one of two revolutionary RISC computers (the other being SPARC, developed at Berkeley by Patterson and another group of graduate student slaves). Both of these architectures had a tremendous influence on the computer industry.

Hennessey and Patterson used MIPS in their various computer organization and architecture textbooks. To quote from a manual with a link below: "The architecture of the MIPS computers is simple and regular, which makes it easy to learn and understand. The processor contains 32 general-purpose 32-bit registers and a well-designed instruction set that makes it a propitious target for generating code in a compiler." In contrast, they refer to the Intel 80x86 as "an architecture that is difficult to explain and impossible to love."


Resources: The simulator for MIPS, called SPIM, is available on the Linux machines in the computer science labs (using the command spim as shown below). It accepts the MIPS assembly language, and this will be the target of out (small) compiler. Resources with way too much information are:


What We Will Use: The main parts of MIPS are concerned with integer arithmetic, using special 32-bit locations called registers (there are 32 of them). However, our compiler will output instructions to act on floating point variables, doubles. These are executed in a floating point coprocessor separate from the main MIPS processor. Most of this will be invisible to you, but for you the implications are that we will use a small and obscure subset of the MIPS instruction set, mostly those instructions that carry out 64-bit operations on floating point numbers. We will mainly use special 64-bit registers. There are effectively 16 of these, denoted by $f0, $f2, $f4, ... $f30. (There must be an even integer following the $f.)

MIPS has instructions for add, sub, mul and div of 64-bit doubles, giving a double as an answer. As a first example:

Example: Find the value of f = g + 5, supposing g already has a value, where everything is a double. This means to access the value of g, and of the constant 5, add these as doubles, and leave the result in the location for f. You might wish for an instruction such as add f, g, 5, but all instructions in MIPS go through the registers. Double values must use the special registers for doubles.

The above simple example requires 4 MIPS instructions: First two "load" instructions to load 64-bit values of g and of the constant 5 into special floating point registers. Then an add is performed on the values in these registers, with the result in another (or one of the same) registers. Finally comes a "store" instruction that will store the sum into a memory locations used for the variable f. Here is what these 4 instructions might look like:

f = g + 5    in MIPS
l.d     $f2, 128($s1)    #  $f2 = M[16]
l.d     $f4, 40($s1)     #  $f4 = M[5]
add.d   $f6, $f2, $f4    #  $f6 = $f2 + $f4
s.d     $f6, 120($s1)    #  M[15] = $f6

With 32-bit integers, MIPS has instructions named lw (for "load word"), sw (for "store word"), and add. But for 64-bit doubles, you use l.d (for "load double"), s.d (for "store double"), and add.d (for "add double"). This example is discussed more thoroughly below.

In this course, all our numbers and variables are going to be offsets from an address stored in $s1. Our compiler will keep all its numbers, variables and temporary locations in a single "array" M of 64-bit doubles. All values and offsets are in bytes, and a 64-bit double holds 8 bytes. So a reference to M[i] is an offset of i*8 from the start of M. There is no actual array M at all, but just a starting address in memory referred to in the MIPS program by M, and offsets from that address. Here's a table describing the offsets from M used by numbers, variables, and temporaries:

 

Memory Allocation Table
Type of value Location in M # Formula Offset
Constants 0.,1.,...,9. M[0],M[1],...,M[9]10 M[ch-'0']=
   M[3]
for ch='3'
((ch-'0')*8)($s1)=
   24($s1)
for ch='3'
Variables a,b,...,z M[10],M[11],...M[35]26 M[ch-'a'+10]=
   M[15]
for ch='f'
((ch-'a'+10)*8)($s1)=
   120($s1)
for ch='f'
Temporary variables M[36],M[37],...,M[161]125 M[n+36]
   for temp n>=0
(n*8+288)($s1)
   for temp n>=0

In the code below, we will load the address of M into the register $s1. Then the location for a variable c, which is in M[2+10] = M[12], will be denoted by (12*8)($s1)=96($s1) which means "96 bytes past the start of M, with the starting address stored in register $s1". We want to go over the simple example f = g + 5 more carefully. At the left this is shown in 5 steps, ending with actual MIPS assembler.

At the right is a simple table giving the exact offsets:

Successive Refinements of Conversion to MIPS
f  =  g + 5
add  f,  g,  5
load value of location  g  into register  $f2 
load value of  5  into register  $f4 
add values in  $f2  and  $f4 , leaving result in register  $f6 
store value in  $f6  into location  f 
load  M[16]  into  $f2 
load  M[5]  into  $f4 
add  $f2  and  $f4 , result in  $f6 
store the value of  $f6  into  M[15] 
l.d     $f2, 128($s1)    #  $f2 = M[16]
l.d     $f4, 40($s1)     #  $f4 = M[5]
add.d   $f6, $f2, $f4    #  $f6 = $f2 + $f4
s.d     $f6, 120($s1)    #  M[15] = $f6

  
Offsets for Digits, Letters, Temporaries
DigOffLetOffLetOff TempOff
00a80n184 t0288
18b88o192 t1296
216c96p200 t2304
324d104q208 t3312
432e112r216 t4320
540f120s224 t5328
648g128t232 t6336
756h136u240 t7344
864i144v248 t8352
972j152w256 t9360
k160x264 t10368
l168y272 t11376
m176z280 t12384

You should realize that the use of an array M in this way for all the storage that our compiler will need was my own arbitrary choice, made to keep the compiler simpler. Essentially, I mapped all the storage needed at run-time into a single array. This is called static storage allocation (in a particularly simple form).

There are other, better ways to handle variables: in general one should use a symbol table for the variables that actually occur in the program, and map each variable at compile time to a memory location at run time. This would allow arbitrarily many variables.

Similarly one could use the same table or a different table for constants as they occur. Alternatively you could handle the constants one-by-one as they arise, just hard-wiring the constant into the assembly code.


Example Program: See example.s for two versions of a program that tests the operations + - * /.

(Revision date: 2014-09-30. Please use ISO 8601, the International Standard.)