Skip to content
Contents

Bytes and halfwords: lb, lbu, lh, lhu, sb, sh

Section 4.3 7 min read

  • sub-word access
  • lb versus lbu, lh versus lhu
  • sb and sh leave the other bytes alone
  • walking a C string

Checked against: MIPS32 Vol. II: LB, LBU, LH, LHU, SB, SH carolyne_ref/carolyne_io.h (print_str, print_char) carolyne_ref/dumps/hello_o0.dump (bfc00138–bfc00168, bfc000c0–bfc000c4) carolyne_ref/isa_mips/exec_unit_ls.py

Before you start

  • §4.1: bytes, little-endian, alignment
  • §3.2: sign extension and zero extension

By the end, you can

  • Pick the right load for a signed or unsigned char and halfword
  • Say which bytes of a word sb and sh change, and which they do not
  • Read a compiled string loop and name the lb, the test and the step
  • Explain why C's char is signed on MIPS and what that costs

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.

instructionmovesinto the registeraddress must be
lb rt,off(rs)1 bytesign-extendedany
lbu rt,off(rs)1 bytezero-extendedany
lh rt,off(rs)2 bytessign-extendedeven
lhu rt,off(rs)2 byteszero-extendedeven
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?

480100
691
802
ff3
fe0104
ff5
006
007

bytes 0x10000102 … 0x10000102, lowest address first: [80]

assembled, most significant on the left:80byte 0= 0x80

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?

440100
331
222
113

sb writes register value 0xaabbccdd, its low byte only, as bytes:

dd→ 0x10000101the low byte goes to the lowest address (little-endian)

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 →0xbfc000000x3c191000_start:lui $t9,0x1000
0xbfc000040x37390ff0ori $t9,$t9,0xff0
0xbfc000080x3c081000lui $t0,0x1000
0xbfc0000c0x81090000loop:lb $t1,0($t0)
0xbfc000100x11200005beqz $t1,0xbfc00028
0xbfc000140x00000000nop
0xbfc000180xaf290000sw $t1,0($t9)
0xbfc0001c0x25080001addiu $t0,$t0,1
0xbfc000200x1000fffab 0xbfc0000c
0xbfc000240x00000000nop
0xbfc000280xaf200008done:sw $zero,8($t9)

registers — read · written this tick

HI 0x00000000LO 0x00000000PC 0xbfc00000

memory 0x10000000 … 0x10000007 — touched this tick

480000
691
0a2
003
000004
005
006
007

console (what the doors printed)

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

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

carolyne_io.h
1while (*text) {
2 print_char(*text++);
3}
MIPS (-O0)
bfc00138b bfc0015c
bfc0013cnop
bfc00140lw v0,24(s8)
bfc00144addiu v1,v0,1
bfc00148sw v1,24(s8)
bfc0014clb v0,0(v0)
bfc00150move a0,v0
bfc00154jal bfc000b0 <print_char>
bfc00158nop
bfc0015clw v0,24(s8)
bfc00160lb v0,0(v0)
bfc00164bnez v0,bfc00140
bfc00168nop

Click 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

  1. 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.
  2. 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.
  3. 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 answered
  1. 1.The byte at the address is 0x80. What do `lb` and `lbu` load?

  2. 2.The word at 0x10000100 is 0x11223344. After `sb $t0,1($t1)` with $t1 = 0x10000100 and $t0 = 0xAABBCCDD, what is the word?

  3. 3.Why does the compiled string loop in print_str use `lb`, not `lbu`?

  4. 4.$t0 = 0x12345678. `sh $t0,2($t1)` with $t1 = 0x10000200 writes which bytes, where?

  5. 5.Bytes FE FF sit at an even address. What do `lhu` and `lh` load from it?


Next: §4.4 — the stack: memory that grows downward.

Where this comes back

One later section builds on this one.