Terms

Caller: the function making the call. Callee: the function that is being called.

callee-saved registers: The registers that a function promises to leave unchanged. Including s0-s11 (saved registers) , sp(stack pointer).

caller-saved registers: The registers that a function does not promise to leave unchanged. Including a0-a7(argument registers) , t0-t6 (temporary registers) ,ra(return address).

The callers perspective

For callee-saved register, it’s appering to be unchanged.

addi s0, x0, 5 # s0 contains 5
jal ra, func.  # call a function
addi s0, s0, 0 # s0 still contains 5

For caller-saved register, such a guarantee does not exist:

addi t0, x0, 5 # t0 contains 5
jal ra, func.  # call a function
addi t0, t0, 0 # t0 may or may not contains 5. thus a garbage

This is a common calling convention bug: when a function returns, you cannot rely on the values in any non-preserved register.

One way to avoid this bug is to save values in the non-preserved registers on the stack before calling the function, then restore the values in the non-preserved registers after the function returns:

addi t0, x0, 5     # t0 contains 5
addi a0, t0, 10    # a0 (argument to func) contains 15
 
# Prologue
addi sp, sp, -8    # decrement stack
sw t0, 0(sp)       # store t0 value on the stack
sw a0, 4(sp)       # store a0 value on the stack
 
# Function call
jal ra, func       # call a function
mv s0, a0          # save return value 1, before restoring a0's old value to avoid overwriting the return value
mv s1, a1          # save return value 2, before moving on, in case a1 is used in the future, to avoid potentially overwriting the return value
 
# Epilogue
lw t0, 0(sp)       # restore t0 value from the stack
lw a0, 4(sp)       # restore a0 value from stack
addi sp, sp, 8     # increment stack
 
addi t0, t0, 0     # t0 contains 5 here because you saved it before the function call!
xori t1, a0, t0    # t0^a0 safely here

The callee perspective

A function cannot noticably change any preserved registers

# Prologue
addi sp, sp, -12    # decrement stack
sw ra, 0(sp)       # store ra value on the stack
sw s0, 4(sp)       # store s0 value on the stack
sw s1, 8(sp)       # store s1 value on the stack
 
# do stuff in the function
 
# Epilogue
lw ra, 0(sp)       # restore ra value from the stack
lw s0, 4(sp)       # restore s0 value from the stack
lw s1, 8(sp)       # restore s1 value from the stack
addi sp, sp, 12    # increment stack
 
# finish up any loose ends
 
ret                # return from function

Notice that we also saved the value of ra on the stack. Remember that the ra register stores the address that we’ll jump back to after this function finishes executing. Saving the value of ra on the stack is necessary if we decide to call another function inside this function. If we make a function call at the “do stuff” comment, then that function call will overwrite ra, and we’ll lose the address that we were supposed to jump back to.

Coding advice

  • For a function that not call other functions, we perfer use caller saved register since it’s no need to s/w on stack.
  • Save the value of ra at the start of a function and restore it at the end of a function.