Recursion: factorial and Fibonacci

These two subroutines call themselves. factorial(8) returns 40320 in $s0, and fib(10) returns 55 in $s1. Both take a nonnegative integer in $a0 and return a result in $v0. For factorial, 0! and 1! are both 1. For Fibonacci, fib(0) is 0 and fib(1) is 1. Negative inputs are outside these routines' contract. For a correct result, it must fit in 32 bits; we will try an input that exceeds that limit below.

Each call reserves its own stack frame. When factorial calls itself, $sp moves down again, so 0($sp) reaches a different saved n in each active call.

.text
.globl main

# factorial(n): n in $a0, the answer in $v0
factorial:
    addi $sp, $sp, -8
    sw $ra, 4($sp)          # this call's return address
    sw $a0, 0($sp)          # this call's n
    blt $a0, 2, fact_one    # 0! and 1! are 1
    addi $a0, $a0, -1
    jal factorial           # factorial(n - 1)
    lw $a0, 0($sp)          # recover this call's n
    mul $v0, $v0, $a0       # n * factorial(n - 1)
    j fact_done
fact_one:
    li $v0, 1
fact_done:
    lw $ra, 4($sp)
    addi $sp, $sp, 8
    jr $ra

# fib(n): n in $a0, the answer in $v0
fib:
    addi $sp, $sp, -12
    sw $ra, 8($sp)
    sw $a0, 4($sp)
    blt $a0, 2, fib_small   # fib(0) is 0 and fib(1) is 1
    addi $a0, $a0, -1
    jal fib                 # fib(n - 1)
    sw $v0, 0($sp)          # keep the first result through the next call
    lw $a0, 4($sp)
    addi $a0, $a0, -2
    jal fib                 # fib(n - 2)
    lw $t0, 0($sp)
    add $v0, $v0, $t0       # fib(n - 1) + fib(n - 2)
    j fib_done
fib_small:
    move $v0, $a0
fib_done:
    lw $ra, 8($sp)
    addi $sp, $sp, 12
    jr $ra

main:
    li $a0, 8
    jal factorial
    move $s0, $v0           # 8!
    li $a0, 10
    jal fib
    move $s1, $v0           # fib(10)
    li $v0, 10              # exit after saving both answers
    syscall

Every jal overwrites the one $ra register. When factorial calls itself, it must keep the address for returning to its caller, so it stores $ra before the inner jal and restores it before jr $ra. fib does the same for each of its two inner calls. The exit syscall in main stops execution after both answers have been saved.

The frames contain these four-byte words. Offsets are measured from $sp after each routine reserves its space:

Routine0($sp)4($sp)8($sp)
factorialsaved nsaved $raoutside its frame
fibfirst resultsaved nsaved $ra

The saved n matters because $a0 is caller-saved. For example, a factorial(3) call stores 3 at 0($sp), changes $a0 to 2, and calls factorial(2). The inner calls may leave $a0 changed; they do not promise to return it as 3. After the call returns with factorial(2) = 2 in $v0, lw $a0, 0($sp) gets the outer call's 3 back, and mul produces 6. Each active call has its own saved word because each made a new frame at a lower address.

fib needs one more word. After its first jal fib, $v0 holds fib(n - 1). A second jal fib will replace $v0 with fib(n - 2). $t0 is caller-saved too, so moving the first result there before the second call would not preserve it. sw $v0, 0($sp) keeps that result in this call's frame. Only after the second call does lw $t0, 0($sp) load it for the final addition.

Step through factorial(3) by changing the first argument in main to 3. Let S be the value of $sp just before main calls it. At the branch in each active call, predict the saved n at 0($sp) and the position of $sp: the calls for 3, 2, and 1 use S − 8, S − 16, and S − 24. Their saved n values are 3, 2, and 1. As the calls return, each restores its own $ra and releases eight bytes. $s0 ends at 6 and $sp returns to S. Select Build, then use Step and the Memory panel to check the three frames. After restoring the original inputs, select Build, then Run. The embedded Test checks the original 8 and 10.

For another small change, turn factorial into a recursive sum from 1 through n. Keep its frame and recursive call, but make the base case return 0 when n is 0, and add the saved n to the returned value. The same nonnegative-input rule applies. With 3 in $a0, the calls should return 0, then 1, then 3, then 6. Select Build, then Run to check the result. Restore the original code when you finish.

Finally, try 10 and then 13 as the factorial input. 10! is 3628800. 13! is 6227020800, which is too large for the 32-bit result of mul. $s0 instead shows 0x7328CC00 in hexadecimal, the low 32 bits of that product (1932053504 in decimal). The recursion still follows the same calls; the multiplication loses the upper bits.