Function Stack Frame Creation and Destruction
What is a Function Stack Frame?
During coding, we frequently abstract independent functionality into functions. A C program fundamentally consists of functions as its basic units.
Questions arise naturally: How are local variables created? Why do uninitialized local variables contain garbage values? How do function calls work? How are return values delivered? What is the underlying mechanism for parameter passing?
Understanding function stack frames provides the answers to all these questions.
Function Stack Frame (stack frame)
A function stack frame refers to the memory space allocated on the call stack during function invocation. This space serves to store:
- Function parameters and return values
- Temporary variables (including non-static local variables and compiler-generated temporaries)
- Context preservation information (registers that must remain unchanged across function calls)
Stack Fundamentals
The stack represents one of the most critical concepts in modern computer programming. Nearly every program utilizes the stack—without stacks, there would be no functions, no local variables, and no modern programming languages as we know them.
In classic computer science, a stack is defined as a special container where users can push data onto the stack and pop data off the stack.
Stack Frame Example
The stack maintains the maintenance information required for function calls, commonly known as stack frames or activation records.
This container follows a strict rule: data pushed first comes out last (First In Last Out, FILO).
In computer systems, the stack is a dynamically allocated memory region with these properties. Programs can push data onto the stack and pop data from the stack top. Push operations grow the stack while pop operations shrink it.
In conventional operating systems, the stack grows downward (from high addresses to low addresses). On i386 or x86-64 architectures, the stack top is positioned using the esp register.
Register Overview
eax: General-purpose register, holds temporary data, commonly used for return values ebx: General-purpose register, holds temporary data ebp: Stack base register esp: Stack top register eip: Instruction register, stores the address of the next instruction
Assembly Instructions
mov: Data transfer instruction push: Push data onto stack, modifying
espaccordingly pop: Pop data from stack to specified location, modifyingespaccordingly sub: Subtraction operation add: Addition operation call: Function call: 1. Push return address 2. Jump to target function jump: Modifyeipto transfer control to target function ret: Restore return address, push toeip, similar topop eip
Introduction to Function Stack Frames
Every function invocation allocates memory space on the stack to preserve variable values during the call. This allocated space is called the runtime stack (function stack frame).
This space is maintained using two registers: esp and ebp. The ebp register tracks the stack base address (pointing to a fixed position in the function's activation record), while esp tracks the stack top address.
The ebp register is also known as the frame pointer.
Activation Record Illustration
Data following the parameters (including the parameters themselves) constitutes the current function's activation record. The ebp register stays fixed at a constant position and does not change during function execution, while esp always points to the stack top and changes continuously as the function executes.
This fixed ebp can locate various data within the function's activation record: before ebp lies the return address (at ebp-4), and before that are the pushed parameters (at ebp-8, ebp-12, etc., depending on parameter count and size). The data directly pointed to by ebp is the previous ebp value from the calling function, enabling ebp restoration when the function returns.
Function Call Procedure
Push some or all parameters onto the stack; if some parameters are not stacked, use specific registers for passing
Push the address of the next instruction onto the stack
Jump to the function body for execution
Push
ebponto the stack
ebp = esp(nowebppoints to the stack top, which contains the oldebp)
sub esp, xxx(allocatexxxbytes of temporary space on the stack)
push xxx(save registers namedxxxas needed)Pushing
ebponto the stack facilitates restoration of the previousebpvalue upon return. Saving registers is necessary because compilers may require certain registers to remain unchanged across calls. The function can push these register values at the start and pop them at the end. The "standard" epilogue mirrors the "standard" prologue in reverse order.
pop xxx(restore saved registers if necessary)
pop ebp(restore savedebpvalue from stack)
ret(retrieve return address from stack and jump to that location)
Note: The GCC compiler has an option -fomit-frame-pointer that eliminates frame pointers, calculating frame variable positions directly from esp. This has trade-offs: the benefit is having an extra ebp available, while the drawbacks are slower frame addressing and losing the ability to trace function call trajectories accurately.
Pushing data onto the stack decreases esp, and popping data increases esp.
Directly decreasing esp allocates space on the stack, while directly increasing esp deallocates space.
Stack frame space remains occupied until the function returns. When recursive calls occur, each recursive level allocates its own stack frame space, which gets released layer by layer during the unwinding phase. Deep recursion can consume excessive stack frame space, potentially causing stack overflow.
The creation and destruction of function stack frames follows similar patterns across different compilers. The following demonstrates this process using VS2019.
Implementation
Sample Code
#include <stdio.h>
int Add(int left, int right)
{
int result = 0;
result = left + right;
return result;
}
int main()
{
int num1 = 3;
int num2 = 5;
int sum = 0;
sum = Add(num1, num2);
printf("%d\n", sum);
return 0;
}
Procedure
In Visual Studio, navigate to Debug > Windows > Call Stack.
The call stack displays function call logic. It becomes evident that before main executes, it is called by invoke_main. Prior to invoke_main, there may be additional callers, but invoke_main maintains its own stack frame, as do main and Add functions. Each function's stack frame is maintained by its own ebp and esp registers.
How is main's stack frame created? The following explains the process.
Note: To observe stack frame creation and destruction clearly, perform the following preparation:
Start debugging and step to the first executable line of main. Right-click and select "Go to Disassembly."
Note: VS compiler reassigns memory for each debugging session. Disassembly code varies slightly between sessions.
main Function
Stack Frame Creation
int main()
{
// Stack frame creation
00BE1820 push ebp
00BE1821 mov ebp,esp
00BE1823 sub esp,0E4h
00BE1829 push ebx
00BE182A push esi
00BE182B push edi
00BE182C lea edi,[ebp-24h]
00BE182F mov ecx,9
00BE1834 mov eax,0CCCCCCCCh
00BE1839 rep stos dword ptr es:[edi]
// Core main function code
int num1 = 3;
00BE183B mov dword ptr [ebp-8],3
int num2 = 5;
00BE1842 mov dword ptr [ebp-14h],5
int sum = 0;
00BE1849 mov dword ptr [ebp-20h],0
sum = Add(num1, num2);
00BE1850 mov eax,dword ptr [ebp-14h]
00BE1853 push eax
00BE1854 mov ecx,dword ptr [ebp-8]
00BE1857 push ecx
00BE1858 call 00BE10B4
00BE185D add esp,8
00BE1860 mov dword ptr [ebp-20h],eax
printf("%d\n", sum);
00BE1863 mov eax,dword ptr [ebp-20h]
00BE1866 push eax
00BE1867 push 0BE7B30h
00BE186C call 00BE10D2
00BE1871 add esp,8
return 0;
00BE1874 xor eax,eax
}
Each line's meaning:
00BE1820 push ebp
This line pushes the ebp register value onto the stack. At this point, ebp contains invoke_main's ebp. The stack pointer decrements by 4 (moving from high to low addresses).
00BE1821 mov ebp,esp
The mov instruction copies esp into ebp, effectively establishing main's ebp. This value equals invoke_main's stack top.
00BE1823 sub esp,0E4h
The sub instruction subtracts 0xE4 from esp, producing a new stack pointer. Combined with the previous ebp and current esp, these two registers now maintain a contiguous stack region—this is main's allocated stack frame space. This region stores local variables, temporary data, and debugging information.
00BE1829 push ebx // Push ebx value, esp-4
00BE182A push esi // Push esi value, esp-4
00BE182B push edi // Push edi value, esp-4
These three instructions save the values of three registers onto the stack. These registers might be modified during subsequent function execution, so their original values are preserved for restoration upon exit.
Stack Frame Initialization
00BE182C lea edi,[ebp-24h] // Load ebp-0x24 address into edi
00BE182F mov ecx,9 // Move 9 into ecx
00BE1834 mov eax,0CCCCCCCCh // Move 0xCCCCCCCC into eax
00BE1839 rep stos dword ptr es:[edi] // Initialize memory from ebp-0x24 to ebp as 0xCC per byte
These four lines are equivalent to the following pseudocode:
edi = ebp-0x24;
ecx = 9;
eax = 0xCCCCCCCC;
for(; ecx != 0; --ecx, edi += 4) {
*(int*)edi = eax;
}
Note: "Garbage characters" phenomenon
#include <stdio.h>
int main()
{
char buffer[20];
printf("%s\n", buffer);
return 0;
}
Why does this code output garbage characters? When main is called, memory in the allocated stack region is initialized to 0xCC for every byte. The buffer array, being uninitialized, happens to occupy this space. Two consecutive 0xCC bytes form the Chinese character encoding for "烫" (garbage character). Therefore, 0xCCCC displays as "烫" when interpreted as text.
Local Variable Creation and Initialization
int num1 = 3;
00BE183B mov dword ptr [ebp-8],3 // Store 3 at ebp-8, where num1 resides
int num2 = 5;
00BE1842 mov dword ptr [ebp-14h],5 // Store 5 at ebp-14h, where num2 resides
int sum = 0;
00BE1849 mov dword ptr [ebp-20h],0 // Store 0 at ebp-20h, where sum resides
These assembly instructions demonstrate the creation and initialization of num1, num2, and sum. Local variables are allocated within the stack frame space of their containing function.
Add Function
Function Call
sum = Add(num1, num2); // Call Add function
Parameter Passing
Parameter passing involves pushing arguments onto the stack frame.
00BE1850 mov eax,dword ptr [ebp-14h] // Load num2 (5 at ebp-14h) into eax
00BE1853 push eax // Push eax value, esp-4
00BE1854 mov ecx,dword ptr [ebp-8] // Load num1 (3 at ebp-8) into ecx
00BE1857 push ecx // Push ecx value, esp-4
Jump and Call
00BE1858 call 00BE10B4
00BE185D add esp,8
00BE1860 mov dword ptr [ebp-20h],eax
The call instruction performs the function call. Before executing call, the address of the instruction following call is pushed onto the stack. This ensures that after the function returns, execution resumes at the next instruction.
Add Function Disassembly
int Add(int left, int right)
{
00BE1760 push ebp
00BE1761 mov ebp, esp
00BE1763 sub esp, 0CCh
00BE1769 push ebx
00BE176A push esi
00BE176B push edi
int result = 0;
00BE176C mov dword ptr[ebp - 8], 0
result = left + right;
00BE1773 mov eax, dword ptr[ebp + 8]
00BE1776 add eax, dword ptr[ebp + 0Ch]
00BE1779 mov dword ptr[ebp - 8], eax
return result;
00BE177C mov eax, dword ptr[ebp - 8]
}
00BE177F pop edi
00BE1780 pop esi
00BE1781 pop ebx
00BE1782 mov esp, ebp
00BE1784 pop ebp
00BE1785 ret
When execution reaches Add, its stack frame must be created. Stack frame creation in Add follows the same pattern as main, differing only in allocated space size.
The sequence:
- Push
main'sebponto the stack- Calculate new
ebpandesp- Save
ebx,esi,ediregister values- Compute the sum, accessing function parameters via
ebpoffsets- Store the computed sum in the
eaxregister for return
Detailed line-by-line analysis:
Stack Frame Maintenance
00BE1760 push ebp // Save main's ebp, esp-4
00BE1761 mov ebp,esp // Assign main's esp to new ebp, now Add's ebp
00BE1763 sub esp,0CCh // Subtract 0xCC from esp, calculating Add's esp
Push Operations
00BE1769 push ebx // Push ebx value, esp-4
00BE176A push esi // Push esi value, esp-4
00BE176B push edi // Push edi value, esp-4
Variable Creation
int result = 0;
00BE176C mov dword ptr [ebp-8],0 // Store 0 at ebp-8, creating result
Computation
result = left + right;
00BE1773 mov eax,dword ptr [ebp+8] // Load value at ebp+8 into eax
00BE1776 add eax,dword ptr [ebp+0Ch] // Add value at ebp+12 to eax
00BE1779 mov dword ptr [ebp-8],eax // Store eax result at ebp-8
Return Value
return result;
00BE177C mov eax,dword ptr [ebp-8] // Move result to eax for return via register
The diagram shows that left and right (labeled as a' and b') correspond to the formal parameters x and y of Add. This demonstrates parameter passing mechanics and confirms that value semantics apply during parameter passing—formal parameters are copies of actual parameters, so modifications to formals do not affect actuals.
Stack Frame Destruction
When a function call completes and returns, the previously created stack frame begins destruction:
00BE177F pop edi // Pop stack top into edi, esp+4
00BE1780 pop esi // Pop stack top into esi, esp+4
00BE1781 pop ebx // Pop stack top into ebx, esp+4
00BE1782 mov esp,ebp // Assign Add's ebp to esp, deallocating Add's stack frame
00BE1784 pop ebp // Pop stack top into ebp, restoring main's ebp, esp+4
00BE1785 ret // Pop return address from stack top into eip, esp+4, jump to return address
After returning from Add to main, execution continues:
00BE185D add esp,8 // Add 8 to esp, skipping num1' and num2' on stack
00BE1860 mov dword ptr [ebp-20h],eax // Store eax value at ebp-0x20, updating main's sum variable
When returning built-in types, values are typically carried back via registers. For larger objects, the calling function allocates space on its stack frame, then implicitly passes this address to the called function. The called function writes the return value directly into the caller's reserved space using that address.