Analyzing Function Stack Frame Creation and Destruction via Disassembly in C
What Is a Function Stack Frame?
A function stack frame is a data structure used in computer programs to implement function calls. During a function call, each function creates a new stack frame in memory to store local variables, return addresses, and parameters.
Typically, a function stack frame includes:
- Local Variable Table: Stores the function's local variables, including basic types (like integers and floating-point numbers) and object references (such as pointers).
- Return Address: Holds the address where execution should resume after the function completes.
- Parameter Table: Stores input arguments, usually arranged in the order they are passed.
- Operand Stack: Used for temporary data and intermediate results during computation, typically managed as a stack.
When a function is called, a new stack frame is created and pushed onto the call stack. Upon completion, this frame is popped off and destroyed. Thus, the stack frame plays a crucial role in storing and passing data during function calls.
The implementation of stack frames depends on the programming language and compiler. In high-level languages, compilers automatically manage stack frame creation and destruction, requiring no manual intervention from developers. In low-level languages or when manually managing memory, programmers must handle these operations explicitly.
Why Understanding Stack Frames Matters
Understanding how stack frames are created and destroyed helps clarify several key concepts:
- How are local variables created?
- Why do uninitialized local variables contain random values?
- How are function parameters passed? In what order?
- How are formal parameters and actual arguments instantiated?
- How does function calling work? How is the return value brought back?
Let’s dive into the process of stack frame creation and destruction.
Detailed Analysis of Stack Frame Creation and Destruction
3.1 What Is a Stack?
The stack is one of the most fundamental concepts in modern computing. Almost every program uses it; without a stack, there would be no functions, no local variables, and no contemporary programming languages.
- In classical computer science, a stack is a special container where data can be pushed (added to the top) and popped (removed from the top). The stack follows the Last In, First Out (LIFO) principle—data that was pushed first is accessed last.
- In computer systems, the stack is a dynamic memory region with these properties. Programs can push data onto the stack or pop it off. Pushing increases the stack size, while popping decreases it.
In classic operating systems, the stack grows downward—from higher addresses to lower ones. On common architectures like i386 or x86-64, the stack pointer is tracked by the
espregister.
3.2 Key Registers and Assembly Instructions
3.2.1 Important Registers
eax: General-purpose register, often used to hold temporary data and return values.ebx: General-purpose register, used for temporary storage.ebp: Base pointer register, points to the base (bottom) of the current stack frame.esp: Stack pointer register, points to the top of the stack.eip: Instruction pointer, holds the address of the next instruction to execute.
3.2.2 Common Assembly Instructions
mov: Data transfer instruction.push: Pushes data onto the stack; updatesesp.pop: Pops data from the stack to a destination; updatesesp.add: Addition operation.sub: Subtraction operation.lea: Load Effective Address, loads the address of a memory operand.call: Calls a function: 1. Pushes the return address onto the stack; 2. Jumps to the target function.ret: Returns from a function: pops the return address from the stack and setseipto it.
3.3 Step-by-Step Stack Frame Lifecycle
3.3.1 Prerequisites
Every function call requires a dedicated stack frame. This space is managed using two registers:
ebp: Points to the base (bottom) of the current stack frame.esp: Points to the top of the stack.
Stack space grows from high addresses to low addresses.
We’ll use Visual Studio 2019 for demonstration.
3.3.2 Sample Code and Setup
#include <stdio.h>
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int c = 0;
c = Add(a, b);
printf("%d\n", c);
return 0;
}
To analyze the disassembly:
- Set a breakpoint at
main(). - Press F10 (or Fn+F10 on laptops) to step through.
- Open the Call Stack, Disassembly, Memory, and Watch windows.
You’ll see the execution flow begin at line 13, then enter invoke_main, which calls main().
3.3.3 Creating the Stack Frame in main
After entering main(), the following steps occur:
-
Push old
ebp00EE18D0 push ebp- Saves the previous function’s base pointer.
espdecreases (stack grows downward).
-
Set
ebpto currentesp00EE18D1 mov ebp, esp- Establishes
ebpas the base of the new stack frame.
- Establishes
-
Allocate stack space
00EE18D3 sub esp, 0E4h- Allocates 228 bytes (
0xE4) for local variables and debugging info. - Now
ebpandespdefine the boundaries of themainfunction’s stack frame.
- Allocates 228 bytes (
-
Save caller-saved registers
00EE18DC push ebx 00EE18DA push esi 00EE18DB push edi- These registers may be modified within
main, so their original values are preserved. - Each
pushreducesespby 4 bytes.
- These registers may be modified within
-
Initialize stack memory with
0xCCCCCCCC00EE18DC lea edi, [ebp-24h] 00EE18DF mov ecx, 9 00EE18E4 mov eax, 0CCCCCCCCh 00EE18E9 rep stos dword ptr es:[edi]- This loop initializes 9 * 4 = 36 bytes starting from
[ebp - 0x24]with0xCCCCCCCC. - This pattern is a debug marker used by Visual Studio to detect uninitialized memory.
Why
0xCCCCappears as "烫" in output? Because0xCCcorresponds to the UTF-8 encoding of the Chinese character "烫". So uninitialzed arrays filled with0xCCprint as "烫烫烫...". - This loop initializes 9 * 4 = 36 bytes starting from
-
Initialize local variables
00EE18F5 mov dword ptr [ebp-8], 0Ah // a = 10 00EE18FC mov dword ptr [ebp-14h], 14h // b = 20 00EE1903 mov dword ptr [ebp-20h], 0 // c = 0- Values are stored at offsets relative to
ebp. The exact offsets depend on the compiler. - Variables are not necessarily stored contiguously.
- Values are stored at offsets relative to
-
Prepare arguments for
Add00EE190A mov eax, dword ptr [ebp-14h] // load b (20) 00EE190D push eax // push b 00EE190E mov ecx, dword ptr [ebp-8] // load a (10) 00EE1911 push ecx // push a- Parameters are pushed right-to-left (standard calling convention).
- These are copies of the actual arguments.
-
Call
Addfunction00EE1912 call 00EE10B9- Two actions:
- Push the return address (
00EE1917). - Jump to
Add.
- Push the return address (
- Two actions:
3.3.4 Entering Add Function
Upon entry:
00EE1790 push ebp
00EE1791 mov ebp, esp
00EE1793 sub esp, 0CCh
00EE1799 push ebx
00EE179A push esi
00EE179B push edi
00EE179C lea edi, [ebp-0Ch]
00EE179F mov ecx, 3
00EE17A4 mov eax, 0CCCCCCCCh
00EE17A9 rep stos dword ptr es:[edi]
- Similar setup: save old
ebp, set newebp, allocate space, preserve registers, initialize stack.
-
Initialize local variable
z00EE17B5 mov dword ptr [ebp-8], 0- Sets
z = 0.
- Sets
-
Compute sum
00EE17BC mov eax, dword ptr [ebp+8] // x = [ebp+8] 00EE17BF add eax, dword ptr [ebp+0Ch] // eax += y 00EE17C2 mov dword ptr [ebp-8], eax // z = eaxebp+8andebp+0Chaccess the parameter values pushed earlier.- Since parameters were pushed right-to-left,
xis atebp+8,yatebp+0Ch. - Result (
30) is stored ineax.
-
Return value preparation
00EE17C5 mov eax, dword ptr [ebp-8] // move result to eax- The return value is placed in
eax—the standard convention for integer returns. eaxremains valid after function exit.
- The return value is placed in
3.3.5 Destroying the Stack Frame in Add
-
Restore saved registers
00EE17C8 pop edi 00EE17C9 pop esi 00EE17CA pop ebx- Restores the original values of
ebx,esi,edi.
- Restores the original values of
-
Reset
esptoebp00EE17D8 mov esp, ebp- Cleans up the stack space allocated for the function.
-
Pop old
ebp00EE17DA pop ebp- Restores the caller’s base pointer.
-
Return to caller
00EE17DB ret- Pops the return address from the stack and jumps to it.
-
Clean up argument space
00EE185D add esp, 8- Removes the 8 bytes (two 4-byte parameters) from the stack.
- This destroys the temporary copies of
xandy.
-
Store return value in
main00EE191A mov dword ptr [ebp-20h], eax- Copies the return value (
30) fromeaxintoc.
- Copies the return value (
Final Stack State Diagram
Note: For large objects, the return mechanism differs—memory is pre-allocated in the caller’s stack frame, and a pointer to it is passed to the callee. See Chapter 10, Section 2 of The Ancient Art of Programming for details.
Summary and Answers to Common Questions
-
How are local variables created?
- A stack frame is allocated. Local variables are assigned space within this frame.
-
Why are uninitialized locals random?
- Stack memory is initialized with
0xCCCCCCCCfor debugging. Uninitialized variables contain this pattern, which prints as "烫烫烫...".
- Stack memory is initialized with
-
How are parameters passed? In what order?
- Parameters are copied and pushed right-to-left onto the stack before the function call.
-
How are formal and actual parameters instantiated?
- Formal parameters are separate memory locations holding copies of actual arguments. Changing them doesn’t affect the originals.
-
How does function calling work? How is the return value returned?
callpushes the return address and jumps to the function.retpops the return address and resumes execution.- Return values are passed via registers (e.g.,
eax). The caller reads the value from the register after the call.