Names are an agreement, not hardware
The CPU numbers its registers $0 to $31. Nothing else. The names you see
in a dump, a0, sp, ra, come from the
.
It is a document, not a circuit. Your toolchain follows it because of the flag
-mabi=32. ABI means application binary interface: the rules two compiled
pieces of code need to work together.
Here is the whole table. Read it once now; chapter 6 uses every row.
The 32 registers and their o32 jobs
MIPS o32 ABI; names as GCC and objdump print them
| $0 | $zero | always 0 (hardware) | always 0 |
| $1 | $at | assembler temporary | gone (the assembler may use it) |
| $2 | $v0 | return value | gone (holds the result) |
| $3 | $v1 | return value, second word | gone |
| $4 | $a0 | argument 1 | gone |
| $5 | $a1 | argument 2 | gone |
| $6 | $a2 | argument 3 | gone |
| $7 | $a3 | argument 4 | gone |
| $8 | $t0 | temporary | gone (caller-saved) |
| $9 | $t1 | temporary | gone (caller-saved) |
| $10 | $t2 | temporary | gone (caller-saved) |
| $11 | $t3 | temporary | gone (caller-saved) |
| $12 | $t4 | temporary | gone (caller-saved) |
| $13 | $t5 | temporary | gone (caller-saved) |
| $14 | $t6 | temporary | gone (caller-saved) |
| $15 | $t7 | temporary | gone (caller-saved) |
| $16 | $s0 | saved | kept (callee-saved) |
| $17 | $s1 | saved | kept (callee-saved) |
| $18 | $s2 | saved | kept (callee-saved) |
| $19 | $s3 | saved | kept (callee-saved) |
| $20 | $s4 | saved | kept (callee-saved) |
| $21 | $s5 | saved | kept (callee-saved) |
| $22 | $s6 | saved | kept (callee-saved) |
| $23 | $s7 | saved | kept (callee-saved) |
| $24 | $t8 | temporary | gone (caller-saved) |
| $25 | $t9 | temporary | gone (caller-saved) |
| $26 | $k0 | reserved for the operating system | never use |
| $27 | $k1 | reserved for the operating system | never use |
| $28 | $gp | global pointer | kept |
| $29 | $sp | stack pointer | kept |
| $30 | $fp | frame pointer (objdump: s8) | kept (callee-saved) |
| $31 | $ra | return address (written by jal, hardware) | gone (jal overwrites it) |
The same table as colors. Each color is one group, and the colors stay the same in every register picture in this book.
The four groups, by color
Which column is all one color, and why?
- hardwired / reserved
- argument / result
- temporary (caller-saved)
- saved (callee-saved)
- pointer
- read this step
- written this step
What to notice
The middle two columns are pure: $8–$15 are all temporaries, $16–$23
all saved. The first and last columns mix the special registers with the
arguments and the pointers. The values shown are what crt0 leaves behind.
The four groups
Arguments and results. A function’s first four arguments arrive in the
$a0–$a3. Its answer goes back in the
$v0 and $v1.
Temporaries. $t0–$t9 are
.
A called function may overwrite them freely. If you still need one after a
call, save it yourself before the call.
Saved registers. $s0–$s7 are
.
A function that wants one must save the old value and put it back before
returning. Your values in them survive any call.
Pointers and specials.
is where jal saves the way back.
points at the top of the stack (§4.4).
points near the data.
is the base of the current function’s frame; objdump calls it s8.
belongs to the assembler (§9.1). The
$k0 and $k1 are for an operating system, and this book never touches them.
Watch crt0 fill the registers
Every program your core runs starts with the same 42 words: the start-up
code, crt0. It clears all 31 writable registers, builds $gp and $sp,
calls main, and sends main’s result to the exit door. Here it is, with a
tiny stand-in for main that returns 0.
The first 42 words of every program: crt0 fills the registers
What does the start-up code do with the registers before main runs?
program (address · word · instruction)
| PC → | 0xbfc00000 | 0x00000825 | _start:move $at,$zero |
| 0xbfc00004 | 0x00001025 | move $v0,$zero | |
| 0xbfc00008 | 0x00001825 | move $v1,$zero | |
| 0xbfc0000c | 0x00002025 | move $a0,$zero | |
| 0xbfc00010 | 0x00002825 | move $a1,$zero | |
| 0xbfc00014 | 0x00003025 | move $a2,$zero | |
| 0xbfc00018 | 0x00003825 | move $a3,$zero | |
| 0xbfc0001c | 0x00004025 | move $t0,$zero | |
| 0xbfc00020 | 0x00004825 | move $t1,$zero | |
| 0xbfc00024 | 0x00005025 | move $t2,$zero | |
| 0xbfc00028 | 0x00005825 | move $t3,$zero | |
| 0xbfc0002c | 0x00006025 | move $t4,$zero | |
| 0xbfc00030 | 0x00006825 | move $t5,$zero | |
| 0xbfc00034 | 0x00007025 | move $t6,$zero | |
| 0xbfc00038 | 0x00007825 | move $t7,$zero | |
| 0xbfc0003c | 0x00008025 | move $s0,$zero | |
| 0xbfc00040 | 0x00008825 | move $s1,$zero | |
| 0xbfc00044 | 0x00009025 | move $s2,$zero | |
| 0xbfc00048 | 0x00009825 | move $s3,$zero | |
| 0xbfc0004c | 0x0000a025 | move $s4,$zero | |
| 0xbfc00050 | 0x0000a825 | move $s5,$zero | |
| 0xbfc00054 | 0x0000b025 | move $s6,$zero | |
| 0xbfc00058 | 0x0000b825 | move $s7,$zero | |
| 0xbfc0005c | 0x0000c025 | move $t8,$zero | |
| 0xbfc00060 | 0x0000c825 | move $t9,$zero | |
| 0xbfc00064 | 0x0000d025 | move $k0,$zero | |
| 0xbfc00068 | 0x0000d825 | move $k1,$zero | |
| 0xbfc0006c | 0x0000e025 | move $gp,$zero | |
| 0xbfc00070 | 0x0000e825 | move $sp,$zero | |
| 0xbfc00074 | 0x0000f025 | move $fp,$zero | |
| 0xbfc00078 | 0x0000f825 | move $ra,$zero | |
| 0xbfc0007c | 0x3c1c1001 | lui $gp,0x1001 | |
| 0xbfc00080 | 0x279c8010 | addiu $gp,$gp,-32752 | |
| 0xbfc00084 | 0x3c1d1000 | lui $sp,0x1000 | |
| 0xbfc00088 | 0x27bd3ff0 | addiu $sp,$sp,16368 | |
| 0xbfc0008c | 0x0ff00063 | jal 0xbfc0018c | |
| 0xbfc00090 | 0x00000000 | nop | |
| 0xbfc00094 | 0x3c081000 | lui $t0,0x1000 | |
| 0xbfc00098 | 0x25083ff8 | addiu $t0,$t0,16376 | |
| 0xbfc0009c | 0xad020000 | sw $v0,0($t0) | |
| 0xbfc000a0 | 0x1000ffff | _park:b 0xbfc000a0 | |
| 0xbfc000a4 | 0x00000000 | nop | |
| 0xbfc0018c | 0x24020000 | main:li $v0,0 | |
| 0xbfc00190 | 0x03e00008 | jr $ra | |
| 0xbfc00194 | 0x00000000 | nop |
registers — read · written this tick
console (what the doors printed)
▮tick 0 of 43
Nothing has run yet. The PC (program counter) points at the first instruction.
Thirty-one move instructions write 0 into every register except $zero. Then gp and sp get real addresses in two steps each. jal writes $ra and jumps to main; jr $ra comes back; the last store hits the exit door.
What to notice
Thirty-one move instructions in a row, one per register, each one a green
box. Then $gp and $sp each take two ticks: lui for the top half,
addiu for the bottom. At jal main, $ra turns green with the address of
the instruction after the nop.
Scrub to the jal and decide:
`jal main` sits at 0xBFC0008C. After it runs, what does $ra hold?
Only two registers are special to the hardware
The CPU treats $8 and $16 the same way. It treats $a0 and $t0 the
same way. Exactly two registers have hardware meaning: $zero, which is
always 0, and $31, which jal writes. Everything else in the table is a
promise between compilers.
Go deeper: why gp takes two steps with a strange constant, and why crt0 zeroes everything
lui $gp,0x1001 then addiu $gp,$gp,-32752 gives 0x10010000 − 0x7FF0 =
0x10008010. Why not lui 0x1000 and add 0x8010? Because addiu sign-extends
its 16-bit constant, and 0x8010 has its top bit set, so it would count as
−32752. The assembler therefore puts one more in the lui and subtracts.
§3.2 makes this rule precise. _gp itself is a linker symbol: the middle of
the small-data area, so that one lw with a 16-bit offset reaches all of it.
Your builds use -G0, so nothing is placed there and $gp is set but unused.
Why does crt0 clear every register? On real hardware a register powers up
with an unknown value. In a simulator it powers up as X, “unknown”, and a
single X that reaches a compare poisons the whole run. Thirty-one move
instructions make the start state the same on every run, on every machine.
The three programs in carolyne_ref/programs/ all begin exactly this way.
Can you now explain it?
Register names are a table compilers agree on. Arguments in $a, results in $v, temporaries may be lost across a call, saved registers must survive one. Only $zero and $ra mean anything to the hardware.
If you remember only three things
- A name is a job, not a circuit. $8 and $t0 are one box. The convention says what to keep in it; the CPU does not check.
- Two kinds of promise across a call. Caller-saved ($t, $a, $v): gone unless the caller saves them. Callee-saved ($s, $sp, $fp, $gp): the function must restore them.
- jal writes $31 with PC + 8. PC is the program counter; the return address skips the delay slot. jr $ra goes back there. That is the whole hardware side of a function call.
Now try to explain this one yourself: GCC (the C compiler) at -O0 keeps
locals at fixed offsets from $fp. Why is $fp callee-saved?
What would go wrong if a called function changed it and did not restore it?
Go deeper: a model answer, once you have tried
The caller reads and writes its locals as lw/sw at offsets from $fp for
its whole life, including after every call it makes. If a callee left $fp
pointing somewhere else, every later access in the caller would hit the wrong
memory: reads return garbage, writes corrupt another frame. So the convention
makes $fp callee-saved: a function that needs its own frame pointer first
stores the caller’s $fp on the stack and restores it before jr $ra. §6.3
shows a real prologue and epilogue doing exactly this.
Check your understanding
0/5 answered1.A C function is called as `f(7, 9)`. Where does the compiler put the 7?
2.Which registers may a function overwrite without saving them first?
3.What does `jal main` at address 0xBFC0008C write, and where?
4.A function wants to use $s0 as a counter. What must it do?
5.The dump says `move s8,zero`. Which register is s8?
Next: Chapter 3 — adding and subtracting: add, addu, sub, subu.