Stack Frame
Functions need to keep track of their local variables. Local variables
go in stack frames on the call stack. When a function is called it
moves the stack pointer down making room for the function's local
variables. When it returns it moves the stack pointer up.
A register is used to keep track of the address of the
current stack frame. According to the RISC-V calling convention, register sp (x2)
contains the address of the bottom (the smallest address) of the current stack
frame.
Code interacts with sp in three ways:
- When a function is called, it will move the sp downward to make space for
its stack frame. (The stack frame contains the function’s local variables.)
- During the execution of a function, it will use (positive) offsets from the
sp to locate its local variables. For example: ld a5, 4(sp) and
sd a6, 44(sp) to load and store local variables using offsets from the sp.
- Before a function returns it will adjusting sp back
to its pre-call value.
In this example addi at the beginning and end of a function "creates"
and "destroys" a stack frame of 8 bytes.
func_label:
addi sp, sp, -8
...
...
...
addi sp, sp, 8
ret
The Function Prologue (Setting the Frame Pointer)
When a function is called, it must allocate its own stack frame and
establish its own frame pointer reference. Under the
standard RISC-V Calling Conventions, the callee performs these steps in its
prologue:
- Allocate Stack Space: Decrement the stack pointer (sp) to make room for local variables, the saved return address, and the old frame pointer.
- Save the Caller's Frame Pointer: Store the current value of fp
(which belongs to the function that called it) onto the stack so it
isn't overwritten.
- Establish the New Frame Pointer: Move the relevant stack base address
into the fp (s0) register.
# Example: Allocating a 32-byte stack frame
addi sp, sp, -32 # 1. move stack pointer down to allocate space
sd ra, 24(sp) # 2. save the return address
sd fp, 16(sp) # 3. save the caller's old frame pointer
mv fp, sp # 4. set the new frame pointer to the base of the frame
The Function Epilogue (Restoring the Frame Pointer)
Before returning control to the caller, the callee must undo its
changes so that the caller's environment is perfectly preserved. The callee performs these steps in its epilogue:
- Restore the Caller's Frame Pointer: Load the original fp value back from the stack memory.
- Deallocate Stack Space: Increment the stack pointer (sp) back to its original position.
- Return: Jump back to the calling function.
# Example: De-Allocating a 32-byte stack frame
ld fp, 16(sp) # 1. restore the caller's frame pointer
ld ra, 24(sp) # 2. restore the return address
addi sp, sp, 32 # 3. move stack pointer back up (de-allocate)
jr ra # 4. return to caller