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

  1. Where is the frame?
  2. Look for push ebp → mov ebp, esp → sub esp, N.
  3. This establishes the stack frame. Everything below ebp is local storage; everything above is arguments. See Stack Frames and Function Prologue/Epilogue.

  4. What lives on the stack?

  5. var_X at negative offsets = local variables.
  6. arg_X at positive offsets = function arguments. See Decoding IDA-Style Argument Offsets.
  7. Positive offsets start at [ebp+8] because [ebp+4] is the return address.

  8. Which registers got saved?

  9. push ebx / esi / edi means the function plans to use them and must restore them later.

  10. What got loaded first?

  11. The first mov from arguments tells you which values the function cares about immediately.

  12. What is the first decision?

  13. Look for the first cmp / test followed 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.