Function Prologue Decomposition Methodology
When you open an x86 function, do not read line-by-line. Read the prologue as one block and answer these five questions before anything else.
The five questions
- Where is the frame?
- Look for
push ebp→mov ebp, esp→sub esp, N. -
This establishes the stack frame. Everything below
ebpis local storage; everything above is arguments. See Stack Frames and Function Prologue/Epilogue. -
What lives on the stack?
var_Xat negative offsets = local variables.arg_Xat positive offsets = function arguments. See Decoding IDA-Style Argument Offsets.-
Positive offsets start at
[ebp+8]because[ebp+4]is the return address. -
Which registers got saved?
-
push ebx / esi / edimeans the function plans to use them and must restore them later. -
What got loaded first?
-
The first
movfrom arguments tells you which values the function cares about immediately. -
What is the first decision?
- Look for the first
cmp/testfollowed by a conditional jump. That's the first branch in the C code. See Reading Conditional Branches in Assembly.
Worked example
encrypt_func proc near
var_8 = dword ptr -8
var_2 = byte ptr -2
var_1 = byte ptr -1
arg_0 = dword ptr 8
arg_4 = dword ptr 0Ch
arg_8 = dword ptr 10h
arg_C = dword ptr 14h
arg_10 = dword ptr 18h
arg_14 = byte ptr 1Ch
push ebp
mov ebp, esp
sub esp, 8
push ebx
push esi
mov esi, [ebp+arg_C] ; esi = 4th argument
mov bl, 64h ; bl = 'd'
push edi
mov edi, [ebp+arg_4] ; edi = 2nd argument
add esi, edi ; esi = arg4 + arg2
cmp [ebp+arg_14], 0 ; if (6th argument == 0)
mov [ebp+var_8], ecx ; local = ecx
mov [ebp+var_1], bl ; local = 'd'
mov [ebp+var_2], bl ; local = 'd'
mov [ebp+arg_4], esi ; overwrite arg2 slot with (arg4 + arg2)
jz short loc_7A46B6 ; jump if flag is zero
Reading it with the method
Frame: push ebp / mov ebp, esp / sub esp, 8 — 8 bytes of locals.
Stack layout:
| Offset | Name | Type | Meaning |
|---|---|---|---|
[ebp-8] |
var_8 |
dword | local variable |
[ebp-2] |
var_2 |
byte | local byte |
[ebp-1] |
var_1 |
byte | local byte |
[ebp+8] |
arg_0 |
dword | 1st argument |
[ebp+12] |
arg_4 |
dword | 2nd argument |
[ebp+16] |
arg_8 |
dword | 3rd argument |
[ebp+20] |
arg_C |
dword | 4th argument |
[ebp+24] |
arg_10 |
dword | 5th argument |
[ebp+28] |
arg_14 |
byte | 6th argument |
Saved registers: ebx, esi, edi.
First loads:
- esi = arg_C → 4th argument.
- edi = arg_4 → 2nd argument.
- Then esi = esi + edi → computes arg4 + arg2.
First decision:
- cmp [ebp+arg_14], 0 followed by jz → if (6th_argument == 0) goto loc_7A46B6;.
C-like reading
int encrypt_func(int arg0, int arg2, int arg3, int arg4,
int arg5, char flag)
{
int local_8 = ecx; // var_8
char local_1 = 'd'; // var_1
char local_2 = 'd'; // var_2
int combined = arg4 + arg2;
arg2 = combined; // compiler reused arg_4 slot
if (flag == 0) {
goto loc_7A46B6;
}
// ...
}
The compiler has already decided to combine the 2nd and 4th arguments before it even checks the flag. That tells you the combined value is needed no matter which branch is taken.