Critical points:
|
Other points of interest:
|
Caller | Callee |
---|---|
main() { int result; result = max( 10, 2 ); printInt( result ); } |
int max(int a, int b) { int result; if ( a < b ) { result = b; } else { result = a; } return result; } |
jal max
instruction to simultaneously (a) put the
current PC in register $ra, and (b) jump to the memory location corresponding to symbol max
.
max
) is executing. The first thing it does is execute
some "subroutine entry" code. Among other things, it makes space on the stack
for its own local variables (in this case, result
).
max
executes - ta-de-dum
max
is done. It executes some "subroutine exit" code - among other things,
it places the return value somewhere the caller can find it, and frees the stack space it allocated for its own use.
jr $ra
.
jal
. The caller executes
a "post-call" sequence of instructions.
Exactly how things are done at each step is decided by convention, an agreement among all software components on how they use the hardware resources. (The hardware doesn't care - it just fetch-increment-execute's.) There are multiple such conventions in the course materials, unfortunately - the book has two or three, and Cebollita uses a different one.
To help fill in what all the decisions are a convention must make, let's examine the Cebollita one in more detail.
addiu $sp, $sp, -<4 * #regs saved>
jal <subroutine-entry-point>
Callee: Procedure Entry
Callee: Procedure Exit
addiu
to $sp to free the space allocated on entry.
jr $ra
Caller: Call Cleanup
Start condition:
When the callee returns, the stack is adjusted in the opposite order,
passing through the stages shown above from bottom to top. The result
is that it the caller ends up with memory looking like it was when the
call was made, and all registers that are important to it having the
values they did just before the call (except $v0 (and possibly $v1),
which hold the function return value).
Caller saves registers:
Caller pushes arguments:
Callee saves $ra/$fp and allocates space for local vars:
At this point, $sp may move, but $fp is fixed (so long as the callee is executing). Its local variables and arguments are both accessible at known,
constant offsets from $fp.
max.c
above.
Here is an annotated version of the
Cebollita-produced assembler code
for main.c
above.
Second, the MIPS convention distinguishes caller-saved from callee-saved resgisters. (In Cebollita, all registers are caller-saved.) Registers $s0-$s7 are callee-saved: if the callee uses them, it has the responsiblity to save the original values no the stack at entry and to restore them on exit. (If it can avoid using them, it avoids having to save them, which is the point.) Registers $t0-$t9 are caller-saved: the callee is free to write into them without preserving their contents, so the caller must save them if it wants to keep the values they hold.
Additionally, the MIPS linkage differs in where it points $fp, and in the locations within the call frame of local storage and saved registers. (Finally, the two versions of the MIPS convention differ in that one of them doesn't use $fp at all - it makes do with just $sp.)