Skip to content
Contents

Who saves what: caller-saved and callee-saved

Section 6.2 9 min read

  • clobber: a call may destroy a register
  • caller-saved: save it yourself
  • callee-saved: the function that uses it restores it
  • $ra: destroyed by jal, saved by non-leaves

Checked against: MIPS o32 ABI: register usage carolyne_ref/dumps/fib_o0.dump (bfc0027c, bfc002b8, bfc002d0, bfc002e0) site/src/data/registers-o32.json

Before you start

  • §2.2: the o32 register table
  • §6.1: the call protocol; leaf and non-leaf

By the end, you can

  • Say which registers survive a call and which do not
  • Explain the two duties: the caller's and the callee's
  • Read fib's $s0 save, use and restore as one story
  • Predict what a program prints when it breaks the rule

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$zeroalways 0 (hardware)always 0
$1$atassembler temporarygone (the assembler may use it)
$2$v0return valuegone (holds the result)
$3$v1return value, second wordgone
$4$a0argument 1gone
$5$a1argument 2gone
$6$a2argument 3gone
$7$a3argument 4gone
$8$t0temporarygone (caller-saved)
$9$t1temporarygone (caller-saved)
$10$t2temporarygone (caller-saved)
$11$t3temporarygone (caller-saved)
$12$t4temporarygone (caller-saved)
$13$t5temporarygone (caller-saved)
$14$t6temporarygone (caller-saved)
$15$t7temporarygone (caller-saved)
$16$s0savedkept (callee-saved)
$17$s1savedkept (callee-saved)
$18$s2savedkept (callee-saved)
$19$s3savedkept (callee-saved)
$20$s4savedkept (callee-saved)
$21$s5savedkept (callee-saved)
$22$s6savedkept (callee-saved)
$23$s7savedkept (callee-saved)
$24$t8temporarygone (caller-saved)
$25$t9temporarygone (caller-saved)
$26$k0reserved for the operating systemnever use
$27$k1reserved for the operating systemnever use
$28$gpglobal pointerkept
$29$spstack pointerkept
$30$fpframe pointer (objdump: s8)kept (callee-saved)
$31$rareturn 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:

groupwho saveswhen
caller-saved $t0–$t9, $a0–$a3, $v0–$v1, $at, $rathe caller, if it still needs the valuebefore the jal, into its own frame
callee-saved $s0–$s7, $sp, $fp, $gpthe callee, if it uses the registerin 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 →0xbfc000000x3c191000_start:lui $t9,0x1000
0xbfc000040x37390ff0ori $t9,$t9,0xff0
0xbfc000080x3c1d1000lui $sp,0x1000
0xbfc0000c0x27bd0fe0addiu $sp,$sp,4064
0xbfc000100x24080007li $t0,7
0xbfc000140x0ff0000fjal 0xbfc0003c
0xbfc000180x00000000nop
0xbfc0001c0xaf280004sw $t0,4($t9)
0xbfc000200x24180020li $t8,32
0xbfc000240xaf380000sw $t8,0($t9)
0xbfc000280x24100007li $s0,7
0xbfc0002c0x0ff00012jal 0xbfc00048
0xbfc000300x00000000nop
0xbfc000340xaf300004sw $s0,4($t9)
0xbfc000380xaf200008sw $zero,8($t9)
0xbfc0003c0x24080063helper:li $t0,99
0xbfc000400x03e00008jr $ra
0xbfc000440x00000000nop
0xbfc000480x27bdfff8helper2:addiu $sp,$sp,-8
0xbfc0004c0xafb00000sw $s0,0($sp)
0xbfc000500x24100063li $s0,99
0xbfc000540x8fb00000lw $s0,0($sp)
0xbfc000580x27bd0008addiu $sp,$sp,8
0xbfc0005c0x03e00008jr $ra
0xbfc000600x00000000nop

registers — read · written this tick

HI 0x00000000LO 0x00000000PC 0xbfc00000

stack around $sp = 0x00000000 — higher addresses on top

higher addresses ↑ · each row is one word, byte 0 on the left
0x00000014
00000000
0x00000010
00000000
0x0000000c
00000000
0x00000008
00000000
0x00000004
00000000
0x00000000
00000000
0xfffffffc
00000000
0xfffffff8
00000000
lower addresses ↓ · the stack grows this way

console (what the doors printed)

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

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

fib.c
1int fib(int n) {
2 if (n < 3) return 1;
3 return fib(n - 1) + fib(n - 2);
4}
MIPS (-O0), the $s0 lines
bfc0027csw s0,20(sp)
bfc002b0jal bfc00270 <fib>
bfc002b8move s0,v0
bfc002c8jal bfc00270 <fib>
bfc002d0addu v0,s0,v0
bfc002e0lw 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

  1. Two duties, one table. Caller-saved: save it yourself before the jal. Callee-saved: the function that touches it puts it back.
  2. Across a call means $s. fib keeps fib(n−1) in $s0 during fib(n−2). A $t there would be silently overwritten.
  3. $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 answered
  1. 1.Which registers are guaranteed to hold the same value after `jal f` returns as before it?

  2. 2.fib saves $s0 in its prologue (sw s0,20(sp)). Why does it need $s0 at all?

  3. 3.In the runner, the caller puts 7 in $t0, calls helper (which writes 99 into $t0), then prints $t0. What prints?

  4. 4.'$s0 is callee-saved' means which of these?

  5. 5.$ra is listed as 'gone after a call'. So who saves $ra, and when?


Next: §6.3 — stack frames and recursion: fib step by step.

Where this comes back

2 later sections build on this one.