x86 Assembly: The Stack Explained

Reverse Engineering Advanced πŸ“… Published: 20/07/2025

The stack is arguably the most critical concept in computer architecture and assembly language programming. Understanding how the stack works is es...

x86 Assembly: The Stack Explained - Complete Guide

The stack is arguably the most critical concept in computer architecture and assembly language programming. Understanding how the stack works is essential for reverse engineering, exploit development, debugging, and low-level programming. This comprehensive guide will take you from basic concepts to advanced stack manipulation techniques.

Table of Contents

Stack Fundamentals

What is the Stack?

The stack is a Last-In-First-Out (LIFO) data structure that serves as temporary storage for:

  • Function parameters - Arguments passed to functions
  • Return addresses - Where to return after function completion
  • Local variables - Variables with automatic storage duration
  • Saved registers - Preserved register values
  • Temporary data - Intermediate calculations and scratch space

Stack Analogy: The Plate Stack

Imagine a stack of plates in a cafeteria:

  • πŸ“₯ PUSH: Add a new plate to the top
  • πŸ“€ POP: Remove the top plate
  • πŸ” PEEK: Look at the top plate without removing it
  • ⚠️ You can only access the topmost plate directly

Stack Architecture in x86

Memory Layout

High Memory Addresses (0xFFFFFFFF)
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚      Command Line       β”‚
    β”‚      Environment        β”‚
    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
    β”‚                         β”‚ ← ESP points here (top of stack)
    β”‚         STACK           β”‚ ↑ Stack grows DOWNWARD
    β”‚      (grows down)       β”‚ β”‚ (toward lower addresses)
    β”‚                         β”‚ β”‚
    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚
    β”‚                         β”‚
    β”‚         HEAP            β”‚
    β”‚      (grows up)         β”‚
    β”‚                         β”‚
    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
    β”‚         DATA            β”‚
    β”‚     (initialized)       β”‚
    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
    β”‚         BSS             β”‚
    β”‚    (uninitialized)      β”‚
    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
    β”‚         TEXT            β”‚
    β”‚        (code)           β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Low Memory Addresses (0x00000000)

Key Stack Registers

ESP (Extended Stack Pointer)

  • Always points to the top of the stack
  • Automatically updated by PUSH/POP instructions
  • Points to the last item pushed (lowest address in use)
  • 32-bit register in x86, 64-bit (RSP) in x64

EBP (Extended Base Pointer)

  • Points to the base of current stack frame
  • Used as a stable reference point for accessing local variables
  • Also called "frame pointer"
  • Manually managed by programmer/compiler

Stack Growth Direction

Critical Concept: On x86, the stack grows downward (toward lower memory addresses):

Initial state:
ESP = 0x7FFE1000  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                  β”‚             β”‚ 0x7FFE1000 ← ESP
                  β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
                  β”‚             β”‚ 0x7FFE0FFC
                  β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
                  β”‚             β”‚ 0x7FFE0FF8
                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
After PUSH EAX:
ESP = 0x7FFE0FFC  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                  β”‚             β”‚ 0x7FFE1000
                  β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
                  β”‚    EAX      β”‚ 0x7FFE0FFC ← ESP (moved down!)
                  β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
                  β”‚             β”‚ 0x7FFE0FF8
                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Core Stack Instructions

PUSH Instruction

Decrements ESP and stores data at the new location:

; PUSH syntax variations
push eax           ; Push register contents
push 0x41414141    ; Push immediate value  
push [ebp+8]       ; Push memory contents
push word 0x1234   ; Push 16-bit value
; What PUSH EAX actually does:
sub esp, 4         ; Decrement stack pointer by 4 bytes
mov [esp], eax     ; Store EAX at new ESP location

POP Instruction

Retrieves data from stack top and increments ESP:

; POP syntax variations  
pop eax            ; Pop into register
pop [ebp-4]        ; Pop into memory location
pop word [ebx]     ; Pop 16-bit value
; What POP EAX actually does:
mov eax, [esp]     ; Load value from stack top
add esp, 4         ; Increment stack pointer by 4 bytes

Stack Pointer Manipulation

; Direct ESP manipulation
add esp, 12        ; Remove 3 DWORDs from stack (cleanup)
sub esp, 20        ; Allocate 20 bytes of stack space
mov esp, ebp       ; Restore stack pointer (common in epilogue)
; Equivalent to multiple POPs but faster
add esp, 16        ; Same as: pop eax; pop ebx; pop ecx; pop edx

Function Calls and the Stack

CALL Instruction Deep Dive

The CALL instruction performs two critical operations:

; CALL function_address does:
push eip           ; Push return address (next instruction)
jmp function_address ; Jump to function
; Example:
0x08048100: call 0x08048200    ; Call function at 0x08048200
0x08048105: mov eax, ebx       ; This address gets pushed as return address
; Stack after CALL:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 0x08048105  β”‚ ← Return address pushed by CALL
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

RET Instruction Deep Dive

The RET instruction reverses the CALL operation:

; RET does:
pop eip            ; Pop return address into instruction pointer
; (processor jumps to this address)
; RET with immediate value:
ret 12             ; Same as: pop eip; add esp, 12
                   ; Used to clean up parameters pushed by caller

Complete Function Call Example

; Calling function with parameters
section .text
caller:
    ; Push parameters (right to left for cdecl)
    push 30            ; Parameter 2
    push 25            ; Parameter 1  
    call add_numbers   ; Call function
    add esp, 8         ; Clean up parameters (2 Γ— 4 bytes)
    ; EAX now contains return value
add_numbers:
    ; Function prologue
    push ebp           ; Save caller's frame pointer
    mov ebp, esp       ; Set up new frame pointer
    ; Access parameters relative to EBP
    mov eax, [ebp+8]   ; First parameter (25)
    add eax, [ebp+12]  ; Add second parameter (30)
    ; Function epilogue  
    mov esp, ebp       ; Restore stack pointer
    pop ebp            ; Restore caller's frame pointer
    ret                ; Return to caller

Stack Frames in Detail

Stack Frame Structure

Each function call creates a new "stack frame" containing all the function's data:

High Addresses
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Parameter n   β”‚ [ebp + 4n + 4]
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚   Parameter 2   β”‚ [ebp + 12]
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€  
β”‚   Parameter 1   β”‚ [ebp + 8]
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Return Address  β”‚ [ebp + 4]    ← Pushed by CALL
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Saved EBP     β”‚ [ebp]        ← EBP points here
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Local Variable 1β”‚ [ebp - 4]
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Local Variable 2β”‚ [ebp - 8]    
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Local Variable nβ”‚ [ebp - 4n]   ← ESP points near here
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Low Addresses

Standard Function Prologue

function_start:
    push ebp           ; Save caller's frame pointer
    mov ebp, esp       ; Establish new frame pointer
    sub esp, 16        ; Allocate space for local variables
    ; Optional: save registers that will be modified
    push edi
    push esi
    push ebx

Standard Function Epilogue

function_end:
    ; Optional: restore saved registers
    pop ebx
    pop esi  
    pop edi
    mov esp, ebp       ; Deallocate local variables
    pop ebp            ; Restore caller's frame pointer
    ret                ; Return to caller

Accessing Function Parameters

; Function: int multiply(int a, int b, int c)
multiply:
    push ebp
    mov ebp, esp
    ; Access parameters (assuming cdecl calling convention)
    mov eax, [ebp+8]   ; a (first parameter)
    mov ebx, [ebp+12]  ; b (second parameter)  
    mov ecx, [ebp+16]  ; c (third parameter)
    ; Perform multiplication
    imul eax, ebx      ; a * b
    imul eax, ecx      ; (a * b) * c
    ; Return value in EAX
    mov esp, ebp
    pop ebp
    ret

Calling Conventions

cdecl (C Declaration)

  • Parameter order: Right to left
  • Stack cleanup: Caller responsibility
  • Return value: EAX (integers), ST(0) (floats)
  • Preserved registers: EBX, ESI, EDI, EBP, ESP
; cdecl example: func(1, 2, 3)
push 3             ; Last parameter first
push 2
push 1             ; First parameter last
call func
add esp, 12        ; Caller cleans up (3 Γ— 4 bytes)

stdcall (Standard Call)

  • Parameter order: Right to left
  • Stack cleanup: Callee responsibility
  • Return value: EAX
  • Used by: Windows API functions
; stdcall example: CreateFileA(...)
push 0             ; hTemplateFile
push 0x80          ; dwFlagsAndAttributes  
push 3             ; dwCreationDisposition
push 0             ; lpSecurityAttributes
push 0             ; dwShareMode
push 0x80000000    ; dwDesiredAccess
push filename      ; lpFileName
call CreateFileA   ; Function cleans up stack automatically

fastcall Convention

  • First two parameters: ECX and EDX registers
  • Additional parameters: Stack (right to left)
  • Stack cleanup: Callee responsibility
  • Performance benefit: Reduces stack operations
; fastcall example: func(a, b, c, d)
push d             ; Only last two parameters on stack
push c
mov edx, b         ; Second parameter in EDX
mov ecx, a         ; First parameter in ECX
call func

Practical Examples

Example 1: Simple Calculator Function

; Calculator: result = (a + b) * c
calculator:
    push ebp
    mov ebp, esp
    sub esp, 4         ; Space for local variable
    ; Get parameters
    mov eax, [ebp+8]   ; a
    add eax, [ebp+12]  ; a + b
    mov [ebp-4], eax   ; Store intermediate result
    mov eax, [ebp-4]   ; Load intermediate result
    imul eax, [ebp+16] ; Multiply by c
    ; EAX contains final result
    mov esp, ebp
    pop ebp
    ret
; Usage:
main:
    push 5             ; c = 5
    push 3             ; b = 3  
    push 2             ; a = 2
    call calculator    ; Result: (2+3)*5 = 25 in EAX
    add esp, 12

Example 2: String Length Function

; Calculate string length (like strlen)
string_length:
    push ebp
    mov ebp, esp
    push edi           ; Save EDI
    mov edi, [ebp+8]   ; Get string pointer
    xor eax, eax       ; Length counter = 0
length_loop:
    cmp byte [edi], 0  ; Check for null terminator
    je length_done     ; Jump if end of string
    inc edi            ; Next character
    inc eax            ; Increment length
    jmp length_loop
length_done:
    pop edi            ; Restore EDI
    mov esp, ebp
    pop ebp
    ret
; Usage:
push message           ; Push string address
call string_length    ; Length returned in EAX
add esp, 4

Example 3: Recursive Factorial

; Recursive factorial calculation
factorial:
    push ebp
    mov ebp, esp
    mov eax, [ebp+8]   ; Get parameter n
    cmp eax, 1         ; Base case: n <= 1
    jle factorial_base
    ; Recursive case: n * factorial(n-1)
    dec eax            ; n - 1
    push eax           ; Push n-1 as parameter
    call factorial     ; Recursive call
    add esp, 4         ; Clean up parameter
    mul dword [ebp+8]  ; EAX = factorial(n-1) * n
    jmp factorial_end
factorial_base:
    mov eax, 1         ; factorial(0) = factorial(1) = 1
factorial_end:
    mov esp, ebp
    pop ebp
    ret
; Usage: Calculate 5!
push 5
call factorial         ; Result: 120 in EAX
add esp, 4

Stack Debugging Techniques

Examining Stack in Debugger

; GDB commands for stack analysis
(gdb) info registers esp ebp    ; Show stack pointers
(gdb) x/10x $esp               ; Examine 10 words at ESP
(gdb) x/10x $ebp               ; Examine 10 words at EBP
(gdb) backtrace                ; Show call stack
(gdb) frame 1                  ; Switch to specific frame
(gdb) info locals              ; Show local variables
(gdb) info args                ; Show function arguments
; x64dbg commands
ESP in Stack window            ; View stack contents
Alt+F9                        ; Stack trace window
Ctrl+Alt+S                    ; Search in stack

Manual Stack Walking

; Walking the stack manually to trace function calls
walk_stack:
    mov ebp, [ebp]     ; Follow saved EBP chain
    cmp ebp, 0         ; Check for end of chain
    je walk_done
    mov eax, [ebp+4]   ; Get return address
    ; Process return address (log, analyze, etc.)
    jmp walk_stack
walk_done:
    ret

Stack Canaries (Security Feature)

; Modern compiler stack protection
function_with_canary:
    push ebp
    mov ebp, esp
    sub esp, 0x10      ; Local variables
    ; Compiler inserts canary
    mov eax, [gs:0x14] ; Thread-local canary value
    mov [ebp-4], eax   ; Store canary on stack
    ; ... function body ...
    ; Check canary before return
    mov eax, [ebp-4]   ; Load canary from stack
    xor eax, [gs:0x14] ; Compare with original
    jne stack_overflow ; Jump if corrupted
    mov esp, ebp
    pop ebp
    ret
stack_overflow:
    call __stack_chk_fail  ; Terminate program

Security Implications

Buffer Overflow Vulnerabilities

Stack-based buffer overflows occur when data exceeds allocated buffer space:

; Vulnerable function
vulnerable_function:
    push ebp
    mov ebp, esp
    sub esp, 64        ; 64-byte buffer
    ; [ebp-64] to [ebp-1] = buffer space
    ; [ebp] = saved EBP
    ; [ebp+4] = return address ← Target for overflow
    ; Dangerous: no bounds checking
    mov edi, ebp
    sub edi, 64        ; Buffer start
    mov esi, [ebp+8]   ; Source string
copy_loop:
    lodsb              ; Load byte from source
    stosb              ; Store to buffer (NO BOUNDS CHECK!)
    test al, al
    jnz copy_loop      ; Continue until null terminator
    mov esp, ebp
    pop ebp
    ret                ; Returns to potentially overwritten address

Stack Smashing Protection

; Safe alternative with bounds checking
safe_function:
    push ebp
    mov ebp, esp
    sub esp, 64
    push 63            ; Maximum copy length
    lea eax, [ebp-64]  ; Buffer address
    push eax
    push dword [ebp+8] ; Source string
    call safe_strcpy   ; Bounds-checked copy function
    add esp, 12
    mov esp, ebp
    pop ebp
    ret

Return Address Protection

  • Stack Canaries: Detect corruption before return
  • Address Space Layout Randomization (ASLR): Randomize stack location
  • Data Execution Prevention (DEP): Make stack non-executable
  • Control Flow Integrity (CFI): Validate return addresses

Advanced Stack Techniques

Variable-Length Argument Lists

; Implementing printf-like functions
; int sum_integers(int count, ...)
sum_integers:
    push ebp
    mov ebp, esp
    mov ecx, [ebp+8]   ; Get count parameter
    lea esi, [ebp+12]  ; Point to first variadic argument
    xor eax, eax       ; Sum accumulator
sum_loop:
    test ecx, ecx      ; Check if more arguments
    jz sum_done
    add eax, [esi]     ; Add current argument
    add esi, 4         ; Move to next argument
    dec ecx            ; Decrement count
    jmp sum_loop
sum_done:
    mov esp, ebp
    pop ebp
    ret
; Usage: sum_integers(4, 10, 20, 30, 40) = 100
push 40
push 30
push 20
push 10
push 4                 ; Count of arguments
call sum_integers
add esp, 20           ; Clean up 5 parameters

Stack-Based Memory Allocation

; Allocating large local arrays
large_array_function:
    push ebp
    mov ebp, esp
    ; Allocate 1000 integers (4000 bytes)
    sub esp, 4000
    ; Initialize array
    lea edi, [ebp-4000] ; Array start
    mov ecx, 1000       ; Element count
    xor eax, eax        ; Value to store
    rep stosd           ; Fill array with zeros
    ; Use array...
    mov dword [ebp-4000], 42    ; array[0] = 42
    mov dword [ebp-3996], 100   ; array[1] = 100
    ; Cleanup is automatic when ESP is restored
    mov esp, ebp
    pop ebp
    ret

Coroutines and Stack Switching

; Simple coroutine implementation
coroutine_yield:
    ; Save current context
    pushf              ; Save flags
    push eax
    push ebx
    push ecx
    push edx
    push esi
    push edi
    push ebp
    ; Save stack pointer
    mov [current_coroutine.esp], esp
    ; Switch to next coroutine
    mov esp, [next_coroutine.esp]
    ; Restore context
    pop ebp
    pop edi
    pop esi
    pop edx
    pop ecx
    pop ebx
    pop eax
    popf
    ret                ; "Return" to different function!

Performance Considerations

Stack vs Heap Allocation

AspectStackHeap
SpeedVery FastSlower
Size LimitLimited (~1-8MB)Large
FragmentationNonePossible
ManagementAutomaticManual
LocalityExcellentVariable

Stack Optimization Tips

  • Minimize local variables: Use registers when possible
  • Avoid deep recursion: Can cause stack overflow
  • Align stack data: Improves performance on modern CPUs
  • Use leaf functions: Functions that don't call others are faster

Common Stack-Related Bugs

Stack Corruption

; Bug: Mismatched push/pop
buggy_function:
    push eax
    push ebx
    ; ... some code ...
    pop eax            ; BUG: Should be pop ebx first!
    pop ebx            ; Values are swapped
    ret

Stack Imbalance

; Bug: Unbalanced stack operations
unbalanced_function:
    push eax
    push ebx
    ; ... some code ...
    pop eax            ; BUG: Missing pop ebx
    ret                ; ESP is now incorrect!

Accessing Invalid Stack Data

; Bug: Using EBP incorrectly
parameter_bug:
    push ebp
    mov ebp, esp
    mov eax, [ebp+4]   ; BUG: This is saved EBP, not parameter!
    ; Should be [ebp+8] for first parameter
    mov esp, ebp
    pop ebp
    ret

Tools for Stack Analysis

Static Analysis Tools

  • IDA Pro: Function analysis and stack frame reconstruction
  • Ghidra: Free alternative with excellent decompilation
  • Radare2: Open-source reverse engineering framework

Dynamic Analysis Tools

  • x64dbg: Windows debugger with stack window
  • GDB: Linux debugger with stack analysis commands
  • Intel Pin: Dynamic binary instrumentation
  • Valgrind: Memory error detection (Linux)

Exercises and Practice

Exercise 1: Stack Trace Implementation

Implement a function that prints the current call stack by walking the EBP chain.

Exercise 2: Safe String Functions

Write bounds-checked versions of strcpy, strcat, and sprintf that prevent buffer overflows.

Exercise 3: Custom Calling Convention

Design and implement a custom calling convention optimized for your specific use case.

Conclusion

The stack is fundamental to understanding how programs execute at the assembly level. Mastering stack concepts enables you to:

  • Debug complex programs by understanding call flows and data storage
  • Reverse engineer software by analyzing function parameters and local variables
  • Write efficient assembly code with proper stack management
  • Understand security vulnerabilities like buffer overflows and ROP attacks
  • Optimize performance by minimizing stack operations

The stack is more than just a data structureβ€”it's the foundation that makes function calls, local variables, and program modularity possible. Whether you're debugging a crash, analyzing malware, or writing high-performance code, understanding the stack is essential.

Next Steps: Practice with a debugger, analyze real programs, and experiment with different calling conventions. The more you work with the stack, the more intuitive it becomes!