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
- Assembly basics (see A friendly introduction to assembly)
- Registers basics (EIP/EBP/ESP) (see Assembly - Registers)
- Memory layout basics (see Owning Memory: Memory Layout Basics)
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.
- Prologue: instructions that set up a new stack frame.
- Epilogue: instructions that tear it down and return to the caller.
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:
- pushes the return address (saved
EIP) and - 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