Two registers, one test
A tests two registers:
| instruction | branches when | otherwise |
|---|---|---|
beq rs,rt,label | rs equals rt | continues |
bne rs,rt,label | rs differs from rt | continues |
b label | always (it is beq $zero,$zero,label) | — |
When the test holds, the branch is and the PC moves to the label. When it does not, the branch is not taken and the program continues. One rule from §1.2 still stands: the instruction right after the branch runs first, taken or not. §5.4 explains why.
Three branches, watched
The runner has three branches: a taken beq, a taken bne, and a beq
that is not taken. Each path prints a character, so the console shows which
paths ran.
Three branches: taken, taken, not taken
What does a branch do when its test holds, and when it does not?
program (address · word · instruction)
| PC → | 0xbfc00000 | 0x3c191000 | _start:lui $t9,0x1000 |
| 0xbfc00004 | 0x37390ff0 | ori $t9,$t9,0xff0 | |
| 0xbfc00008 | 0x24080003 | li $t0,3 | |
| 0xbfc0000c | 0x24090003 | li $t1,3 | |
| 0xbfc00010 | 0x11090003 | beq $t0,$t1,0xbfc00020 | |
| 0xbfc00014 | 0x00000000 | nop | |
| 0xbfc00018 | 0x240a004e | li $t2,78 | |
| 0xbfc0001c | 0xaf2a0000 | sw $t2,0($t9) | |
| 0xbfc00020 | 0x240a0059 | same:li $t2,89 | |
| 0xbfc00024 | 0xaf2a0000 | sw $t2,0($t9) | |
| 0xbfc00028 | 0x24090004 | li $t1,4 | |
| 0xbfc0002c | 0x15090003 | bne $t0,$t1,0xbfc0003c | |
| 0xbfc00030 | 0x00000000 | nop | |
| 0xbfc00034 | 0x240a003d | li $t2,61 | |
| 0xbfc00038 | 0xaf2a0000 | sw $t2,0($t9) | |
| 0xbfc0003c | 0x240a0021 | differ:li $t2,33 | |
| 0xbfc00040 | 0xaf2a0000 | sw $t2,0($t9) | |
| 0xbfc00044 | 0x11090003 | beq $t0,$t1,0xbfc00054 | |
| 0xbfc00048 | 0x00000000 | nop | |
| 0xbfc0004c | 0x240a002e | li $t2,46 | |
| 0xbfc00050 | 0xaf2a0000 | sw $t2,0($t9) | |
| 0xbfc00054 | 0xaf200008 | never:sw $zero,8($t9) |
registers — read · written this tick
console (what the doors printed)
▮tick 0 of 18
Nothing has run yet. The PC (program counter) points at the first instruction.
A taken branch (green chip) moves the PC arrow to the target after the orange delay slot has run, and the instructions in between are never executed. A not-taken branch (grey chip) runs its slot and then simply continues. The console shows which paths ran.
What to notice
At each branch a chip appears on the target row: green for taken, grey for
not taken. After a taken branch the orange slot runs, then the PC arrow
jumps and the skipped instructions never turn blue or green. After the
not-taken beq, the arrow just moves down.
Where the target comes from
How does a 32-bit word name a 32-bit target? It does not. The 16-bit field is
a
:
a distance in instructions. Here is the bnez that closes hello.c’s
loop, from the dump:
bnez v0,bfc001b8 from hello.c at -O0 (0x1440FFF2 at 0xBFC001EC)
Where is the target address in this word?
What to notice
Nowhere. The field holds 0xFFF2, which is −14. rs is v0, rt is
$zero (that is what makes bne print as bnez). The target
0xBFC001B8 that objdump printed was computed, from −14 and the branch’s
own address.
The is (branch address + 4) + (offset × 4). Watch the arithmetic one step at a time:
From the field 0xFFF2 to the target 0xBFC001B8
Why +4, and why ×4?
0x1440fff2 at 0xbfc001ec: bne $v0,$zero,0xbfc001b8
- 1. the 16-bit offset field, bits 15–0 of the word0x0000fff2 = 6552200000000000000001111111111110010
bne $v0,$zero,0xbfc001b8: the field holds 0xfff2. It is NOT an address; it counts instructions.
- 2. sign-extend it to 32 bits0xfffffff2 = -14
- 3. multiply by 4 (shift left by 2)0xffffffc8 = -56
- 4. PC + 4: the address of the delay slot0xbfc001f0
- 5. target = (PC + 4) + byte offset0xbfc001b8
- the field, and bits an extension or shift filled
- the branch/jump target
What to notice
Sign extension makes 0xFFF2 into −14: fourteen instructions backward.
Times 4 gives −56 bytes, because instructions are 4 bytes and the two low
bits are never stored. The distance is measured from the delay slot at
0xBFC001F0, not from the branch. Result: 0xBFC001B8.
Before reading on, decide:
`b bfc001e4` at 0xBFC001B0 has the field 0x000C. Check the target with the formula.
Relative, not absolute
Because the field is a distance, the same word means a different target at a
different address. Copy 0x1440FFF2 to another place and it still says “14
instructions back from my slot”. That is what lets the linker place a
function anywhere without rewriting its branches. It is also why a branch
reaches only ±128 KB: 32768 instructions each way, times 4 bytes. Anything
further needs a jump (§5.3).
Go deeper: why the target is relative to PC + 4, and how your core computes it
In the 1985 pipeline, by the time a branch is decoded the fetch unit has already added 4 to the PC to fetch the next word. That sum, PC + 4, is sitting on a wire for free. Adding the shifted offset to it rather than to the PC saved nothing in logic but matched what the hardware already had, and the delay slot made it the natural reference point anyway: the slot’s address is where the branch “is” when it takes effect.
Carolyne’s branch unit does the same sum. Its station entry carries pc and
npc, the next-PC the front end computed, and the body adds the sign-extended
offset times 4 to npc. The immediate rule imm_br places the 16 field bits
at bit 2 and sign-extends from bit 17, so the ×4 is a wiring choice, not an
adder. A taken branch then redirects fetch to the sum; a not-taken one lets
the front end’s own npc stand.
Can you now explain it?
A branch tests two registers and, if the test holds, moves the PC to a target. The word holds a signed distance in instructions from the delay slot; the target is branch + 4 + offset × 4. The next instruction runs either way.
If you remember only three things
- beq and bne: equal, or different. b is beq $zero,$zero. All of them run the slot first, then move the PC or not.
- The field is a distance, not an address. Sign-extend, times 4, add to branch + 4. The same word means a different target elsewhere, and that is by design.
- ±128 KB, then jump. 32768 instructions each way. Function calls and far targets use j and jal (§5.3).
Now try to explain this one yourself: suppose GCC had put one more
instruction into hello.c’s loop body. What changes in the word 0x1440FFF2
at the loop’s end? Why does the branch still find the top?
Go deeper: a model answer, once you have tried
The bnez moves four bytes down, and the top of the loop stays where it was
relative to the extra instruction, so the distance grows by one: the field
becomes −15, 0xFFF1, and the word 0x1440FFF1. The target is recomputed
from the new branch address and the new offset and lands on the same top.
The assembler does this arithmetic for every branch when it turns a label
into a field, which is why you never write offsets by hand.
Check your understanding
0/5 answered1.A `beq` at 0xBFC00040 is NOT taken. After the branch and its delay slot, where does the program continue?
2.How far can a conditional branch reach?
3.What does `beq $zero,$zero,label` do?
4.Why is the branch offset multiplied by 4 instead of being stored in bytes?
5.The same branch word 0x1440FFF2 is copied to a different address. What happens to its target?
Next: §5.2 — comparing with zero, and how a compiler writes if and while.