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?
-32752 (signed) · 0x8010
-32752 (signed) · 0xffff8010 · 4294934544 if read unsigned
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:
| rule | instructions | why |
|---|---|---|
| sign-extend | addi, addiu, slti, sltiu, and the offset of every load and store | arithmetic and addresses need negative constants |
| zero-extend | andi, ori, xori | a logic constant is a bit mask, not a number |
| place in the top half | lui | the 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 → | 0xbfc00000 | 0x3c191000 | _start:lui $t9,0x1000 |
| 0xbfc00004 | 0x37390ff0 | ori $t9,$t9,0xff0 | |
| 0xbfc00008 | 0x2408ffff | li $t0,-1 | |
| 0xbfc0000c | 0xaf280004 | sw $t0,4($t9) | |
| 0xbfc00010 | 0x240d0020 | li $t5,32 | |
| 0xbfc00014 | 0xaf2d0000 | sw $t5,0($t9) | |
| 0xbfc00018 | 0x3409ffff | li $t1,0xffff | |
| 0xbfc0001c | 0xaf290004 | sw $t1,4($t9) | |
| 0xbfc00020 | 0x3c0a1000 | lui $t2,0x1000 | |
| 0xbfc00024 | 0x354a3ff0 | ori $t2,$t2,0x3ff0 | |
| 0xbfc00028 | 0x3c0b1001 | lui $t3,0x1001 | |
| 0xbfc0002c | 0x256b8010 | addiu $t3,$t3,-32752 | |
| 0xbfc00030 | 0x240c0000 | li $t4,0 | |
| 0xbfc00034 | 0xaf2c0008 | sw $t4,8($t9) |
registers — read · written this tick
console (what the doors printed)
▮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?
lui $v0,0x1000 then ori $v0,$v0,0x3ff0 = 0x10003FF0
Why does OR work here without any carry?
268435456 (signed) · 0x10000000
16368 (signed) · 0x00003ff0
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?
268500992 (signed) · 0x10010000
-32752 (signed) · 0xffff8010 · 4294934544 if read unsigned
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:
| step | value |
|---|---|
%lo(x) = low 16 bits, read signed | 0x8010 → −32752 |
x − %lo(x) | 0x10008010 + 0x7FF0 = 0x10010000 |
%hi(x) = that, shifted right 16 | 0x1001 |
check: lui %hi + addiu %lo | 0x10010000 − 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 write | the assembler emits | example in the dumps |
|---|---|---|
li a0,10 | addiu a0,zero,10 | hello_o0.dump bfc001cc |
li t0,0xffff | ori t0,zero,0xffff | fits zero-extended, so one word |
li t0,0x10003ff0 | lui t0,0x1000 + ori t0,t0,0x3ff0 | two words |
la gp,_gp | lui 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
- 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.
- lui + ori is the plain pair. Top half, then bottom half, no carry. That is what li expands to for a big constant.
- 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 answered1.Both `addiu $t0,$zero,0xFFFF` and `ori $t1,$zero,0xFFFF` carry the same 16 bits. What do $t0 and $t1 hold?
2.How do you put 0x12345678 into $t0?
3.To build 0x10008010 GCC emits `lui gp,0x1001` — not 0x1000. Why?
4.Which instructions zero-extend their 16-bit constant?
5.After `lui $t0,0xABCD`, what is in the bottom 16 bits of $t0?