|
 |
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:
-
The
SPIM Manual.
- Another resource is from "Computer Organization",
by Patterson and Hennessy:
Appendix A.
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 |
Dig | Off | Let | Off | Let | Off |
Temp | Off |
0 | 0 | a | 80 | n | 184 |
t0 | 288 |
1 | 8 | b | 88 | o | 192 |
t1 | 296 |
2 | 16 | c | 96 | p | 200 |
t2 | 304 |
3 | 24 | d | 104 | q | 208 |
t3 | 312 |
4 | 32 | e | 112 | r | 216 |
t4 | 320 |
5 | 40 | f | 120 | s | 224 |
t5 | 328 |
6 | 48 | g | 128 | t | 232 |
t6 | 336 |
7 | 56 | h | 136 | u | 240 |
t7 | 344 |
8 | 64 | i | 144 | v | 248 |
t8 | 352 |
9 | 72 | j | 152 | w | 256 |
t9 | 360 |
| | k | 160 | x | 264 |
t10 | 368 |
| | l | 168 | y | 272 |
t11 | 376 |
| | m | 176 | z | 280 |
t12 | 384 |
---|
|
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.)
|