Skip to content
Contents

Immediates: the 16-bit number inside an instruction

Section 3.2 10 min read

  • sign extension
  • zero extension
  • lui and the two halves
  • %hi / %lo and the +1
  • li and la

Checked against: MIPS32 Vol. II: ADDIU, ORI, ANDI, XORI, LUI carolyne_ref/isa_mips/imm.py (imm_s16, imm_lui) carolyne_ref/isa_mips/operand.py (OPR_IMM_*) carolyne_ref/dumps/hello_o0.dump (bfc0007c, bfc000c8, bfc001cc)

Before you start

  • §1.3: the I-format has a 16-bit immediate
  • §3.1: two's complement; bit 15 of a 16-bit value is its sign bit

By the end, you can

  • Widen a 16-bit constant both ways and say which instructions use which
  • Build any 32-bit constant with lui and ori
  • Explain why lui sometimes carries one more than the target's top half
  • Expand li and la by hand

Sixteen bits, two ways to widen

The immediate field of an I-format word is 16 bits. A register is 32. Before the value can be used, the CPU must fill the other 16 bits. There are two rules.

copies bit 15 into bits 31 to 16. A 16-bit −1 (0xFFFF) becomes a 32-bit −1 (0xFFFFFFFF). The value is kept. fills bits 31 to 16 with zeros. 0xFFFF becomes 0x0000FFFF, which is 65535.

One 16-bit field, widened both ways

Which bit decides whether the two results differ?

bit 31bit 27bit 23bit 19bit 15bit 11bit 7bit 3
the 16-bit value inside the instructionbit 15 is the sign bit

-32752 (signed) · 0x8010

sign-extended to 32 bits (addiu, slti, loads, stores)copies of the sign bit fill the top

-32752 (signed) · 0xffff8010 · 4294934544 if read unsigned

zero-extended to 32 bits (andi, ori, xori)zeros fill the top

32784 (unsigned) · 0x00008010

  • the sign bit
  • the bits the extension adds
  • the bits copied from the input

Click any input bit to flip it.

What to notice

Bit 15 has a thick border: it is the sign bit of the 16-bit value. While it is 0, both results are the same. Click it on, and the sign-extended row fills its top half with ones while the zero-extended row stays small.

Which rule an instruction uses is fixed by the instruction, not by you:

ruleinstructionswhy
sign-extendaddi, addiu, slti, sltiu, and the offset of every load and storearithmetic and addresses need negative constants
zero-extendandi, ori, xoria logic constant is a bit mask, not a number
place in the top halfluithe other half of a 32-bit constant

Carolyne’s description says the same in code: addiu and the loads carry the rule imm_s16 (place bits 15–0, then sign-extend), ori carries no rule and gets the default zero fill, and lui carries imm_lui, which places the 16 bits at bit 16.

The same 0xFFFF, two results

Watch the two rules produce −1 and 65535 from identical bits. The runner also builds two 32-bit constants, and pauses before the strange one.

The same 16 bits, 0xFFFF, widened two ways — and the two lui pairs

Why does addiu with 0xFFFF give −1 while ori with 0xFFFF gives 65535?

program (address · word · instruction)

PC →0xbfc000000x3c191000_start:lui $t9,0x1000
0xbfc000040x37390ff0ori $t9,$t9,0xff0
0xbfc000080x2408ffffli $t0,-1
0xbfc0000c0xaf280004sw $t0,4($t9)
0xbfc000100x240d0020li $t5,32
0xbfc000140xaf2d0000sw $t5,0($t9)
0xbfc000180x3409ffffli $t1,0xffff
0xbfc0001c0xaf290004sw $t1,4($t9)
0xbfc000200x3c0a1000lui $t2,0x1000
0xbfc000240x354a3ff0ori $t2,$t2,0x3ff0
0xbfc000280x3c0b1001lui $t3,0x1001
0xbfc0002c0x256b8010addiu $t3,$t3,-32752
0xbfc000300x240c0000li $t4,0
0xbfc000340xaf2c0008sw $t4,8($t9)

registers — read · written this tick

HI 0x00000000LO 0x00000000PC 0xbfc00000

console (what the doors printed)

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

tick 0 of 14

Nothing has run yet. The PC (program counter) points at the first instruction.

Tick 3 (addiu) sign-extends 0xFFFF to 0xFFFFFFFF = −1; tick 7 (ori) zero-extends it to 0x0000FFFF = 65535. Ticks 9–10 build 0x10003FF0 with lui + ori. Ticks 11–12 build 0x10008010 with lui + addiu: the lui half is 0x1001, one more than the top half of the target, because the addiu half is negative.

What to notice

Tick 3 and tick 7 read the same 16 bits. addiu turns them into −1; ori turns them into 65535. Then two pairs: lui + ori builds 0x10003FF0 plainly, and lui + addiu builds 0x10008010 with a lui of 0x1001.

Before reading on, decide:

The immediate field of an addiu holds 0x8000. $t1 is 0. What does `addiu $t0,$t1,imm` put in $t0?

Building a 32-bit constant: lui, then ori

lui rt,imm puts the 16 bits in the top half of rt and zeros the bottom half. ori then fills the bottom half. Here is the pair GCC emitted for the door address in hello.c:

lui v0,0x1000 from hello.c (0x3C021000 at 0xBFC000C8)

Where do the 16 bits of 0x1000 land in the register?

3126
opcode6b
001111
15
2521
rs5b
00000
0
2016
rt5b
00010
$v0 ($2)
150
imm16b
0001000000000000
0x1000 (goes to the top half)
binary00111100000000100001000000000000
hex0x3c021000one hex digit = four bits; a digit that spans two fields has two colors
asmlui $v0,0x1000I-format
Hover or focus a field to see its bits, value and role.

lui $v0,0x1000 then ori $v0,$v0,0x3ff0 = 0x10003FF0

Why does OR work here without any carry?

bit 31bit 27bit 23bit 19bit 15bit 11bit 7bit 3
1. lui: put 0x1000 in the top halfthe low half is zero

268435456 (signed) · 0x10000000

2. ori: the 16-bit value 0x3ff0 (zero-extended)

16368 (signed) · 0x00003ff0

result: OR the two

268451824 (signed) · 0x10003ff0

  • bits that are set
  • bits that stay zero

Click any input bit to flip it.

What to notice

After lui, the bottom half is all zeros. OR-ing a value into zeros just copies it. Nothing carries, nothing is signed. Any 32-bit constant can be built this way, in exactly two instructions.

When the low half is negative: the +1

Now the pair that builds $gp in crt0 (§2.2): lui gp,0x1001 then addiu gp,gp,-32752. The target is 0x10008010, whose top half is 0x1000. Yet lui loads 0x1001. Why?

lui $gp,0x1001 then addiu $gp,$gp,-32752 = 0x10008010

Where did the extra 1 in 0x1001 come from?

bit 31bit 27bit 23bit 19bit 15bit 11bit 7bit 3
1. lui: put 0x1001 in the top halfthe low half is zero

268500992 (signed) · 0x10010000

2. addiu: the 16-bit value 0x8010 = -32752 after sign extension

-32752 (signed) · 0xffff8010 · 4294934544 if read unsigned

result: add the two

268468240 (signed) · 0x10008010

The low half 0x8010 has its top bit set, so addiu reads it as the negative number -32752. Adding it subtracts 1 from the top half: 0x10010000 + (-32752) = 0x10008010. That is why the assembler puts 0x1001 in the lui — one more than the top half of the target.

  • bits that are set
  • bits that stay zero

Click any input bit to flip it.

What to notice

The low half 0x8010 has bit 15 set. addiu sign-extends it to −32752 and subtracts. To land on 0x10008010, the top half must start one higher, at 0x1001. The red box states the arithmetic.

This is the rule. Written out for x = 0x10008010:

stepvalue
%lo(x) = low 16 bits, read signed0x8010 → −32752
x − %lo(x)0x10008010 + 0x7FF0 = 0x10010000
%hi(x) = that, shifted right 160x1001
check: lui %hi + addiu %lo0x10010000 − 32752 = 0x10008010 ✓

Why use addiu at all, when ori needs no correction? Because the low half is often an offset that later instructions add to. lw $t0,%lo(x)($gp) sign-extends its offset, so the pair must be built the same way. You will see this in every la and in every global variable access.

li and la are pseudo-instructions

li rt,value (“load immediate”) and la rt,label (“load address”) are not instructions. The assembler expands them:

you writethe assembler emitsexample in the dumps
li a0,10addiu a0,zero,10hello_o0.dump bfc001cc
li t0,0xffffori t0,zero,0xfffffits zero-extended, so one word
li t0,0x10003ff0lui t0,0x1000 + ori t0,t0,0x3ff0two words
la gp,_gplui gp,%hi(_gp) + addiu gp,gp,%lo(_gp)bfc0007c, with the +1

The rule of thumb: a value that fits in 16 signed bits is one addiu; one that fits in 16 unsigned bits is one ori; anything else is a lui pair.

Go deeper: why the two rules exist, and how Carolyne encodes them

The split is by purpose. Arithmetic constants and address offsets are numbers, and numbers need to be negative: addiu sp,sp,-32, lw $t0,-16($t0). Logic constants are masks, and a mask is a bit pattern: andi v0,v0,0xff keeps the low byte and must not turn into 0xFFFFFFFF because bit 15 happened to be set. Two rules, each right for its users.

In Carolyne’s description each immediate operand names its widening rule as a small function over the instruction word. imm_s16 places bits 15–0 at bit 0 and calls sign_extend(from_bit=15); imm_lui places bits 15–0 at bit 16 and stops; the zero-extended operands (andi, ori, xori, the shift amount) carry no rule and receive the default, “place and leave the rest zero”. The sign fill itself is a mux between all-ones and all-zeros on the sign bit, so the hardware is a fan-out, not an adder.

Can you now explain it?

Sixteen bits, widened by rule: arithmetic and offsets copy the sign bit, logic fills zeros, lui fills the top half. A 32-bit constant is lui plus ori, or lui plus addiu with a +1 when the low half is negative.

If you remember only three things

  1. The instruction picks the rule, not the programmer. addiu, slti and every load/store offset sign-extend; andi, ori, xori zero-extend. 0xFFFF is −1 to one group and 65535 to the other.
  2. lui + ori is the plain pair. Top half, then bottom half, no carry. That is what li expands to for a big constant.
  3. lui + addiu needs the %hi/%lo correction. When the low half's bit 15 is set, addiu subtracts, so lui carries one more. That is what la expands to.

Now try to explain this one yourself: lui $t0,0x1000 then ori $t0,$t0,0xFFF0 gives 0x1000FFF0. Swap ori for addiu. Why is the result not 0x1000FFF0, and what is it?

Go deeper: a model answer, once you have tried

ori adds the pattern 0x0000FFF0 to 0x10000000: 0x1000FFF0. addiu reads 0xFFF0 as a signed number, −16, and adds it: 0x10000000 − 16 = 0x0FFFFFF0. To reach 0x1000FFF0 with addiu, the assembler would write lui $t0,0x1001 then addiu $t0,$t0,-16, which is exactly %hi = 0x1001 and %lo = −16. Same target, two different pairs, and only the %hi/%lo rule tells you which lui to write.

Check your understanding

0/5 answered
  1. 1.Both `addiu $t0,$zero,0xFFFF` and `ori $t1,$zero,0xFFFF` carry the same 16 bits. What do $t0 and $t1 hold?

  2. 2.How do you put 0x12345678 into $t0?

  3. 3.To build 0x10008010 GCC emits `lui gp,0x1001` — not 0x1000. Why?

  4. 4.Which instructions zero-extend their 16-bit constant?

  5. 5.After `lui $t0,0xABCD`, what is in the bottom 16 bits of $t0?


Next: §3.3 — logic, shifts and rotates.

Where this comes back

10 later sections build on this one.