Six instructions below the word
A word is four bytes. To work with one or two of them, MIPS has six
instructions for
.
They use the same base + offset addressing as lw and sw.
| instruction | moves | into the register | address must be |
|---|---|---|---|
lb rt,off(rs) | 1 byte | sign-extended | any |
lbu rt,off(rs) | 1 byte | zero-extended | any |
lh rt,off(rs) | 2 bytes | sign-extended | even |
lhu rt,off(rs) | 2 bytes | zero-extended | even |
sb rt,off(rs) | the low byte of rt | — | any |
sh rt,off(rs) | the low halfword of rt | — | even |
The loads differ only in the extension (§3.2). The stores write one or two bytes and touch nothing else.
Which bytes, and how the register fills
Pick an instruction and move the window. The strip shows the bytes chosen and the 32-bit value that reaches the register.
Sub-word loads from eight bytes at 0x10000100
Load the byte 0x80 at 0x102 with lb, then with lbu. What changes?
bytes 0x10000102 … 0x10000102, lowest address first: [80]
lb puts 0xffffff80 = -128 in the register (sign-extended: the top bit of the loaded value fills the rest).
What to notice
lb at 0x102 loads 0x80 and fills the top 24 bits with ones: −128.
lbu at the same address fills them with zeros: 128. Same byte, two
registers. Try lh at 0x104: bytes FE FF become 0xFFFE, then −2.
Now a store. sb writes one byte of the register and leaves the rest of the
word exactly as it was.
sb $t0,1($t1): one byte of 0xAABBCCDD lands at 0x101
Which byte of the register goes to memory, and what happens to the other three cells of the word?
sb writes register value 0xaabbccdd, its low byte only, as bytes:
What to notice
Only the register’s low byte, DD, is stored, and only the cell at 0x101
changes. The word 0x11223344 becomes 0x1122DD44. A store to one byte is
not a store of the word with the other bytes zeroed.
Before reading on, decide:
The byte at 0x103 above is 0xFF. What do `lb` and `lbu` load from it?
Walking a string
A C string is bytes in memory ending with a zero byte. To print it, a program
loads one byte, prints it, moves the pointer up by one, and stops at the
zero. The runner does exactly that with lb.
Walking a string one byte at a time: lb, beqz, putchar, addiu
How does a program print a C string when it only knows where the string starts?
program (address · word · instruction)
| PC → | 0xbfc00000 | 0x3c191000 | _start:lui $t9,0x1000 |
| 0xbfc00004 | 0x37390ff0 | ori $t9,$t9,0xff0 | |
| 0xbfc00008 | 0x3c081000 | lui $t0,0x1000 | |
| 0xbfc0000c | 0x81090000 | loop:lb $t1,0($t0) | |
| 0xbfc00010 | 0x11200005 | beqz $t1,0xbfc00028 | |
| 0xbfc00014 | 0x00000000 | nop | |
| 0xbfc00018 | 0xaf290000 | sw $t1,0($t9) | |
| 0xbfc0001c | 0x25080001 | addiu $t0,$t0,1 | |
| 0xbfc00020 | 0x1000fffa | b 0xbfc0000c | |
| 0xbfc00024 | 0x00000000 | nop | |
| 0xbfc00028 | 0xaf200008 | done:sw $zero,8($t9) |
registers — read · written this tick
memory 0x10000000 … 0x10000007 — touched this tick
console (what the doors printed)
▮tick 0 of 28
Nothing has run yet. The PC (program counter) points at the first instruction.
Each loop turn loads ONE byte (one cyan cell), prints it, and moves $t0 up by 1, not by 4. The string ends with a zero byte; lb loads it, beqz sees 0, and the loop leaves.
What to notice
Each turn of the loop touches ONE cell of memory, and $t0 grows by 1, not
by 4. At the fourth lb the byte is 0; beqz sees a zero register and the
loop leaves. The zero byte is never printed.
What the compiler did
print_str in carolyne_io.h is the same loop in C. Here it is beside GCC’s
-O0 output. Click a C line to see its instructions.
print_str's while loop: C and the -O0 dump (text lives on the stack at 24(s8))
carolyne_ref/carolyne_io.h; hello_o0.dump bfc00138–bfc00168
1while (*text) {2 print_char(*text++);3}bfc00138b bfc0015cbfc0013cnopbfc00140lw v0,24(s8)bfc00144addiu v1,v0,1bfc00148sw v1,24(s8)bfc0014clb v0,0(v0)bfc00150move a0,v0bfc00154jal bfc000b0 <print_char>bfc00158nopbfc0015clw v0,24(s8)bfc00160lb v0,0(v0)bfc00164bnez v0,bfc00140bfc00168nopClick any highlighted line to see it light up in the other listing, and read what it does.
What to notice
Both *text reads are lb, not lbu: C’s char is signed on MIPS, so a
char becomes an int by sign extension. The bnez tests the loaded byte
directly against zero. text++ is an addiu by 1 and a store back to its
stack slot.
print_char(char value) shows the other side. It casts to unsigned char,
and GCC writes sb v0,8(s8) then lbu v1,8(s8) (at bfc000c0 and
bfc000c4): store the byte, reload it zero-extended. The cast is an lbu.
Go deeper: how your core does a byte store without a byte mask
Memory hands out whole words. To store one byte, Carolyne’s load/store unit first reads the containing word — from the data memory, or from a younger store of the same word still waiting in the store buffer — then replaces one byte of it and pushes the whole word to the buffer. That is a read-modify-write, and it is why the store buffer never needs a byte mask: every entry is a full word.
A byte load takes the same road backwards: read the containing word, then pick byte 0, 1, 2 or 3 by the low two address bits, then sign- or zero-extend. The extension is a mux between all-ones and all-zeros on bit 7 (or bit 15), the same trick §3.2 described for immediates. The helpers that do the picking and merging are shared between the MIPS and RISC-V descriptions, because little-endian byte lanes are the same in both.
Can you now explain it?
Six instructions reach below the word. Loads differ only by extension: lb and lh copy the sign, lbu and lhu fill zeros. Stores change one or two bytes and leave the rest. A string walk is lb, test, print, add 1.
If you remember only three things
- u means zero-extend, again. lbu and lhu give 0 to 255 and 0 to 65535; lb and lh give signed values. Pick by the C type.
- A sub-word store is surgical. sb writes one byte, sh two; the other bytes of the word are untouched. Little-endian says which byte of the register they are: the low one.
- Strings are bytes plus a zero. The pointer grows by 1 per character, and the loop ends when lb returns 0.
Now try to explain this one yourself: print_char(char value) stores its
argument with sb and reloads it with lbu. Why store and reload at all,
and why is the reload unsigned?
Go deeper: a model answer, once you have tried
At -O0 every parameter gets a home on the stack, so value is stored there
first (sb v0,8(s8), one byte, because it is a char). The function then
casts it to unsigned char before writing the door. Reading the byte back
zero-extended is exactly that cast: lbu v1,8(s8) turns any byte, even one
above 0x7F, into 0 to 255. Had the compiler used lb, a byte like 0xC9 would
have become −55, and the door would have received 0xFFFFFFC9. At -O2 the
store and reload vanish and the cast becomes an andi v0,v0,0xff (§3.3).
Check your understanding
0/5 answered1.The byte at the address is 0x80. What do `lb` and `lbu` load?
2.The word at 0x10000100 is 0x11223344. After `sb $t0,1($t1)` with $t1 = 0x10000100 and $t0 = 0xAABBCCDD, what is the word?
3.Why does the compiled string loop in print_str use `lb`, not `lbu`?
4.$t0 = 0x12345678. `sh $t0,2($t1)` with $t1 = 0x10000200 writes which bytes, where?
5.Bytes FE FF sit at an even address. What do `lhu` and `lh` load from it?