Skip to content
Contents

Call and return, arguments and results

Section 6.1 8 min read

  • the call protocol: $a0–$a3 in, jal, $v0 out, jr $ra
  • leaf and non-leaf functions
  • prologue and epilogue
  • more than four arguments

Checked against: MIPS o32 ABI: calling convention carolyne_ref/dumps/fib_o0.dump (bfc0014c–bfc0018c outchar; bfc00230–bfc00234) carolyne_ref/dumps/hello_o0.dump (bfc000b0 print_char, bfc000ec print_int) carolyne_ref/programs/fib.c

Before you start

  • §5.3: jal writes $ra = jal + 8; jr $ra returns
  • §4.4: a frame is claimed with addiu and filled with sw

By the end, you can

  • Trace a call through $a0, $ra, $v0 and back
  • Say which functions may skip the stack and which must save $ra
  • Name the prologue and epilogue of a real function in a dump
  • Say where a fifth argument goes

The protocol in four lines

A is a fixed exchange between two functions, in a fixed order:

  1. The caller puts the arguments in $a0, $a1, $a2, $a3.
  2. jal f writes $ra = its own address + 8 and jumps to f.
  3. f leaves its result in $v0 (a 64-bit result uses $v1 too).
  4. jr $ra jumps back to the address in $ra.

The call protocol: arguments in, jal, result out, jr $ra

Four crossings between caller and callee, always in this order and always through these registers.

the caller the callee addiu $a0,$zero,6 jal square nop sw $v0,4($t9) square: mul $v0,$a0,$a0 jr $ra nop ① arguments: $a0–$a3 ② jal: PC → callee, $ra = jal + 8 ③ result: $v0 (and $v1) ④ jr $ra: PC → the address in $ra $s0–$s7, $sp, $fp, $gp come back unchanged (callee-saved: the callee restores them) $t0–$t9, $a0–$a3, $v0–$v1, $ra may be anything (caller-saved: save them yourself if you need them) a leaf (calls nobody): keeps $ra, needs no frame a non-leaf: saves $ra before its own jal, restores it before jr $ra

What to notice

Nothing else crosses between the two boxes. The caller does not know how square works; square does not know who called it. They agree only on the four registers and the order, and that agreement is the o32 convention of §2.2.

Watch a call

The runner calls square twice with different arguments. It pauses before the first jal to ask you which registers will change.

Calling a leaf function twice: $a0 in, jal, $v0 out, jr $ra back

How does one function call another and get an answer back?

program (address · word · instruction)

PC →0xbfc000000x3c191000_start:lui $t9,0x1000
0xbfc000040x37390ff0ori $t9,$t9,0xff0
0xbfc000080x24040006li $a0,6
0xbfc0000c0x0ff0000djal 0xbfc00034
0xbfc000100x00000000nop
0xbfc000140xaf220004sw $v0,4($t9)
0xbfc000180x24180020li $t8,32
0xbfc0001c0xaf380000sw $t8,0($t9)
0xbfc000200x24040007li $a0,7
0xbfc000240x0ff0000djal 0xbfc00034
0xbfc000280x00000000nop
0xbfc0002c0xaf220004sw $v0,4($t9)
0xbfc000300xaf200008sw $zero,8($t9)
0xbfc000340x70841002square:mul $v0,$a0,$a0
0xbfc000380x03e00008jr $ra
0xbfc0003c0x00000000nop

registers — read · written this tick

HI 0x00000000LO 0x00000000PC 0xbfc00000

console (what the doors printed)

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

tick 0 of 19

Nothing has run yet. The PC (program counter) points at the first instruction.

Before each jal, $a0 turns green with the argument. The jal writes $ra. square reads $a0 and writes $v0. jr $ra brings the PC back to the instruction after the jal's slot, and the caller prints $v0. square touches no stack: it is a leaf.

What to notice

$a0 turns green before each jal, $ra turns green at the jal, $v0 turns green inside square, and the PC arrow comes back to the instruction after the nop. The two returns land in two different places, because $ra was different each time.

Leaves and non-leaves

square is a : it calls nobody. Its $ra is never overwritten, so it returns with jr $ra and touches no memory. print_char and print_int in hello.c are leaves too. At -O0 GCC still gives them an 8-byte frame for $fp, but nothing is saved except $fp itself.

A calls others. Its own jal writes $ra, destroying the caller’s return address. So it must save $ra before the call and restore it after. Here is outchar from fib.c, which calls print_char:

outchar: a non-leaf that must save $ra (fib.c and the -O0 dump)

carolyne_ref/programs/fib.c; fib_o0.dump bfc0014c–bfc0018c

fib.c
1int outchar(int a) {
2 print_char(a);
3 return 0;
4}
MIPS (-O0)
bfc0014caddiu sp,sp,-24
bfc00150sw ra,20(sp)
bfc00154sw s8,16(sp)
bfc00158move s8,sp
bfc0015csw a0,24(s8)
bfc00160lw v0,24(s8)
bfc00164seb v0,v0
bfc00168move a0,v0
bfc0016cjal bfc000b0 <print_char>
bfc00170nop
bfc00174move v0,zero
bfc00178move sp,s8
bfc0017clw ra,20(sp)
bfc00180lw s8,16(sp)
bfc00184addiu sp,sp,24
bfc00188jr ra
bfc0018cnop

Click any highlighted line to see it light up in the other listing, and read what it does.

What to notice

sw ra,20(sp) near the top and lw ra,20(sp) near the bottom are the whole reason this function has a frame. Between them, jal print_char overwrites $ra; without the save, outchar’s own jr ra would jump back into outchar forever.

The instructions before the body are the ; the mirror image at the end is the . You met both in §4.4; now you know why the $ra line is there.

Before reading on, decide:

outchar's `jal print_char` sits at 0xBFC0016C. Inside print_char, what is $ra?

Calling with a computed value

The pattern “compute, move into $a0, call” is everywhere. In fib.c’s DISPLAY_INT, a digit is computed into v0 and passed on:

bfc00230:  move  a0,v0             # the argument
bfc00234:  jal   bfc0014c <outchar>
bfc00238:  nop

More than four arguments? The fifth and later go on the stack, in the caller’s frame at 16($sp) and up. That is just above the 16-byte argument area of §4.4. None of the reference programs needs that, so this book only mentions it.

Go deeper: why the link goes into a register, and how Carolyne writes it

Many older ISAs pushed the return address onto the stack as part of the call instruction. MIPS refused: a call that writes memory would be a store hidden inside a jump, breaking the one-thing-per-instruction rule that keeps the pipeline simple. So jal writes a register, and software decides whether that value needs to reach memory. A leaf never pays for a store it does not need; a non-leaf pays one sw and one lw. This is the same reasoning that gave MIPS no push and no pop.

Carolyne’s branch unit writes the link the same way it writes any register: the jal/jalr/bltzal/bgezal µops carry $31 (or rd) as their one destination, and the body writes pc + 8 through the unit’s writeback port inside a guard on the linking µops, so a plain j or jr writes nothing. The reorder buffer then retires it like an ALU result. Nothing in the engine knows that this particular register is “the return address”.

Can you now explain it?

A call is four crossings: arguments in $a0–$a3, jal writing $ra, the result in $v0, jr $ra back. A leaf needs no frame; a non-leaf saves $ra in its prologue and restores it in its epilogue.

If you remember only three things

  1. Four registers, one order. $a0–$a3 in, $ra by jal, $v0 out, $ra by jr. Caller and callee agree on nothing else.
  2. Leaf or not decides the frame. square and print_char keep $ra and need nothing. outchar and main call others, so sw ra and lw ra bracket their bodies.
  3. The fifth argument walks. Arguments five and up go on the stack above the 16-byte argument area; the first four ride in registers.

Now try to explain this one yourself: in the runner, the caller reloads $a0 before the second jal square. Could it have relied on $a0 still being 6 after the first call? Use §2.2’s table.

Go deeper: a model answer, once you have tried

No. $a0 is caller-saved: the callee may overwrite it, and the caller must assume it did. square happens not to touch $a0, but a different square that computed with $a0 in place would be equally correct under the convention. A value the caller needs after a call belongs in an $s register (saved by the callee, §6.2) or on the caller’s own stack. The runner’s caller does the simple thing: it reloads $a0 before every call.

Check your understanding

0/5 answered
  1. 1.Put the four steps of a call in order.

  2. 2.In the runner, where does square find its argument, and where does it leave its result?

  3. 3.Why does square need no stack frame at all?

  4. 4.A C function takes five int arguments. Where does the fifth go?

  5. 5.print_char and print_int are leaves, yet at -O0 each has an 8-byte frame. What is in it, and why does it vanish at -O2?


Next: §6.2 — who saves what: caller-saved and callee-saved.

Where this comes back

One later section builds on this one.