Understanding the stack frames
A stack frame is a dedicated section of the program’s call stack that is created every time a function is called. It stores all the information required for that function to execute correctly, such as return addresses, saved registers, and local variables. Stack frames are what allow programs to safely handle nested function calls and recursion.
This how a stack frame looks like

Before diving into code, there are two important assumptions we will use throughout this explanation. First, the stack grows toward lower memory addresses. Second, every function must store the base address of its own stack frame, which marks where that function’s execution context begins in memory.
The logic is straightforward. The program starts execution in main(), prints a message, and then calls the function x(), which prints another message. Although the behavior is simple, this program clearly demonstrates how stack frames are created and destroyed during function calls.
Assembly Output (High-Level View)
When this program is compiled, each function is translated into assembly instructions. For now, we are not trying to learn assembly language, but rather understand what is happening conceptually behind these instructions.
Both main() and x() begin with the same two instructions:
These two lines are responsible for creating a stack frame.
Program Startup and main() Stack Frame Creation
main() Stack Frame CreationWhen the program starts, the operating system sets up an initial stack and then transfers control to main(). At this moment, the stack pointer register (RSP) points to the top of the stack, but main() does not yet have its own stack frame.
The first instruction, pushq %rbp, saves the old base pointer onto the stack. Because the stack grows downward, this operation decreases RSP. Saving the old base pointer ensures that the program can later return safely to the caller.
The next instruction, movq %rsp, %rbp, establishes a new base pointer for main(). From this point on, RBP marks the beginning of main()’s stack frame. Conceptually, the stack now looks like this:
At this stage, main() has its own well-defined execution context.
Executing Code Inside main()
main()When main() calls printf, that function creates its own stack frame, executes, and then destroys it upon returning. Once printf finishes, main()’s stack frame remains unchanged. Nothing special happens to main()’s frame during this call.
Calling x() from main()
x() from main()Next, main() calls the function x(). The call x instruction performs two critical actions automatically. It pushes the return address (the instruction to resume execution in main()) onto the stack, and then transfers control to x().
At this moment, the stack contains main()’s frame along with the return address:
Stack Frame Creation for x()
x()Once execution enters x(), the same prologue instructions appear again. The old base pointer (which belongs to main()) is pushed onto the stack, and a new base pointer is set for x().
Now the stack has two stack frames, one for main() and one for x():
At this point, each function has an independent view of the stack. This isolation is what makes nested calls and recursion possible, as each function knows exactly where its own data begins.
Returning from x()
x()After x() finishes executing its printf call, it reaches the function epilogue. The popq %rbp instruction restores main()’s base pointer, effectively destroying x()’s stack frame. The ret instruction then pops the return address from the stack and jumps back to main().
The stack is now back to the state it was in before x() was called, as if x() never existed.
Exiting main()
main()Finally, main() performs the same cleanup steps. It restores the caller’s base pointer and returns control to the operating system. At this point, the program terminates.
Last updated