You may have noticed that I’ve been using the term
function for something
refered to in the textbook as a
subroutine. In general, these terms can be
used interchangibly. While there are arguably some different implications to
each term, they’re not widely agreed upon in the context of software.
A Few Arguments
If we have six arguments or fewer, we can construct our function quite simply.
Let’s consider a function
int max(int a, int b) that simply returns the
larger of its two arguments. Take a look at
! max - finds the max of two integers fmt: .asciz "Max: %d\n" .align 4 .global main main: save %sp, -96, %sp ! call max mov 20, %o0 mov 28, %o1 call max nop ! print the result mov %o0, %o1 set fmt, %o0 call printf nop ! exit mov 0, %o0 ! exit with 0 mov 1, %g1 ! exit trap ta 0 ! trap to system ! int max(int,int) ! returns the larger of the two given values max: save %sp, -64, %sp cmp %i0, %i1 ble,a maxret mov %i1, %i0 maxret: ret restore
You can see before the call to
max that we’re putting our arguments into
%o1. This is exactly the same as how we called other functions
we’ve used previously, like
max, we immediately
save. This saves the local and input registers,
turns the output registers into input registers, and adjusts the stack and
frame pointers. You must supply at least 64 bytes to store those local and
The parameters we set in the
%o1 registers are now available in
%i1 registers, respectively. We can do a comparison between,
them and if neccessary move the value from
%i0 to ensure that
the largest value is stored in
Finally, we reach the
ret instruction, and we branch back to the instruction
immediately following the delay slot of the call. But, our
has its own delay slot, which we’ve filled with
restore to undo the
from the start of the function.
Having returned back to
main, the larger of the two values is guaranteed to
be stored in
If we have more than six arguments, we run out of input registers and have to
do things differently. Naievely, it seems like you have enough registers, with
%o7 at your disposal, but the registers
reserved, so they cannot be used. Instead, we can place our remaining two
arguments on the stack. By convention, there’s 24 bytes per frame left on the
stack for function arguments. That’s like having space for six more registers!
Given that we’re going to be relying on these standard conventions in our program, we should probably just use the standard 96 byte save in our functions. In general, it’s simpler than trying to optimize our memory usage within each function.
Let’s consider a function
int max8(int a, int b, int c, int d, int e, int f,
int g, int h) that simply returns the largest argument it is given. We can
store the last two values on the stack using the right offsets, as shown in
! max8 - finds the max of eight integers fmt: .asciz "Max: %d\n" .align 4 .global main main: save %sp, -96, %sp ! call max mov 20, %o0 mov 28, %o1 mov 83, %o2 mov 12, %o3 mov 11, %o4 mov 43, %o5 mov 8, %l0 st %l0, [%sp + 4 + 64] mov 13, %l0 st %l0, [%sp + 8 + 64] call max8 nop ! print the result mov %o0, %o1 set fmt, %o0 call printf nop ! exit mov 0, %o0 ! exit with 0 mov 1, %g1 ! exit trap ta 0 ! trap to system ! int max8(int,int,int,int,int,int,int,int) ! returns the largest of the eight given values max8: save %sp, -96, %sp ld [%fp + 4 + 64], %o0 ld [%fp + 8 + 64], %o1 ! values are now in %i0, %i1, %i2, %i3, %i4, %i5, %o0, %o1 cmp %i0, %i1 ble,a 1f mov %i1, %i0 1: cmp %i0, %i2 ble,a 1f mov %i2, %i0 1: cmp %i0, %i3 ble,a 1f mov %i3, %i0 1: cmp %i0, %i4 ble,a 1f mov %i4, %i0 1: cmp %i0, %i5 ble,a 1f mov %i5, %i0 1: cmp %i0, %00 ble,a 1f mov %o0, %i0 1: cmp %i0, %o1 ble,a 1f mov %o1, %i0 1: ret restore
As you can see, it’s only a few extra lines of code to put additional
parameters on the stack. By convention, the space from
%sp + 68 to
%sp + 92
is dedicated to input arguments. Note that we’re using positive offsets from
the stack pointer to store our arguments. This is different from how we
addressed local stack variables, which used negative offsets from the frame
max8 changes the stack. The stack pointer grows more
negative, and the frame pointer takes on the stack pointer’s old value.
Consequentially, when accessing our extra arguments from within
max8, we now
use positive offsets from the frame pointer.
As you use more functions and your programs grow in size, it may be annoying to
specify unique, meaningful labels for every branch. If a named label would
be more of a hiderence than a help, you can use a single-digit numeric label
instead. These labels can be duplicated, and are branched to by specifying the
digit followed by
b. The suffix
f indicates that you wish to branch
to the next matching label, while the suffix
b indicates that you wish to
branch to the previous matching label.
I suggest only using these short labels when you’ll be branching from some place very nearby. The code above is easily understandable in part because there’s a clear pattern, and the label is only ever a couple lines away from the branch statement. The further the branch, the more important a descriptive label is.