Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Analyzing Function Stack Frame Creation and Destruction via Disassembly in C

Tech 1

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 esp register.

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; updates esp.
  • pop: Pops data from the stack to a destination; updates esp.
  • 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 sets eip to 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:

  1. Set a breakpoint at main().
  2. Press F10 (or Fn+F10 on laptops) to step through.
  3. 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:

  1. Push old ebp

    00EE18D0  push        ebp
    
    • Saves the previous function’s base pointer.
    • esp decreases (stack grows downward).
  2. Set ebp to current esp

    00EE18D1  mov         ebp, esp
    
    • Establishes ebp as the base of the new stack frame.
  3. Allocate stack space

    00EE18D3  sub         esp, 0E4h
    
    • Allocates 228 bytes (0xE4) for local variables and debugging info.
    • Now ebp and esp define the boundaries of the main function’s stack frame.
  4. 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 push reduces esp by 4 bytes.
  5. Initialize stack memory with 0xCCCCCCCC

    00EE18DC  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] with 0xCCCCCCCC.
    • This pattern is a debug marker used by Visual Studio to detect uninitialized memory.

    Why 0xCCCC appears as "烫" in output? Because 0xCC corresponds to the UTF-8 encoding of the Chinese character "烫". So uninitialzed arrays filled with 0xCC print as "烫烫烫...".

  6. 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.
  7. Prepare arguments for Add

    00EE190A  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.
  8. Call Add function

    00EE1912  call        00EE10B9
    
    • Two actions:
      1. Push the return address (00EE1917).
      2. Jump to Add.

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 new ebp, allocate space, preserve registers, initialize stack.
  1. Initialize local variable z

    00EE17B5  mov         dword ptr [ebp-8], 0
    
    • Sets z = 0.
  2. 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 = eax
    
    • ebp+8 and ebp+0Ch access the parameter values pushed earlier.
    • Since parameters were pushed right-to-left, x is at ebp+8, y at ebp+0Ch.
    • Result (30) is stored in eax.
  3. 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.
    • eax remains valid after function exit.

3.3.5 Destroying the Stack Frame in Add

  1. Restore saved registers

    00EE17C8  pop         edi
    00EE17C9  pop         esi
    00EE17CA  pop         ebx
    
    • Restores the original values of ebx, esi, edi.
  2. Reset esp to ebp

    00EE17D8  mov         esp, ebp
    
    • Cleans up the stack space allocated for the function.
  3. Pop old ebp

    00EE17DA  pop         ebp
    
    • Restores the caller’s base pointer.
  4. Return to caller

    00EE17DB  ret
    
    • Pops the return address from the stack and jumps to it.
  5. 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 x and y.
  6. Store return value in main

    00EE191A  mov         dword ptr [ebp-20h], eax
    
    • Copies the return value (30) from eax into c.

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

  1. How are local variables created?

    • A stack frame is allocated. Local variables are assigned space within this frame.
  2. Why are uninitialized locals random?

    • Stack memory is initialized with 0xCCCCCCCC for debugging. Uninitialized variables contain this pattern, which prints as "烫烫烫...".
  3. How are parameters passed? In what order?

    • Parameters are copied and pushed right-to-left onto the stack before the function call.
  4. 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.
  5. How does function calling work? How is the return value returned?

    • call pushes the return address and jumps to the function.
    • ret pops 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.

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.