Be sure to distinguish
between 32bit and 64bit assembly code.
Introduction
A RISC-V function prologue sets up the stack frame and saves registers,
while the epilogue restores them, either using only the stack pointer
(sp) or adding a frame pointer (s0) for variable or large stack frames.
The frame pointer (fp or s0, register x8) is used in RISC-V assembly,
but compilers often omit it by default to save a register and optimize
code size.
How and When the fp Register Is Used
- Frame Pointer Convention: By convention, register x8 acts as fp
when a fixed stack frame pointer is needed, pointing to the bottom
of the current stack frame.
- Debugging and Variable Access: It helps code or debuggers easily
track local variables and arguments via fixed offsets like 8(fp).
- Compressed Instructions: The alias s0/fp (x8) is specifically
chosen so that stack accesses can leverage 16-bit compressed
instructions (c.lwsp / c.sdsp or frame-pointer-based equivalents)
in prologues and epilogues.
- General Saved Register: When a function does not require a dedicated
frame pointer, fp/s0 can be freely repurposed as a standard saved
register (s0).
Do People Actually Use It?
- Compilers (GCC / Clang): Modern compilers default to omitting
the frame pointer (-fomit-frame-pointer) for leaf functions or
standard optimization levels (-O1, -O2, -O3), relying entirely
on the stack pointer (sp or x2) to track offsets.
- When It's Enabled: Compilers turn it back on when using
-fno-omit-frame-pointer, when dealing with variable-length arrays
(VLAs), or for certain debugging configurations.
- Hand-Written Assembly: As noted by experienced RISC-V developers,
many hand-written routines or smaller software environments skip
setting up an fp entirely, managing adjustments directly via sp.
Example Without the Frame Pointer
Functions without a frame pointer rely solely on the stack pointer (sp)
to track local storage and return addresses. This saves a register (s0)
for general use, but requires fixed offsets from sp.
- addi sp, sp, -4: Decrements the stack pointer to allocate 4 bytes
on the stack
- .sw ra, 8(sp): Saves the return address register (ra) into the
allocated stack space (using double word store sd for 64-bit;
use sw for 32-bit).
- lw ra, 0(sp): Restores the return address register from the stack
during the epilogue.
- addi sp, sp, 4: Restores the stack pointer back to its original
position before returning.
# Without FP Example (32-bit architecture)
my_func:
# Prologue
addi sp, sp, -4
sw ra, 0(sp)
# Function Body
# ... do work ...
# Epilogue
lw ra, 0(sp)
addi sp, sp, 4
ret
Example With the Frame Pointer (32-bit architecture)
Functions with a frame pointer (s0) copy the stack pointer into s0/fp
after saving the old frame pointer and return address. This is
helpful for debugging or when stack sizes change dynamically.
- addi sp, sp, -8: Allocates a stack frame (8 bytes).
- sw ra, 4(sp): Saves the return address at the top of the frame.
- sw fp, 0(sp): Saves the previous frame pointer (fp) onto the stack.
- mv fp, sp: Sets the new frame pointer to point to the current
stack pointer.
- lw ra, 4(sp) & lw fp, 0(sp): Restores both ra and the old s0/fp
frame pointer during the epilogue.
# With FP Example (32-bit architecture)
my_func_fp:
# Prologue
addi sp, sp, -8 # allocate stack frame space
sw ra, 4(sp) # save of return address
sw fp, 0(sp) # save of current s0/fp
mv fp, sp # set new frame pointer
# Function Body
# ... do work using s0/fp as base ...
# Epilogue
lw ra, 4(sp) # restore of return address
lw fp, 0(sp) # restore of current s0/fp
addi sp, sp, 8 # restore caller's stack pointer
ret
Links
RISC-V from scratch 4: Creating a function prologue for our UART driver (2 / 3)