The Function Prologue and Epilogue
The push rbp / mov rbp, rsp setup and its matching teardown, and what a stack frame is.
The Function Prologue and Epilogue
You have already met these lines in passing. Almost every unoptimized function on x86-64 opens with the same two instructions and closes with a matching pair:
push rbp ; \ prologue
mov rbp, rsp ; /
... ; the actual function body
pop rbp ; epilogue
ret
That opening is the prologue and that closing is the epilogue. Their job is to set up and tear down a stack frame: the slice of stack memory the function uses for its own local data. This lesson explains exactly what those instructions do and why.
Meet rbp, the Frame Pointer
In the last lesson we used rsp, the stack pointer, which moves around constantly as values are pushed and popped. That movement is a problem if you want a stable way to refer to your local variables: an offset that is correct at the start of the function might be wrong later once rsp has shifted.
The solution is a second register dedicated to this:
rbpis the base pointer (also called the frame pointer). The prologue parks it at a fixed spot for the duration of the call, and the function then refers to its locals and arguments as offsets fromrbp, such as-4[rbp]. Becauserbpdoes not move, those offsets stay valid no matter whatrspdoes.
Reading the Prologue Line by Line
Here is a function with a couple of locals, and its -O0 listing:
#include <iostream>
int compute(int a, int b)
{
int sum{a + b};
int product{a * b};
return sum + product;
}
int main()
{
std::cout << compute(3, 4) << '\n';
return 0;
}
compute(int, int):
push rbp
mov rbp, rsp
mov DWORD PTR -20[rbp], edi
mov DWORD PTR -24[rbp], esi
mov edx, DWORD PTR -20[rbp]
mov eax, DWORD PTR -24[rbp]
add eax, edx
mov DWORD PTR -4[rbp], eax
mov eax, DWORD PTR -20[rbp]
imul eax, DWORD PTR -24[rbp]
mov DWORD PTR -8[rbp], eax
mov edx, DWORD PTR -4[rbp]
mov eax, DWORD PTR -8[rbp]
add eax, edx
pop rbp
ret
The first two lines are the prologue:
push rbp
mov rbp, rsp
push rbpsaves the caller's base pointer onto the stack. The caller had its own frame, and we must not destroy itsrbp; pushing it means we can restore it later. (As a bonus, this is also what keepsrbpvalues forming a chain back through every caller, which is what a debugger walks to print a backtrace.)mov rbp, rspthen pointsrbpat the current top of the stack. From this instant on,rbpmarks the base of our frame, and it will not move again until we leave.
After these two lines, the frame is established. Everything the function touches, the spilled arguments a and b at -20[rbp] and -24[rbp], and the locals sum and product at -4[rbp] and -8[rbp], is addressed relative to that fixed rbp.
What a Stack Frame Is
A stack frame is simply the region of the stack belonging to one function call. It typically holds:
- the saved caller
rbp(just pushed), - the function's local variables,
- copies of arguments the function chose to store on the stack,
- and, sitting just above the frame, the return address that
callpushed.
Every active function call has exactly one frame, and they sit stacked on top of each other in the order the calls were made, which is the picture from the previous lesson.
Making Room: sub rsp
compute above did not need to reserve extra space; the small "red zone" below rsp was enough, so GCC skipped it. But functions often grow the frame explicitly. Recall outer from the previous lesson:
outer(int):
push rbp
mov rbp, rsp
sub rsp, 8
...
leave
ret
The extra sub rsp, 8 is the third common prologue move: it lowers rsp to carve out 8 bytes of working space below rbp. (Remember: subtracting from rsp grows the stack.) So the fuller prologue pattern is "save rbp, set rbp, optionally sub rsp to reserve locals."
Reading the Epilogue
The epilogue undoes the prologue, in reverse, just before returning. There are two forms, and you will see both.
The first form, used by compute:
pop rbp
ret
pop rbprestores the caller's base pointer that we saved at the very start, and simultaneously pops it off the stack sorspis back where it belongs.retpops the return address and jumps back to the caller.
The second form, used by outer:
leave
ret
leave is a single instruction that does the whole teardown at once: it sets rsp back to rbp (discarding everything we reserved with sub rsp) and then pops rbp. It is exactly equivalent to mov rsp, rbp followed by pop rbp. Compilers reach for leave when the frame was grown with sub rsp, and for a plain pop rbp when it was not. Either way, the meaning is identical: "restore the caller's frame and prepare to return."
Why You Care
Recognising the prologue and epilogue lets you instantly find the boundaries of a function and mentally skip the bookkeeping to get to the real work. When you open a listing, your eyes should jump straight past push rbp / mov rbp, rsp at the top and leave / ret (or pop rbp / ret) at the bottom, the same way you skip the braces of a C++ function. And when these lines are missing, as they often are at -O2, that itself tells you something: the optimizer decided this function did not need a frame at all.
Key Takeaways
- The prologue (
push rbp/mov rbp, rsp) sets up a stack frame; the epilogue tears it down. rbpis the frame pointer: parked at a fixed spot so locals and arguments can be addressed as stable offsets like-4[rbp].push rbpsaves the caller's frame pointer;mov rbp, rspmakesrbpthe base of the new frame.sub rsp, NreservesNbytes of local space by growing the stack downward.- The epilogue is either
pop rbp/ret, orleave/ret;leaveequalsmov rsp, rbpthenpop rbp. - A stack frame holds the saved
rbp, locals, and stored arguments for one function call.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
The Function Prologue and Epilogue - Quiz
Test your understanding of the lesson.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!