Skip to content
Contents

Load and store: lw, sw and base + offset

Section 4.2 10 min read

  • base + offset addressing
  • load: memory to register
  • store: register to memory; the source is written first
  • arrays without a scaled index
  • globals through lui + offset

Checked against: MIPS32 Vol. II: LW, SW carolyne_ref/dumps/hello_o0.dump (bfc00190, bfc001b8, bfc001e0) carolyne_ref/dumps/hello_o2.dump (bfc000bc, bfc000c8) carolyne_ref/dumps/fib_o0.dump (bfc001f4) carolyne_ref/isa_mips/exec_unit_ls.py

Before you start

  • §4.1: addresses, words, alignment
  • §3.2: a 16-bit immediate is sign-extended

By the end, you can

  • Compute the address of any lw or sw from the base register and the offset
  • Say which operand of sw is the data and which is the base
  • Load an array element with sll, addu and lw
  • Read a global-variable access written as lui plus an offset

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”.

instructiondoeswhich operand is what
lw rt,off(rs)rt = the word at rs + offrt is written; rs is the base
sw rt,off(rs)the word at rs + off = rtrt 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?

3126
opcode6b
101011
43
2521
rs5b
11101
$sp ($29)
2016
rt5b
11111
$ra ($31)
150
imm16b
0000000000011100
28 (0x001c)
binary10101111101111110000000000011100
hex0xafbf001cone hex digit = four bits; a digit that spans two fields has two colors
asmsw $ra,28($sp)I-format
Hover or focus a field to see its bits, value and role.

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. 1. the 16-bit offset field, bits 15–0 of the word0x0000001c = 28
    00000000000000000000000000011100

    sw $ra,28($sp): the constant inside the instruction is 0x001c.

  2. 2. sign-extend it to 32 bits0x0000001c = 28
  3. 3. the base register $sp holds0x10003fd0
  4. 4. address = $sp + offset0x10003fec
cycle 0/31 tick = 1 arithmetic step
  • 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. 1. the 16-bit offset field, bits 15–0 of the word0x0000fff0 = 65520
    00000000000000001111111111110000

    sw $t4,-16($t0): the constant inside the instruction is 0xfff0.

  2. 2. sign-extend it to 32 bits0xfffffff0 = -16
  3. 3. the base register $t0 holds0x10000ff0
  4. 4. address = $t0 + offset0x10000fe0
cycle 0/31 tick = 1 arithmetic step
  • 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 →0xbfc000000x3c081000_start:lui $t0,0x1000
0xbfc000040x24091234li $t1,4660
0xbfc000080x240affffli $t2,-1
0xbfc0000c0xad090000sw $t1,0($t0)
0xbfc000100xad0a0004sw $t2,4($t0)
0xbfc000140x250b0008addiu $t3,$t0,8
0xbfc000180x8d6cfffclw $t4,-4($t3)
0xbfc0001c0x8d6dfff8lw $t5,-8($t3)
0xbfc000200x3c191000lui $t9,0x1000
0xbfc000240x37390ff0ori $t9,$t9,0xff0
0xbfc000280xaf2d0004sw $t5,4($t9)
0xbfc0002c0xaf200008sw $zero,8($t9)

registers — read · written this tick

HI 0x00000000LO 0x00000000PC 0xbfc00000

memory 0x10000000 … 0x1000000f — touched this tick

000000
001
002
003
000004
005
006
007
000008
009
00a
00b
00000c
00d
00e
00f

console (what the doors printed)

▮
cycle 0/121 tick = 1 instruction (not a cycle)

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

  1. The word holds a distance, never an address. sw ra,28(sp) lands 28 bytes above wherever $sp points. Same code, any stack.
  2. First operand: the register. Second: the address. For lw the register is written; for sw it is read. Memory is sw's destination.
  3. 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 answered
  1. 1.$t0 holds 0x10000004, and the word at 0x10000004 is 0x00000030. What does `lw $t0,0($t0)` do?

  2. 2.Which register does `sw $t0,8($t1)` write?

  3. 3.How far can a load or store reach from its base register?

  4. 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. 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?


Next: §4.3 — bytes and halfwords: lb, lbu, lh, lhu, sb, sh.

Where this comes back

One later section builds on this one.