One addressing mode
MIPS names a memory location in exactly one way:
.
The address is the value of a register, the base, plus a constant, the
offset. In assembly the offset comes first and the base sits in brackets:
28($sp) means “$sp plus 28”.
| instruction | does | which operand is what |
|---|---|---|
lw rt,off(rs) | rt = the word at rs + off | rt is written; rs is the base |
sw rt,off(rs) | the word at rs + off = rt | rt is read; rs is the base |
The offset is 16 bits, sign-extended (§3.2). So a base reaches 32 KB below
itself and 32 KB above. That is why -16($t0) in the smoke program worked,
and why every stack access can use $sp as the base.
Here is a store from the start of main in hello.c:
sw ra,28(sp) from hello.c at -O0 (0xAFBF001C at 0xBFC00190)
Which field is the data, which is the base, and where is the 28?
What to notice
I-format. rs = $sp is the base. rt = $ra is the data being stored: a
source here, not a destination. The 16-bit field holds 28. Nothing in the
word is an address; the address only exists once $sp is added.
How the address forms, step by step
The widget below does the arithmetic one step per tick. $sp has the value
it has at that point in main, after addiu sp,sp,-32.
sw ra,28(sp): from the field to the address
Why is the result a multiple of 4?
0xafbf001c at 0xbfc00190: sw $ra,28($sp)
- 1. the 16-bit offset field, bits 15–0 of the word0x0000001c = 2800000000000000000000000000011100
sw $ra,28($sp): the constant inside the instruction is 0x001c.
- 2. sign-extend it to 32 bits0x0000001c = 28
- 3. the base register $sp holds0x10003fd0
- 4. address = $sp + offset0x10003fec
- the field, and bits an extension or shift filled
- the memory address
Now a negative offset, from the smoke program of §1.2:
sw $t4,-16($t0): a negative offset reaches below the base
What does the field 0xFFF0 turn into?
0xad0cfff0 at 0xbfc00034: sw $t4,-16($t0)
- 1. the 16-bit offset field, bits 15–0 of the word0x0000fff0 = 6552000000000000000001111111111110000
sw $t4,-16($t0): the constant inside the instruction is 0xfff0.
- 2. sign-extend it to 32 bits0xfffffff0 = -16
- 3. the base register $t0 holds0x10000ff0
- 4. address = $t0 + offset0x10000fe0
- the field, and bits an extension or shift filled
- the memory address
What to notice
The field 0xFFF0 is not 65520. Sign-extended, it is −16, and the address
lands 16 bytes below $t0. The offset is a signed number in both figures;
only bit 15 tells them apart.
Load and store, watched
A writes a register and leaves memory alone. A writes memory and leaves every register alone. The runner stores two words, then reads them back through a second base and negative offsets.
Store two words, load one back through a negative offset
How does an instruction say which memory word it wants?
program (address · word · instruction)
| PC → | 0xbfc00000 | 0x3c081000 | _start:lui $t0,0x1000 |
| 0xbfc00004 | 0x24091234 | li $t1,4660 | |
| 0xbfc00008 | 0x240affff | li $t2,-1 | |
| 0xbfc0000c | 0xad090000 | sw $t1,0($t0) | |
| 0xbfc00010 | 0xad0a0004 | sw $t2,4($t0) | |
| 0xbfc00014 | 0x250b0008 | addiu $t3,$t0,8 | |
| 0xbfc00018 | 0x8d6cfffc | lw $t4,-4($t3) | |
| 0xbfc0001c | 0x8d6dfff8 | lw $t5,-8($t3) | |
| 0xbfc00020 | 0x3c191000 | lui $t9,0x1000 | |
| 0xbfc00024 | 0x37390ff0 | ori $t9,$t9,0xff0 | |
| 0xbfc00028 | 0xaf2d0004 | sw $t5,4($t9) | |
| 0xbfc0002c | 0xaf200008 | sw $zero,8($t9) |
registers — read · written this tick
memory 0x10000000 … 0x1000000f — touched this tick
console (what the doors printed)
▮tick 0 of 12
Nothing has run yet. The PC (program counter) points at the first instruction.
sw changes memory (cyan) and no register. lw changes a register (green) and no memory. Ticks 7 and 8 read through $t3 with negative offsets: the same words, named from a different base.
What to notice
Ticks 4 and 5 turn memory cyan and no register green. Ticks 7 and 8 turn a
register green and touch memory only to read it. Tick 6 built a second base
8 bytes up, so the same two words are now -4($t3) and -8($t3).
Before reading on, decide:
The 16-bit field of `sw ra,28(sp)` holds 0x001C. Is 0x001C the address of the stored word?
Arrays: no scaled index
Many CPUs can write “base + 4 × index” inside one load. MIPS cannot. The offset is a constant, so an array element takes three instructions:
sll $t2, $t1, 2 # $t2 = i * 4: each word is 4 bytes
addu $t2, $t0, $t2 # $t2 = base + i * 4
lw $t3, 0($t2) # a[i]
That sll v0,v0,2 you met in §3.3 (fib_o0.dump bfc001f4) is exactly the
first line of this pattern. The multiply by 4 is a shift and the add is an
addu. The load then uses offset 0.
Globals and the doors: lui plus an offset
A global variable has a fixed address, so no register holds it yet. At -O0
the compiler builds the address with lui + ori and loads with offset 0.
At -O2 it saves an instruction. From hello_o2.dump:
bfc000bc: lui a0,0x1000 # a0 = 0x10000000
bfc000c8: sw v0,16368(a0) # 0x10000000 + 0x3FF0 = 0x10003FF0: the door
The offset does the ori’s job. 16368 is 0x3FF0, which fits in the signed
16-bit field. So lui plus a store reaches the door in two instructions
instead of three. This is %hi/%lo from §3.2 with the %lo half inside the
store. When the low half has bit 15 set, the lui carries the same +1.
Go deeper: the load delay slot that MIPS I had, and how your core handles loads
A load takes longer than an add: the address must be formed and the memory asked. The original MIPS I (1985) refused to stall the pipeline for it. Instead the ISA said: the instruction right after a load may not use the loaded value. That was the load delay slot, a second “no interlock” rule beside the branch delay slot. MIPS II (1990) dropped it and added the interlock: the hardware now waits when a value is not ready. MIPS32 keeps only the branch delay slot, so nothing in this book depends on the load one.
Carolyne runs loads and stores on their own in-order station, through a two-stage unit. Stage one forms the address and reads the containing word (or takes it from a younger store in the store buffer, §9.3); stage two extracts the bytes, sign- or zero-extends, and writes the register. A later instruction that needs the value simply waits in its station until the write lands, which is the interlock MIPS II promised, done by renaming.
Can you now explain it?
One addressing mode: base register plus a signed 16-bit offset. lw copies memory into rt; sw copies rt into memory, and rt is written first in the assembly either way. Arrays add a shift and an add; globals put the low half of the address in the offset.
If you remember only three things
- The word holds a distance, never an address. sw ra,28(sp) lands 28 bytes above wherever $sp points. Same code, any stack.
- First operand: the register. Second: the address. For lw the register is written; for sw it is read. Memory is sw's destination.
- Everything else is built from this. Arrays: sll, addu, lw with 0. Globals and doors: lui, then the offset carries the low half.
Now try to explain this one yourself: at -O0 the loop counter i of
hello.c lives at 16(s8). Every use of it is a lw or a sw. Why does the
compiler not keep i in a register? What changes at -O2?
Go deeper: a model answer, once you have tried
At -O0 the compiler gives every C variable a home on the stack and translates
each statement on its own: read the variable, compute, write it back. That is
slow but easy to debug, because i is always at 16(s8) when a debugger
looks. At -O2 the compiler keeps i in a register for the whole loop and
touches memory only when it must. The hello_o2.dump loop has no 16(s8) at
all; the counter lives in a register and the loads and stores are gone.
Chapter 9 compares the two builds side by side.
Check your understanding
0/5 answered1.$t0 holds 0x10000004, and the word at 0x10000004 is 0x00000030. What does `lw $t0,0($t0)` do?
2.Which register does `sw $t0,8($t1)` write?
3.How far can a load or store reach from its base register?
4.The base of a word array is in $t0 and the index i is in $t1. How does the compiler load a[i]?
5.At -O2, GCC wrote `lui a0,0x1000` then `sw v0,16368(a0)` to reach the putchar door at 0x10003FF0. Why not lui + ori + sw with offset 0?