Lecture 11
Andreas Moshovos
Fall 2007, Revised Winter 2024, Fall 204
A complete function example: The Ackerman recursive subroutine.
The following C code computes the Ackerman function:
int
Ackerman(unsigned
int x, unsigned int y) {
if (x == 0) return y+1;
if (y == 0) return Ackerman (x-1, 1);
return Ackerman (x-1, Ackerman(x, y-1));
}
This is an interesting to implement function because it’s recursive and also shows how to use a function call as an argument to call another function.
An equivalent implementation of Ackerman that illustrates what is supposed to happen in the last line is as follows:
int
Ackerman(unsigned
int x, unsigned int y) {
int tmp;
if (x == 0) return y+1;
if (y == 0) return Ackerman (x-1, 1);
tmp = Ackerman(x, y-1);
return Ackerman (x-1, tmp);
}
Let’s first figure out what needs
to be saved on the stack. We definitely need to save the return address since
Ackerman calls functions. We also need to preserve the value of parameter x
across the call to function Ackerman(x, y-1). We do not need to preserve y since we never needed after a
call. So, in total we need space for two words.
Here’s what the stack frame will look like after Ackerman’s
prologue:
|
sp à |
+0 |
Saved a0 / argument x |
|
|
+4 |
Saved ra / return address |
Here’s an implementation in NIOS II assembly:
.text
Ackerman:
# Prologue
addi
sp, sp, -8
sw ra,
4(sp)
# if (x == 0) return y
+1
# we write this as:
# if (x != 0) skip
over the code that returns y + 1
bne a0, x0, Xnot0
Xis0:
# return value is y +
1
addi
a0, a1, 1
# done, return from
the function
beq x0,
x0, epilogue
Xnot0:
# test if y is 0 and
return Ackerman (x – 1, 1)
bne a1, x0, Ynot0
Yis0:
# pre-call
# pass arguments
# first argument is
x-1
addi
a0, a0, -1
# second is 1
addi a1, a1, 1
call Ackerman
# the return value is
already in r2, exit the function
beq x0,
x0, epilogue
Ynot0:
# first call Ackerman
(x, y-1)
# pre-call
# preserve the value
of x on the stack
sw a0, 0(sp)
# pass arguments
# x is in the right
place
# decrement y by 1
addi a1, a1, -1
call
Ackerman
# post-call
# call Ackerman (x-1,
tmp) where tmp is the value returned from Ackerman (x, y-1)
# pre-call
# 2nd
argument is the value returned from the Ackerman (x, y-1)
mv a1, a0, r0
# restore x
# decrement x by 1 and
pass as first argument
lw a0,
0(sp)
addi a0, a0, -1
call
Ackerman
# return value is
already in r2
epilogue:
# Epilogue
# restore ra and
de-allocate stack space
lw ra, 4(sp)
addi sp,
sp, 8
ret # return to caller
A closer
look at how we can figure out what needs to be preserved across calls
Let us
now look at a more systematic way of determining what register values need to
be preserved across a call site.
Control
Flow: The
figure below shows the control flow graph of the function:

Paths
through the Control Flow Graph: There
are three partially overlapping execution paths through the code. All three
(1), (2), and (3), pass through the first if condition (x == 0). Path (1) exits
if the condition is true returning y+1. Paths (2) and (3) continue and evaluate
the second if condition (y == 0). They diverge depending on whether the
condition is true (path (2)) or false (path (3)). Path (2) calls Ackerman (x-1)
while path (3) performs two calls to ackerman before returning.

Lifetime
Analysis: The
function accepts as input x and y. Let us now consider all three possible paths
trying to determine whether x and y remain “live” at each point along the path.
We care about x and y because when the function is called, they are in r4 and
r5. By convention those two registers are caller saved. That is, if we care for
their values and we do a call, we have to preserve them by saving and restoring
them across the call (this is done typically by pushing them on the stack).
A value is live at some block along a path, if
we expect that we will use it in some other block that we might encounter later
on in the path. This is best understood by walking through our example. Let us
first annotate where the input values are used. Ultimately, what we are
interested in are values that need to survive across a call to a function.
Those are values that need to be explicitly preserved and need to reside either
a callee saved register or on the stack. We are looking for scenarios where:
Xx has a value and is in a caller saved register
Call some_function
Use Rx expecting that it still holds the same value it did before the call.
This
will become clearer as we go through the example.
Let’s
first consider path (1):

There are
two uses of x and y in this path as noted by the colored boxes. There are no
calls before any of the two uses of x and y. So, we do not need to save and
restore them across a call since there is no call :)
Now
let’s look at path (2). In the following diagram we annotated those using blue
boxes:

We also
added checkmarks next to the last use of x and y. As in path (1) none of the
uses are *after* a call. The last use of x is where it is being used as an
argument to Ackerman(x-1,1). This means that before we call Ackerman there, we
will need to take the existing x in a0 and substract 1 from iti. Then it
becomes the first argument to the call. After the call, we return, thus we no
longer care for the original x. For y, things are simpler. We use it only for
evaluating the second if condition and from that point on, in path (2) we no
longer care for it.

Finally,
let’s look at path (3) (uses of x and y are now annotated with green boxes and
the last use of each is marked with a check mark):

The
last use of y is just before the first call to Ackerman (tmp = Ackerman(x,
y-1)). After that we no longer care about y. However, look at x. The last use
of x is in the second call to Ackerman. This means that x needs to
“survive” the first call. So, x is live across the first call to Ackerman along
path (3) and for this reason it needs to be preserved since we will be making a
call, and presently it is held in a0 which is a caller-saved register. This is
why we make space on the stack and we save it there.
If you
want to dive a bit deeper than this, consider what other options might existed
for preserving a0. One option might have been to copy the value in a
callee-saved register and to do so just before the call. Something like this:
mov s0, a0
call Ackerman
// s0 still holds the value it had before the call since it is a callee-saved register
However,
since s0 is a callee-saved register and
since we are function as well we cannot simply use it anytime we wish. Whoever
called us, expects that any value they left there, if any (we don’t know
whether they care), will remain intact. So, before we overwrite the existing
value in s0 we need to save its existing value on the stack. At the end, we are
still saving and restoring a value from the stack either way. What further
limits our options here is that Ackerman calls itself.