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:

  • rbp is 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 from rbp, such as -4[rbp]. Because rbp does not move, those offsets stay valid no matter what rsp does.

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 rbp saves the caller's base pointer onto the stack. The caller had its own frame, and we must not destroy its rbp; pushing it means we can restore it later. (As a bonus, this is also what keeps rbp values forming a chain back through every caller, which is what a debugger walks to print a backtrace.)
  • mov rbp, rsp then points rbp at the current top of the stack. From this instant on, rbp marks 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 call pushed.

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 rbp restores the caller's base pointer that we saved at the very start, and simultaneously pops it off the stack so rsp is back where it belongs.
  • ret pops 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.
  • rbp is the frame pointer: parked at a fixed spot so locals and arguments can be addressed as stable offsets like -4[rbp].
  • push rbp saves the caller's frame pointer; mov rbp, rsp makes rbp the base of the new frame.
  • sub rsp, N reserves N bytes of local space by growing the stack downward.
  • The epilogue is either pop rbp / ret, or leave / ret; leave equals mov rsp, rbp then pop rbp.
  • A stack frame holds the saved rbp, locals, and stored arguments for one function call.