Function Prologues and Epilogues in Assembly & C
If you've ever delved into low-level programming or Assembly language, you've likely encountered function prologues and epilogues. These are fundamental constructs that ensure function calls are properly managed within the stack and register-based architecture of modern computing. They are added to the beginning and end of all function calls be default on most modern compilers. Here, we'll be diving into what function prologues and epilogues are, how they work in RISC-V Assembly and C code, and when you might actually want to **avoid** using them.
What are Funciton Prologue and Epilogues?
... and why have I never heard of these before?
A function prologue is the sequence of instructions executed at the start of a function to prepare the stack and registers. The concept of function Prologues and Epilogues has been around since at least the 1970s (as far as I can tell). Programming languages like C and Pascal have stack-based function calls that require managing the stack.
Most modern languages and compilers add this duo, by default, to all functions you write, unless you explicitly override that. This likely explains why many developers (myself included!) had never heard of this concept. If we do hear about it, it might instead be referred to as something like "function call overhead". These largely invisible functions are added at compile time, or they are managed by the interpreter and the developer is none the wiser.
In the case of a Prologue, it is responsible for making sure the old states are saved before the target function is called and setting up the new stack for the call. The flow looks something like this:
1. Save the current return address and caller-saved registers.
2. Allocate some stack space for local variables.
3. Set up a frame pointer (if needed).
The function's Epilogue is invoked at the **end** of the function's life. Its job is to restore things to their previous state before the current function was called and to then return control back to the caller. The flow looks something like:
1. Restore all previously saved registers state.
2. Deallocate any stack space.
3. Jump back to the calling function.
An example in RISC-V Assembly
In RISC-V assembly, function prologues and epilogues need to be explicitly written, to manage the stack. We carry out the same logic we talked about before, but since this is Assembly, we're wholly responsible for shuffling around register values and pointers ourselves.
Here’s an example of a `fox_boop` function with the basic prologue and epilogue code:
fox_boop:
addi sp, sp, -16 # we allocate stack space
sw ra, 12(sp) # we save the return address
sw s0, 8(sp) # then we save old frame pointer
addi s0, sp, 16 # lastly we set the new frame pointer
[... some function-y stuff goes here ...]
lw ra, 12(sp) # we restore the previous return address
lw s0, 8(sp) # we restore the previous frame pointer
addi sp, sp, 16 # de-allocate stack space
ret # return to calling function
When might you avoid using Prologues and Epilogues?
If they are added by default and, generally, keep our stacks and program memory space safe and clean, why would we ever want to actively go around these guardrails? There are a few scenarios where it would make sense to halt their usage (and would be required to in order to have correct code in some instances):
1. **Leaf Functions:** If a function does not call any other functions, then it might not need to save registers. In the case of inline functions, compilers are smart enough and avoid using Prologue and Epilogue (ie. you don't usually need to explicitly state that).
2. **Interrupt Handlers or Low-Level Code:** In real-time or interrupt-driven applications, minimizing stack operations can reduce latency. Within Operating System's kernel development, custom prologue/epilogues might be required or none at all.
3. **Embedded & Real-Time Systems:** Some embedded systems avoid frame pointers to save memory and execution time. Bare-metal programming will often require managing the stack manually.
Function Prologue and Epilogue avoidance in C
In C, function Prologues and Epilogues are inserted automatically by the compiler. If you want to go around this and NOT use them for a function (say your writing some Assembly code inline for a kernel), then you can do this by adding an `__attribute__` to the function with an argument `naked`, like so:
__attribute__((naked))
int fox_boop(int count)
{
// boop the fox 42 times and return new boop count
return count + 42;
}
In summary, avoiding unnecessary prologues/epilogues can lead to significant optimizations, but programmers should use caution when it comes to manually interviening. Understanding these sort of low-level details, even if you never write a kernel or program on bare metal, can make you a better programmer, more aware of what's going on under the hood. It allows you to write more efficient and optimized code in light of that knowledge. Hope you found this helpful!
© 2024-present by Andie Keller.
Content of this website is licensed under CC BY-NC-SA 4.0.
Crafted with 💖. Built with gempost.