< BACK TO TERMINAL

Owning Memory: Understanding the Stack Frame

Introduction

Now that we’ve mapped the segments in memory, we can zoom in on the stack and see what really happens during a function call.

Prerequisites

Remember the stack?

Previously, we have seen that the stack is a LIFO (Last-In First-Out) memory segment used for function calls, local variables, and temporary values, and that the stack grows downward (towards lower addresses).

Stack Frame Mechanics

When a function is called, a new stack frame is created at the top of the stack. When the function returns, the stack frame is destroyed. You can visually represent this with the following layout.

Prologue and Epilogue

To build and destroy a stack frame, the code typically has a prologue and an epilogue.

Between the prologue and epilogue, the function body is executed (what the function specifically does).

Arguments

To pass arguments to a function, the caller pushes them onto the stack before calling the function. The callee accesses them using positive offsets relative to EBP.

On x64 architectures, the arguments are usually passed in registers, and only the remaining ones are passed on the stack when there are too many arguments (registers first).

Example

For better comprehension, some parts of the code have been removed.

main:
    push    ebp                ; [Prologue] Save previous base pointer
    mov     ebp, esp           ; [Prologue] Set new base pointer for main

    push    20                 ; Push 2nd argument
    push    10                 ; Push 1st argument
    call    add_func           ; Call add_func (pushes return address)
    add     esp, 8             ; Caller cleans 2 arguments (2 * 4 bytes)

    mov     esp, ebp           ; [Epilogue] Restore stack pointer
    pop     ebp                ; [Epilogue] Restore previous base pointer
    ret                        ; [Epilogue] Return to the caller

add_func:
    push    ebp                ; [Prologue] Save caller's base pointer
    mov     ebp, esp           ; [Prologue] Set new base pointer for add_func

    mov     eax, [ebp+8]       ; Arg1 (10)
    add     eax, [ebp+12]      ; Arg2 (20) => EAX = 30

    mov     esp, ebp           ; [Epilogue] Restore stack pointer
    pop     ebp                ; [Epilogue] Restore previous base pointer
    ret                        ; Return with result in EAX

How to call a function?

To invoke a function, the caller pushes arguments onto the stack. The call instruction then:

  1. pushes the return address (saved EIP) and
  2. transfers control to the callee.

Instructions like push automatically adjust ESP (the stack grows downward).

push    20                 ; Push 2nd argument
push    10                 ; Push 1st argument
call    add_func           ; Call add_func

The Prologue

push    ebp                ; Save caller's EBP
mov     ebp, esp           ; Establish this function's stack frame

The body

The function code now executes and can access arguments using addresses relative to EBP.

mov     eax, [ebp+8]       ; Arg1 (10)
add     eax, [ebp+12]      ; Arg2 (20) => EAX = 30

The Epilogue

mov     esp, ebp           ; [Epilogue] Restore stack pointer
pop     ebp                ; [Epilogue] Restore previous base pointer
ret                        ; [Epilogue] Return to the caller

Now, the stack is the same as before the call instruction. If needed, we can cleanup the arguments.

add     esp, 8

Next steps

See Owning Memory: Basic Stack Buffer Overflow

← Previous articleNext article →