fib’s frame, drawn
GCC gave fib a 32-byte frame at -O0. Its prologue (fib_o0.dump bfc00270) is addiu sp,sp,-32, sw ra,28(sp), sw s8,24(sp),
sw s0,20(sp), move s8,sp, then sw a0,32(s8). Drawn from the top:
offset from $sp | holds | why |
|---|---|---|
| 32 | n, the argument, spilled | above the frame, in the caller’s argument area |
| 28 | saved $ra | fib calls fib: the inner jal overwrites $ra |
| 24 | saved $fp ($s8) | callee-saved; this call uses it as its frame pointer |
| 20 | saved $s0 | callee-saved; this call keeps fib(n−1) in it |
| 16 | unused | padding to a multiple of 8 |
| 0 … 12 | argument area | reserved for the two fib calls this call makes |
The spill at 32($fp) is the interesting line. $fp equals the new $sp.
So 32($fp) is the old $sp, the first word of the caller’s
.
Every fib keeps its n in the frame of the fib that called it, and
reserves the same 16 bytes at its own bottom for the fibs it calls.
The whole function
Here is every line of fib, with the C beside it. Click a C line.
fib, complete: fib.c and the 32 words of fib_o0.dump bfc00270–bfc002ec
carolyne_ref/programs/fib.c lines 35–39; carolyne_ref/dumps/fib_o0.dump
1int fib(int n) {2 if (n < 3)3 return 1;4 return fib(n - 1) + fib(n - 2);5}bfc00270addiu sp,sp,-32bfc00274sw ra,28(sp)bfc00278sw s8,24(sp)bfc0027csw s0,20(sp)bfc00280move s8,spbfc00284sw a0,32(s8)bfc00288lw v0,32(s8)bfc0028cslti v0,v0,3bfc00290beqz v0,bfc002a4bfc00294nopbfc00298li v0,1bfc0029cb bfc002d4bfc002a0nopbfc002a4lw v0,32(s8)bfc002a8addiu v0,v0,-1bfc002acmove a0,v0bfc002b0jal bfc00270 <fib>bfc002b4nopbfc002b8move s0,v0bfc002bclw v0,32(s8)bfc002c0addiu v0,v0,-2bfc002c4move a0,v0bfc002c8jal bfc00270 <fib>bfc002ccnopbfc002d0addu v0,s0,v0bfc002d4move sp,s8bfc002d8lw ra,28(sp)bfc002dclw s8,24(sp)bfc002e0lw s0,20(sp)bfc002e4addiu sp,sp,32bfc002e8jr rabfc002ecnopClick any highlighted line to see it light up in the other listing, and read what it does.
What to notice
Two jal bfc00270 <fib> inside fib itself. Each one runs this same code
again with a new frame 32 bytes lower, and a new n spilled at the new
32(s8). When the inner call’s jr ra comes back, this frame’s $s8 still
points at this frame, so 32(s8) is still this call’s n.
Run fib(3)
The runner uses the exact -O0 shape with a small _start. The stack pane
follows $sp. It pauses before the first jr $ra to ask where it goes.
fib(3) with the real -O0 frame: two activations at once, two saved $ra, then the unwinding
What is on the stack, exactly, while fib(3) runs?
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 | 0x24040003 | li $a0,3 | |
| 0xbfc00014 | 0x0ff00009 | jal 0xbfc00024 | |
| 0xbfc00018 | 0x00000000 | nop | |
| 0xbfc0001c | 0xaf220004 | sw $v0,4($t9) | |
| 0xbfc00020 | 0xaf200008 | sw $zero,8($t9) | |
| 0xbfc00024 | 0x27bdffe0 | fib:addiu $sp,$sp,-32 | |
| 0xbfc00028 | 0xafbf001c | sw $ra,28($sp) | |
| 0xbfc0002c | 0xafbe0018 | sw $fp,24($sp) | |
| 0xbfc00030 | 0xafb00014 | sw $s0,20($sp) | |
| 0xbfc00034 | 0x03a0f025 | move $fp,$sp | |
| 0xbfc00038 | 0xafc40020 | sw $a0,32($fp) | |
| 0xbfc0003c | 0x8fc20020 | lw $v0,32($fp) | |
| 0xbfc00040 | 0x28420003 | slti $v0,$v0,3 | |
| 0xbfc00044 | 0x10400004 | beqz $v0,0xbfc00058 | |
| 0xbfc00048 | 0x00000000 | nop | |
| 0xbfc0004c | 0x24020001 | li $v0,1 | |
| 0xbfc00050 | 0x1000000d | b 0xbfc00088 | |
| 0xbfc00054 | 0x00000000 | nop | |
| 0xbfc00058 | 0x8fc20020 | recurse:lw $v0,32($fp) | |
| 0xbfc0005c | 0x2442ffff | addiu $v0,$v0,-1 | |
| 0xbfc00060 | 0x00402025 | move $a0,$v0 | |
| 0xbfc00064 | 0x0ff00009 | jal 0xbfc00024 | |
| 0xbfc00068 | 0x00000000 | nop | |
| 0xbfc0006c | 0x00408025 | move $s0,$v0 | |
| 0xbfc00070 | 0x8fc20020 | lw $v0,32($fp) | |
| 0xbfc00074 | 0x2442fffe | addiu $v0,$v0,-2 | |
| 0xbfc00078 | 0x00402025 | move $a0,$v0 | |
| 0xbfc0007c | 0x0ff00009 | jal 0xbfc00024 | |
| 0xbfc00080 | 0x00000000 | nop | |
| 0xbfc00084 | 0x02021021 | addu $v0,$s0,$v0 | |
| 0xbfc00088 | 0x03c0e825 | done:move $sp,$fp | |
| 0xbfc0008c | 0x8fbf001c | lw $ra,28($sp) | |
| 0xbfc00090 | 0x8fbe0018 | lw $fp,24($sp) | |
| 0xbfc00094 | 0x8fb00014 | lw $s0,20($sp) | |
| 0xbfc00098 | 0x27bd0020 | addiu $sp,$sp,32 | |
| 0xbfc0009c | 0x03e00008 | jr $ra | |
| 0xbfc000a0 | 0x00000000 | nop |
registers — read · written this tick
stack around $sp = 0x00000000 — higher addresses on top
console (what the doors printed)
▮tick 0 of 78
Nothing has run yet. The PC (program counter) points at the first instruction.
Every call to fib claims 32 bytes and saves $ra, $fp and $s0 in the same three slots (28, 24, 20). fib(3) calls fib(2), which returns 1 at once (n < 3); then fib(3) calls fib(1), which also returns 1. So at most two frames are alive: $sp goes 0x10000FE0 → 0x10000FC0 → 0x10000FA0 and back. Each jr $ra reads a different saved $ra. $v0 ends as 1 + 1 = 2.
What to notice
fib(3) claims a frame at 0x10000FC0. Its first call, fib(2), claims
one at 0x10000FA0, finds 2 < 3, and returns 1 through its own saved
$ra. fib(3) parks the 1 in $s0, calls fib(1), which uses the same
address 0x10000FA0 for its frame, and returns 1. addu makes 2, and
fib(3)’s jr $ra goes back to _start.
Each running call is an
.
At the deepest point two activations are alive: fib(3) and one child. The
child’s frame sits 32 bytes below its parent’s, holds a different $ra, and
reads a different n from its own 32(s8). That is all
needs.
Before reading on, decide:
fib(3)'s frame occupies 0x10000FC0–0x10000FDF. Where does fib(3) spill its own n = 3?
Go deeper: what -O2 does to fib, and how your core runs the spill and reload
At -O2, GCC keeps n in a callee-saved register instead of spilling it,
keeps the first result in another, drops the frame pointer, and saves only
$ra and the $s registers it uses. The frame shrinks and the six
lw v0,32(s8) reloads vanish. The recursion itself stays: fib still calls
itself twice, and each activation still gets its own frame. Chapter 9 shows
the -O2 dump beside this one.
The -O0 code has a pattern your core sees constantly: sw a0,32(s8) followed
two instructions later by lw v0,32(s8). The store is still waiting in the
store buffer when the load asks for the same address. Carolyne’s load/store
unit searches the buffer for the newest older store to that word and takes
its data directly, without waiting for the store to reach memory. That
store-to-load forwarding is what keeps -O0 code from crawling, and it is one
of the mechanisms the twelve control bugs of the bring-up were found in.
Can you now explain it?
fib's frame is 32 bytes: $ra at 28, $fp at 24, $s0 at 20, the argument spilled at 32 above it, an argument area below. Every call gets a fresh frame one step lower; at most two are alive in fib(3); the returns unwind through two different saved $ra.
If you remember only three things
- Same code, new frame, new n. The jal inside fib runs the same 32 words with $sp 32 bytes lower. 32(s8) is a different word each time.
- Two saved $ra, two different places to return to. fib(2) returns into fib(3); fib(3) returns to _start. Each reads its own 28(sp).
- The frame is the whole mechanism. No special instruction, no hardware stack. Prologue, epilogue and the argument area do everything.
Now try to explain this one yourself: call fib(4) instead. How deep
does the stack get, and which activations are alive at the deepest point?
How many bytes below 0x10000FE0 is $sp then?
Go deeper: a model answer, once you have tried
fib(4) calls fib(3), which calls fib(2): three activations alive at
once, fib(2) being a base case. Three frames of 32 bytes: $sp reaches
0x10000FE0 − 96 = 0x10000F80. After fib(2) returns, fib(3) calls
fib(1) into the same slot; after fib(3) returns, fib(4) calls fib(2)
into 0x10000FA0. The deepest point is always the leftmost path of the call
tree, and each extra level of n adds one frame, 32 bytes, and one more
saved $ra.
Check your understanding
0/5 answered1.How deep is the stack at the deepest point of fib(3), counting only fib's frames?
2.In fib's frame, at what offset from $sp is the saved $ra, and where is n?
3.What is `move s8,sp` in the prologue for?
4.What does recursion need from the hardware?
5.Why does fib compiled at -O2 need a smaller frame than at -O0?