Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Function Stack Frame Creation and Destruction

Tech May 8 3

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:

  1. Function parameters and return values
  2. Temporary variables (including non-static local variables and compiler-generated temporaries)
  3. 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 esp accordingly pop: Pop data from stack to specified location, modifying esp accordingly sub: Subtraction operation add: Addition operation call: Function call: 1. Push return address 2. Jump to target function jump: Modify eip to transfer control to target function ret: Restore return address, push to eip, similar to pop 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 ebp onto the stack

ebp = esp (now ebp points to the stack top, which contains the old ebp)

sub esp, xxx (allocate xxx bytes of temporary space on the stack)

push xxx (save registers named xxx as needed)

Pushing ebp onto the stack facilitates restoration of the previous ebp value 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 saved ebp value 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:

  1. Push main's ebp onto the stack
  2. Calculate new ebp and esp
  3. Save ebx, esi, edi register values
  4. Compute the sum, accessing function parameters via ebp offsets
  5. Store the computed sum in the eax register 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.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.