When a function is called, a stack frame is created to support the function’s execution. The stack frame contains the function’s local variables and the arguments passed to the function by its caller. The frame also contains housekeeping information that allows the called function (the callee) to return to the caller safely. The exact contents and layout of the stack vary by processor architecture and function call convention.
The stack is a last in first out (LIFO) structure, the data stored first is retrieved last. Values are placed onto the stack via push
and removed via pop
.
To keep track the stack, the system uses the base pointer ebp
and the stack pointer esp
, whereas esp
points to the top of the stack and ebp
points to the bottom of the stack. In the Intel architecture, as program adds data to the stack, the stack grows downward from high memory to low memory. When items removed from the stack, stack shrinks upward from low to high memory. When a word value is pushed onto the stack, the memory address of esp
decreases 2 bytes and when a word value is popped off the stack, the memory address of esp
increases 2 bytes. When a double word value is pushed onto/popped off the stack, the memory address of esp
decreases/increases 4 bytes.
So when we say “top of the stack” on x86, we actually mean the lowest address in the memory area occupied by the stack.
To push new data onto the stack we use the push
instruction. What push
does is first decrement esp
by 4, and then store its operand in the location esp
points to.
push eax
is actually equivalent to this:
sub esp,4
mov [esp],eax
Similarly, the pop
instruction takes a value off the top of stack and places it in its operand, increasing the stack pointer afterwards.
pop eax
is actually equivalent to this:
mov eax,[esp]
add esp,4
Function has three components: function prologue, function body, and function epilogue. The purpose of function prologue is to save the previous state of the program and set up the stack for the local variables of the function. The function body is usually responsible for some kind of unique and specific task. This part of the function may contains various instructions, branches (jumps) to other functions, etc. The function epilogue is used to restore the program’s state to its initial.
Let’s take a look at an example
;;; ;------------------------------------------------------
;;; ;------------------------------------------------------
;;; ; _foobar: needs 3 arguments. The arguments are pushed onto the stack before calling this function.
;;; ;
;;; ; Examples:
;;; ; push 110
;;; ; push 45
;;; ; push 67
;;; ; call _foobar
;;; ;
;;; ;REGISTERS MODIFIED: EAX
;;; ;------------------------------------------------------
;;; ;------------------------------------------------------
_foobar:
; ebp must be preserved across calls. Since
; this function modifies it, it must be
; saved.
;
push ebp
; From now on, ebp points to the current stack
; frame of the function
;
mov ebp, esp
; Make space on the stack for local variables
;
sub esp, 16
; eax <-- a. eax += 2. then store eax in xx
;
mov eax, DWORD[ebp+8]
add eax, 2
mov DWORD[ebp-4], eax
; eax <-- b. eax += 3. then store eax in yy
;
mov eax, DWORD[ebp+12]
add eax, 3
mov DWORD[ebp-8], eax
; eax <-- c. eax += 4. then store eax in zz
;
mov eax, DWORD[ebp+16]
add eax, 4
mov DWORD[ebp-12], eax
; add xx + yy + zz and store it in sum
;
mov eax, DWORD[ebp-8]
mov edx, DWORD[ebp-4]
lea eax, [edx+eax]
add eax, DWORD[ebp-12]
mov DWORD[ebp-16], eax
; Compute final result into eax, which
; stays there until return
;
mov eax, DWORD[ebp-4]
imul eax, DWORD[ebp-8]
imul eax, DWORD[ebp-12]
add eax, DWORD[ebp-16]
; The leave instruction here is equivalent to:
;
; mov esp, ebp
; pop ebp
;
; Which cleans the allocated locals and restores
; ebp.
;
leave
ret
section .text
global _start
_start:
push 34
push 59
push 98
call _foobar
mov ebx, 0
mov eax, 1
int 0x80
Let’s examine the stack
push 34
push 59
push 98
First, three values are pushed onto the stack
call _foobar
Next, we call the function _foobar. To return to the caller, a function must have the correct return address.
call
instruction will push the return address onto the stack before jumping to the target address.
push ebp
This instruction saves the value of register ebp
to the stack.
mov ebp,esp
This instruction will make register ebp
point to the same location as register esp
.
sub esp,16
This instruction makes empty space on the stack for local variables.
mov eax, DWORD[ebp+8]
add eax, 2
mov DWORD[ebp-4], eax
mov eax, DWORD[ebp+12]
add eax, 3
mov DWORD[ebp-8], eax
mov eax, DWORD[ebp+16]
add eax, 4
mov DWORD[ebp-12], eax
mov eax, DWORD[ebp-8]
mov edx, DWORD[ebp-4]
lea eax, [edx+eax]
add eax, DWORD[ebp-12]
mov DWORD[ebp-16], eax
mov eax, DWORD[ebp-4]
imul eax, DWORD[ebp-8]
imul eax, DWORD[ebp-12]
add eax, DWORD[ebp-16]
leave
After executing instruction above, local variables will be removed from the stack, register esp
will point to the return address, and register ebp
will be back to its initial value.
ret
The ret
instruction pops the return address off the stack and returns control from a function to the calling program.