Lab 09: MIPS32 assembly in
SPIM
In this lab, you will begin practice with assembly machine-level
programming. You’ll be programming using the MIPS32 instruction set,
which normally can be used to program a family of computer processors of
the MIPS architecture. Here, instead, we will be running our programs on
the SPIM simulator, a free piece of softaware available from Jim Larus,
a U. of Wisconsin researcher. We’ll use the beginning of the lab to set
up this software, and then write MIPS assembly programs, testing them in
SPIM, for the remainder of the lab.
The kind of programming exercises you’re being asked to do here are
not too complex, mostly because our goal is to get you used to the
logistics and quirks of this style of programming. There are many
aspects of this kind of programming that will be new, and potentially
many details that we could cover. Rather than give you complete
information about the assembler syntax, and SPIM, instead I encourage
you to learn today by copying my program examples, tweaking the code
I’ve given you (by figuring out what that code does), and getting those
to work.
Neverthless, there are several resources you can access to learn the
details of MIPS and SPIM, listed below:
• the SPIM
simulator
• SPIM’s
MIPS information links
• the
MIPS32 programmer’s guide
For today, you might most benefit by referencing these:
• the
MIPS32 quick reference
• this
SPIM system call reference
These two, and the instructions and examples below, ought to be enough
for you get this work done.
I’ve placed several sample programs in this repo. They are versions
of the code I showed you yesterday in lecture. You’ll want to dig
through these to learn the general format of MIPS assembly programs, and
to see use of the instructions and system calls you’ll need to get your
programs working. The instructions used can be summarized as below:
ADD $Rdst, $Rsrc1, $Rsrc2
: sum two registers, placing
result in a third
ADDI $Rdst, $Rsrc, constant
: sum a register with a
constant
SUB $Rdst, $Rsrc1, $Rsrc2
: subtract two registers,
placing result in a third
LI $Rdst, constant
: set a register to some constant’s
value
LA $Rdst, label
: load a register with the address of a
labelled location in the data segment
MOVE $Rdst, $Rsrc
: copy a value from one register into
another
LW $Rdst, ($Rtgt)
: load a register from memory at an
address
SW $Rsrc, ($Rtgt)
: store a register into memory at an
address
LB $Rdst, ($Rtgt)
: load a register with a byte in
memory at an address
SB $Rsrc, ($Rtgt)
: store a byte of a register into
memory at an address
BLT $Rsrc1,$Rsrc2,label
: jump to a line if one
register’s value is less than another. Also GT
,
GE
, LE
, EQ
, and
NE
.
B label
: jump to a line if one register’s value is less
than another. Also GT
, GE
, LE
,
EQ
, and NE
.
The registers you can use in these instructions hold a 32-bit value.
You’ll normally either treat that value as an integer (using two’s
complement), or as its 32 bits (where certain bit operations can be used
to examine or change its bits), or as an address that points somewhere
in memory where data can be fetched and stored. There are 32 of these
registers, however some of these are reserved for specific uses (that
I’ll work to teach you). The registers are divided up and named for
these different kinds of uses.
The registers are v0
, v1
,
a0-a3
, t0-t9
, s0-s7
,
gp
, sp
, fp
, and ra
.
For now, we’ll only use t0-t9
in our programs, with some
others used when following specific conventions for specific other
instructions.
We’ll learn a lot of this by just looking at some sample
programs.
Our first MIPS program
Take a look at the sample.s
program included in this
repo. I’ve also included it as text just below. The code computes the
sum of the numbers from 1 up to 100. It tracks the sum within register
t0
and the current integer of its count using the register
t2
. It has a loop of instructions that increment that count
by 1, and then bump the sum up by that count. Here is that code,
described further below:
.globl main
.text
main:
li $t0, 0 # sum = 0
li $t1, 1 # inc = 1
li $t2, 0 # count = 0
li $t3, 100 # last = 100
loop:
bgt $t3, $t2, done # if last == count goto done
addu $t2, $t2, $t1 # count += inc
addu $t0, $t0, $t2 # sum += count
b loop
done:
li $v0, 0 # return 0
jr $ra #
You’ll see three kinds of lines. The top two lines are “header”
information that communicate that a main
function is being
defined, and then that the “text” of the program will follow just after.
Littered throughout the code are lines that are labels. These
names main
, loop
, and done
can be
used by program instructions as targets of “jumps” and “branches” in the
code. Finally, there are the actual instruction lines. They each start
with an instruction’s “mnemonic” like li
,
addu
, bgt
describing what the instrcution is,
followed by the registers that they act upon.
The instruction
li $t3, 100
“loads” the value of 100
into the register named
t3
. The instruction
add $t2, $t2, $t1
computes the sum of registers t2
and t1
,
and stores the result into register t2
. If, instead, we
wanted to store that sum into t7
the instruction would
be
add $t7, $t2, $t1
The instruction
beq $t3, $t2, done
is a “branch.” It checks the values of t3
and
t2
, comparing their values. If t3
and
t2
hold the same value, then the processor should jump to
the instruction just below the label done
. If not, then it
should continue by executing the addu
instruction just
below.
This is all great, but probably you want to see the code in
action.
Setting up SPIM to run MIPS
code
This repo contains a “tarball” of SPIM’s source code. This is a
compressed binary archive file that contains the C
source code for the simulator. Type the following commands in the
console while working within your lab09
folder:
tar xvfpz spim-reedcs.tgz
pushd spim-reedcs/spim
At this point, we’ll want to make
the SPIM program.
However, if you are on a Windows machine that’s using
WSL, you’ll first need to install two standard tools needed to
build SPIM. So WSL users should type this additional command:
sudo apt-get install flex bison
Having these tools on your system, you can (all) then type
make
This will make the SPIM simulator as an executable file named
spim
. You’ll want to make copies of that simulator
executable (along with a supporting file) and put them within this repo
folder so you can complete your work. Type these commands:
popd
cp spim-reedcs/spim/spim .
cp spim-reedcs/CPU/exceptions.s .
cp spim-reedcs/helloworld.s .
./spim -f helloworld.s
The last line will actually run SPIM on a simple MIPS32 assembly
program, one that greets the world.
Sample programs
To run one our sample program type
./spim -f sample.s
It takes no input and produces no output, so its not the best program
to run. Instead run the interactive verison of it by typing
./spim -f sum_interacts.s
To perform input and output it uses special SPIM machinery for
“system calls”. There are several such calls that SPIM supports in your
programs. They will allow you to write more interactive code, much like
a C++ program. They each have a number. Here are the ones we’ll use
• System call #1: Output an integer to the
console.
• System call #4: Output a string to the console.
• System call #5: Get an integer input from the
console.
To system call #1, we are asked to load the register v0
with the number 1. (For the others, we instead load it with 4 or 5.) We
are also required to put the value that we want to output into register
a0
. This is the “argument register” normally. So we use the
move
instruction, like so:
move $a0, $t0
so that we can output the sum.
For outputting a string, we instead are required to put the address
that’s the start of a character string into a0
. The top
lines of that program
.data
prompt: .asciiz "Enter an integer: "
feedback: .asciiz "The sum from 1 to its value is "
eoln: .asciiz "\n"
tell SPIM to label three locations in the program’s image with
addresses prompt
, feedback
, and
ending
where three zero-terminated ASCII character strings
each live. Our code uses the “load address” instruction to put those
addresses into a0
before system call #4, like so:
li $v0, 4
la $a0, prompt
syscall
We also use a system call that fills in a console input into a
register. In the code you see the lines
li $v0, 5
syscall
move $t3, $v0
This calls the “input integer” system procedure. It happens to place
the input value into register v0
by convention. This
register is normally the “return value” register. And then our last line
moves (well, copies) that value into register t3
so that we
can use it as our maximum count value within the loop.
Incidentally, these system calls are documented
here.
Exercises
Exercise 1. integer division
We’ve included a MIPS assembly program product.s
that
asks for two positive integers as input and computes the product of
those two integers, outputting the product to the console. It uses the
method of repeated addition to compute the product.
Write a MIPS assembly program division.s
that asks for
two positive integers as input and computes the division of the one
integer by the other. It should output the quotient and also the
remainder that are computed by that division.
To do this I’d like you to use the method of repeated
subtraction. Let’s call the two numbers n and d.
To compute the quotient, we repeatedly subtract d from
n in a loop, counting how many times we can do this until we
reduce the value down to the remainder due to this division.
You can assume the dividend they’ve entered is non-negative and that
the divisor entered is positive.
This code should not be too dissimilar from product.s
and you can use its code as a starting point for your code.
Exercise 2. capitalize
The sample program hellobye.s
modifies the contents of
memory with store byte instructions SB
to rewrite the
string “hello” in memory so that it instead reads “bye”. It scans
through the string, changing its contents, by incrementing a register
$t0
that is used as a pointer to a character in the string.
SB
updates the data at that character. The program, in
addition to writing b
, y
, e
, also
writes the character with code 0 at the end of the string. This “null
termination” makes it so that system call #4 knows when to stop reading
characters of the string in memory.
Modify this program as a copy named capitalize.s
. It
should instead scan through the string labelled by
text_ptr
, assuming that it is a string of lowercase letters
of the alphabet, and capitalize them. It should work for any
string stored at text_ptr
, not just the string “hello”. You
can test it by changing the string in the .data
segment
from “hello” to other strings of lowercase letters.
Note that the ASCII code for lower case a
is 97. The
ASCII code for A
is 65. This convention holds for the whole
alphabet. The upper case code of a capital letter is 32 smaller than the
lower case code of a letter.
Your program can use an LB
instructions, loading the
ASCII code for a character in the string into a register, subtract 32
from that, then replace the character in memory with a SB
instruction.
BONUS: Exercise 3
Write a program guess.s
that has an integer between 1
and 100 stored in its data segment. (See output100.s
from
lecture yesterday for an example of how I stored 100 in the data segment
of the program using the .word
directive.) This will serve
as the “random number” to be guessed by the program user for a guessing
game program that you write.
The program should prompt the user for an integer value, telling them
whether they are correct and, if not, reporting whether they guessed too
high or too low. They should be allowed to keep guessing in this way
until they guess the number correctly. It should tell them when their
guess is correct.
NOTE: For this one we can’t generate a random
number. Instead, you’ll want to test the code by varying the
.word
number listed in the .data
segment of
the program for several different runs. Try enough hidden numbers to
convince yourself that the code is correct.