Loads, stores and immediates

MIPS has three ways of naming an operand and only one of them reaches memory. What offset(base) can and cannot do, why indexing an array costs a shift and an add, and what the assembler's label forms turn into.

The instruction set lecture wrote an instruction's operands as destination and source without saying what can go in them. This is what can, and the way an operand is written is its addressing mode. MIPS has three, which is fewer than any other machine in this editor.

modewrittenin C
register$t0x
immediate77
base plus offset4($t0)p[1], *(p + 1)

The first two name a value the CPU already has or the assembler already knows. The third names an address, and only a load or a store may use it.

Arithmetic never reaches memory

MIPS is a load/store architecture, which means exactly this: lw, lh, lb, lbu, lhu, sw, sh and sb are the only instructions that touch memory, and everything else works on registers. There is no add that reads a variable, no cmp against a word in memory, no increment of a counter that lives at an address.

So the shape of every program that works on data in memory is the same three steps: load it into a register, do the work there, store it back. On the M68K, where add.l total, d0 adds the long at a label straight into a register, that is one instruction; here it is three, and the reason the design went that way is that a load can take many cycles and an add takes one, so the two are kept apart.

offset(base)

lw $t3, 4($t2) reads the word at the address $t2 + 4. The register is the base, the number is the offset in bytes, and the offset is a signed 16 bit constant the assembler puts inside the instruction. That is the whole mode: one register, one constant, added while the instruction runs.

The four words sit at 0x10010000, where .data puts the first label:

addressvaluewhich element
0x100100000000000Anumbers[0]
0x1001000400000014numbers[1]
0x100100080000001Enumbers[2]
0x1001000C00000028numbers[3]

$t3 comes out at 10 and $t4 at 20. $t7 is 1001000C, and $t8 is 30, because the offset may be negative and -4($t7) is one word back.

Two things the mode cannot do. It cannot add two registers, so there is no lw $t0, ($t1 + $t2). And it cannot scale anything, so an index has to be turned into a byte offset by your own code.

Indexing an array

C hides the size of an element: numbers[i] means the address of numbers plus i times four, because the elements are 4 byte words. MIPS makes you write both halves of that.

$t4 comes out at 30 and $t5 at 40, and after the sw the word at 0x10010008 reads 00000063, which is 99. sll $t2, $t1, 2 is the multiplication by 4: shifting left by 2 multiplies by 4, and every size on this machine is a power of two, so a shift is always what you want here.

Once the address is in a register, the constant offset does the rest of the work: 0($t3) and 4($t3) are two neighbouring elements out of one computed address, and a loop over pairs pays for the arithmetic once.

Try changing li $t1, 2 to li $t1, 0 and watching which word changes instead.

The assembler's label forms

lw $t5, numbers is not one of the three modes. It is a pseudo-instruction, and the assembler turns it into a lui that puts the address in $at and a real lw through it.

$t1 is 10, $t2 is 30 and $t4 is 30 as well. Click each of those lines after building and the editor prints what they became: two instructions for the first three, three for the last, and every one of them writes $at.

That is the trade. lw $t1, numbers reads like C and costs two instructions and $at every time it runs, so inside a loop you do the la once, before the loop, and use offset(base) inside it. la is the one you will write most, because a pointer in a register is what the loop wants.

Walking with a pointer

An array in memory has no length, no bounds and no element names, so a loop over it is built out of a pointer and a count.

$t1 comes out at 00000096, which is 150, and $t0 at 10010014, twenty bytes on and one word past the last element. The addi $t0, $t0, 4 is C's p++ written out, because C hides the size of what a pointer points at and assembly does not.

The other way to write that loop keeps $t0 at the base and computes sll and add on every pass, which is four instructions instead of one and gives you the index in a register. Use the pointer when you touch every element in order, and the index when you need the index itself, or when the loop jumps around the array the way a binary search does.

Branches and jumps are addressed differently

None of the three modes applies to the instruction stream. beq $t0, $t1, label holds a distance in words from the branch to the label, so it reaches about 32 kilobytes either way. j label and jal label hold the label's word address in 26 bits, which reaches anywhere in the same 256 megabyte quarter of memory. jr $t0 takes a full 32 bit address out of a register, which is how a jump table and a subroutine return both work.

You write a label in all four and the assembler works out which of those it needs.

Your turn

The four words are at 0x10010000. Leave numbers[2] in $t0, working the address out at run time from the index in $t1 rather than writing the offset 8 yourself.

Show solution

The second one wants numbers[i] = 99 with i already in $t1. The test starts it at 3, so the last of the four words is the one that changes and the array ends up as 10, 20, 30, 99.

Show solution