A call may destroy a register
When jal returns, the callee has run, and it used registers to do its
work. Any register it wrote is now
from the caller’s point of view. The convention decides which registers a
callee may clobber freely and which it must restore. Here is the table from
§2.2 again, read for its last column:
The 32 registers: which survive a call
MIPS o32 ABI; the same table as §2.2
| $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) |
What to notice
Read the last column only. “Kept” registers come back as they were: $s0–$s7,
$gp, $sp, $fp. Every “gone” register may hold anything after a call,
including $ra, which the call itself overwrote.
Two duties follow:
| group | who saves | when |
|---|---|---|
caller-saved $t0–$t9, $a0–$a3, $v0–$v1, $at, $ra | the caller, if it still needs the value | before the jal, into its own frame |
callee-saved $s0–$s7, $sp, $fp, $gp | the callee, if it uses the register | in its prologue; restored in its epilogue |
The word saved names the side that pays. A callee saves the $s
registers it touches; a caller saves the $t values it wants back.
Watch the rule break, then hold
The runner keeps a value across a call twice: first in $t0, then in
$s0. It pauses after the first call to ask what will print.
A value in $t0 across a call is lost; the same value in $s0 survives
After a call returns, which registers still hold what they held before?
program (address · word · instruction)
| PC → | 0xbfc00000 | 0x3c191000 | _start:lui $t9,0x1000 |
| 0xbfc00004 | 0x37390ff0 | ori $t9,$t9,0xff0 | |
| 0xbfc00008 | 0x3c1d1000 | lui $sp,0x1000 | |
| 0xbfc0000c | 0x27bd0fe0 | addiu $sp,$sp,4064 | |
| 0xbfc00010 | 0x24080007 | li $t0,7 | |
| 0xbfc00014 | 0x0ff0000f | jal 0xbfc0003c | |
| 0xbfc00018 | 0x00000000 | nop | |
| 0xbfc0001c | 0xaf280004 | sw $t0,4($t9) | |
| 0xbfc00020 | 0x24180020 | li $t8,32 | |
| 0xbfc00024 | 0xaf380000 | sw $t8,0($t9) | |
| 0xbfc00028 | 0x24100007 | li $s0,7 | |
| 0xbfc0002c | 0x0ff00012 | jal 0xbfc00048 | |
| 0xbfc00030 | 0x00000000 | nop | |
| 0xbfc00034 | 0xaf300004 | sw $s0,4($t9) | |
| 0xbfc00038 | 0xaf200008 | sw $zero,8($t9) | |
| 0xbfc0003c | 0x24080063 | helper:li $t0,99 | |
| 0xbfc00040 | 0x03e00008 | jr $ra | |
| 0xbfc00044 | 0x00000000 | nop | |
| 0xbfc00048 | 0x27bdfff8 | helper2:addiu $sp,$sp,-8 | |
| 0xbfc0004c | 0xafb00000 | sw $s0,0($sp) | |
| 0xbfc00050 | 0x24100063 | li $s0,99 | |
| 0xbfc00054 | 0x8fb00000 | lw $s0,0($sp) | |
| 0xbfc00058 | 0x27bd0008 | addiu $sp,$sp,8 | |
| 0xbfc0005c | 0x03e00008 | jr $ra | |
| 0xbfc00060 | 0x00000000 | nop |
registers — read · written this tick
stack around $sp = 0x00000000 — higher addresses on top
console (what the doors printed)
▮tick 0 of 25
Nothing has run yet. The PC (program counter) points at the first instruction.
helper overwrites $t0 and does not restore it: allowed, because $t0 is caller-saved. The caller's 7 is gone and 99 prints. helper2 overwrites $s0 too, but it saves the old value on the stack first and restores it before jr $ra: $s0 is callee-saved. The caller's 7 comes back.
What to notice
helper writes $t0 and returns without a care: it is allowed to. The
caller’s 7 is gone and 99 prints. helper2 writes $s0 too, but first it
sws the old value and last it lws it back; the stack pane shows the 7
parked and retrieved. The caller’s 7 survives.
fib keeps a result in $s0
Why would a function want a callee-saved register? To hold a value across a
call. fib(n) computes fib(n−1), then must keep that result safe while
it calls fib(n−2). A $t register would be clobbered by the second call.
So it uses $s0, and because $s0 is callee-saved, fib must save its
caller’s $s0 first.
fib's $s0: saved, used across the second call, restored (fib_o0.dump)
carolyne_ref/dumps/fib_o0.dump bfc0027c, bfc002b0–bfc002d0, bfc002e0
1int fib(int n) {2 if (n < 3) return 1;3 return fib(n - 1) + fib(n - 2);4}bfc0027csw s0,20(sp)bfc002b0jal bfc00270 <fib>bfc002b8move s0,v0bfc002c8jal bfc00270 <fib>bfc002d0addu v0,s0,v0bfc002e0lw s0,20(sp)Click any highlighted line to see it light up in the other listing, and read what it does.
What to notice
Three roles in one register: saved at the top for the caller, used in the
middle to survive a call, restored at the bottom. The inner fib calls also
follow the rule, so $s0 truly survives them: they save and restore their
own callers’ $s0, which is this activation’s value.
Before reading on, decide:
Could fib have used $t0 instead of $s0 to hold fib(n−1), and skipped the save and restore?
$ra: clobbered by the call, saved by the callee
$ra sits in the caller-saved list, and that is correct. After jal f, the
caller’s $ra holds f’s return address, not its own. So who saves it? The
callee, in its prologue, for its own sake. Its inner jals will overwrite
it, and it needs its own way home at the end. A leaf has no inner jal and
skips the save (§6.1).
Go deeper: why the split exists, and what Carolyne knows about it (nothing)
If every register were callee-saved, a tiny leaf would pay to save every
register it touched. If every register were caller-saved, a loop calling a
function would save all its live values on every iteration. Splitting the
file in two lets both sides pay only for what they use: callees save the
few $s registers they need, callers keep short-lived values in $t and
long-lived ones in $s. The compiler assigns variables to groups by how
long they live and how many calls they must cross.
Carolyne’s engine knows none of this. To it, $s0 and $t0 are two entries
of one register class, renamed the same way, written back the same way. The
convention exists entirely in the compiler’s output. That is the point of
§2.2 again: the hardware sees numbers, the software agrees on jobs.
Can you now explain it?
A call may clobber $t, $a, $v and $ra. It must leave $s, $sp, $fp and $gp as it found them. A caller keeps long-lived values in $s registers; a callee that uses one saves and restores it; a non-leaf saves its own $ra.
If you remember only three things
- Two duties, one table. Caller-saved: save it yourself before the jal. Callee-saved: the function that touches it puts it back.
- Across a call means $s. fib keeps fib(n−1) in $s0 during fib(n−2). A $t there would be silently overwritten.
- $ra is both. Clobbered by every jal from the caller's view; saved in the prologue by every function that makes calls.
Now try to explain this one yourself: main in hello.c saves $ra
and $s8 but not $s0. fib saves $ra, $s8 and $s0. Explain both
lists from the rule.
Go deeper: a model answer, once you have tried
Both are non-leaves, so both save $ra. Both use $s8 as a frame pointer
at -O0, so both save it, because $s8 is callee-saved. fib needs one more
value to survive a call, fib(n−1), and puts it in $s0, so it must save
$s0 too. main keeps its loop counter on the stack (16(s8)), not in a
register, and needs nothing else across its calls. So it touches no $s0
and saves none. Every save in a prologue answers to one line of the table.
Check your understanding
0/5 answered1.Which registers are guaranteed to hold the same value after `jal f` returns as before it?
2.fib saves $s0 in its prologue (sw s0,20(sp)). Why does it need $s0 at all?
3.In the runner, the caller puts 7 in $t0, calls helper (which writes 99 into $t0), then prints $t0. What prints?
4.'$s0 is callee-saved' means which of these?
5.$ra is listed as 'gone after a call'. So who saves $ra, and when?