3

Consider folowing example code:

program code


build/program-x86:     file format elf32-i386

Disassembly of section my_text:

080a9dfc <subroutine_fnc>:
 80a9dfc:   55                      push   %ebp
 80a9dfd:   89 e5                   mov    %esp,%ebp
 80a9dff:   57                      push   %edi
 80a9e00:   56                      push   %esi
 80a9e01:   53                      push   %ebx
 80a9e02:   83 ec 14                sub    $0x14,%esp        // 20 bytes for local variables
 80a9e05:   c7 45 e0 00 00 00 00    movl   $0x0,-0x20(%ebp)  // zero local variable at address bp-0x20
 80a9e0c:   8d 7d f3                lea    -0xd(%ebp),%edi   // pointer in area of the local variables
 80a9e0f:   8b 75 0c                mov    0xc(%ebp),%esi    // 2. parameter
 80a9e12:   83 c6 30                add    $0x30,%esi        // add ascii ASCII '0' to the parameter

 80a9e15:   ba 01 00 00 00          mov    $0x1,%edx         // constatnt 1
 80a9e1a:   8b 5d 08                mov    0x8(%ebp),%ebx    // 1. function parameter
 80a9e1d:   89 f9                   mov    %edi,%ecx         // local buffer
 80a9e1f:   b8 03 00 00 00          mov    $0x3,%eax         // syscall read
 80a9e24:   cd 80                   int    $0x80             // read(par1, ptr to local var, 1)
 80a9e26:   83 f8 01                cmp    $0x1,%eax         // return value is 1?
 80a9e29:   74 0c                   je     80a9e37 <subroutine_fnc+0x3b>  // yes
 80a9e2b:   bb 01 00 00 00          mov    $0x1,%ebx
 80a9e30:   b8 01 00 00 00          mov    $0x1,%eax
 80a9e35:   cd 80                   int    $0x80             // no exit(1)

 80a9e37:   0f b6 45 f3             movzbl -0xd(%ebp),%eax   // expand value to 32 bits
 80a9e3b:   3c 2f                   cmp    $0x2f,%al         // is value < ASCII '0'
 80a9e3d:   7e 17                   jle    80a9e56 <subroutine_fnc+0x5a>  // yes end of the subroutine
 80a9e3f:   0f be d0                movsbl %al,%edx
 80a9e42:   39 f2                   cmp    %esi,%edx         // is value above >= par2 + '0'
 80a9e44:   7d 10                   jge    80a9e56 <subroutine_fnc+0x5a>  // yes => end
 80a9e46:   8b 45 0c                mov    0xc(%ebp),%eax    // read again param2
 80a9e49:   0f af 45 e0             imul   -0x20(%ebp),%eax  // multiply ebp-0x20 by param2
 80a9e4d:   8d 54 10 d0             lea    -0x30(%eax,%edx,1),%edx // add result with read character - ASCII '0'
 80a9e51:   89 55 e0                mov    %edx,-0x20(%ebp)  // store result to the local variable at ebp-0x20
 80a9e54:   eb bf                   jmp    80a9e15 <subroutine_fnc+0x19>  // repeat

 80a9e56:   8b 45 e0                mov    -0x20(%ebp),%eax  // function returns value from local variable at ebp-0x20
 80a9e59:   83 c4 14                add    $0x14,%esp
 80a9e5c:   5b                      pop    %ebx
 80a9e5d:   5e                      pop    %esi
 80a9e5e:   5f                      pop    %edi
 80a9e5f:   5d                      pop    %ebp
 80a9e60:   c3                      ret    

080a9e61 <toplevel_fnc>:
 80a9e61:   55                      push   %ebp
 80a9e62:   89 e5                   mov    %esp,%ebp
 80a9e64:   57                      push   %edi
 80a9e65:   56                      push   %esi
 80a9e66:   53                      push   %ebx
 80a9e67:   83 ec 20                sub    $0x20,%esp         // reserve stack space for local variables
 80a9e6a:   c6 45 f3 41             movb   $0x41,-0xd(%ebp)   // store ASCII 'A' at ebp-0xd
 80a9e6e:   c7 44 24 04 0a 00 00    movl   $0xa,0x4(%esp)     // store 10 to the first 32-bit slot bellow stack top
 80a9e75:   00 
 80a9e76:   c7 04 24 00 00 00 00    movl   $0x0,(%esp)        // store zero to the stack top
 80a9e7d:   e8 7a ff ff ff          call   80a9dfc <subroutine_fnc> // call subroutine_fnc(0,10)
 80a9e82:   89 c7                   mov    %eax,%edi          // store result
 80a9e84:   ba 80 01 00 00          mov    $0x180,%edx
 80a9e89:   b9 42 02 00 00          mov    $0x242,%ecx
 80a9e8e:   be 00 7f 0c 08          mov    $0x80c7f00,%esi    // setup pointer to "data"
 80a9e93:   89 f3                   mov    %esi,%ebx
 80a9e95:   b8 05 00 00 00          mov    $0x5,%eax          // syscall open
 80a9e9a:   cd 80                   int    $0x80              // open("data", 0x242, 0x180)
 80a9e9c:   89 45 dc                mov    %eax,-0x24(%ebp)   // store result to ebp-0x24
 80a9e9f:   85 c0                   test   %eax,%eax          // set flags according to the eax test
 80a9ea1:   79 0e                   jns    80a9eb1 <toplevel_fnc+0x50>  // sign is not set (>=0)
 80a9ea3:   b8 01 00 00 00          mov    $0x1,%eax
 80a9ea8:   89 c3                   mov    %eax,%ebx
 80a9eaa:   b8 01 00 00 00          mov    $0x1,%eax          // syscall exit
 80a9eaf:   cd 80                   int    $0x80              // exit(1)

 80a9eb1:   89 7d e0                mov    %edi,-0x20(%ebp)   // store subroutine_fnc result into ebp-0x20
 80a9eb4:   8d 75 f3                lea    -0xd(%ebp),%esi    // ebp-0xd is pointer to the 'A' character
 80a9eb7:   eb 22                   jmp    80a9edb <toplevel_fnc+0x7a>

 80a9eb9:   8b 5d dc                mov    -0x24(%ebp),%ebx   // fill ebx by open result (fd)
 80a9ebc:   89 f1                   mov    %esi,%ecx          // pointer to 'A'
 80a9ebe:   ba 01 00 00 00          mov    $0x1,%edx
 80a9ec3:   b8 04 00 00 00          mov    $0x4,%eax          // syscall write
 80a9ec8:   cd 80                   int    $0x80              // write(fd from open, "A", 1)
 80a9eca:   85 c0                   test   %eax,%eax          // check result
 80a9ecc:   79 09                   jns    80a9ed7 <toplevel_fnc+0x76>
 80a9ece:   89 d3                   mov    %edx,%ebx          // setup sign
 80a9ed0:   b8 01 00 00 00          mov    $0x1,%eax
 80a9ed5:   cd 80                   int    $0x80              // exit(1)
 80a9ed7:   83 6d e0 01             subl   $0x1,-0x20(%ebp)   // subtract 1 from ebp-0x20
 80a9edb:   83 7d e0 00             cmpl   $0x0,-0x20(%ebp)   // value 0 reached
 80a9edf:   75 d8                   jne    80a9eb9 <toplevel_fnc+0x58> // no => repeat

 80a9ee1:   8b 5d dc                mov    -0x24(%ebp),%ebx   // fd from open syscall
 80a9ee4:   b8 06 00 00 00          mov    $0x6,%eax          // syscall close
 80a9ee9:   cd 80                   int    $0x80              // close(fd from open)
 80a9eeb:   85 c0                   test   %eax,%eax          // test result
 80a9eed:   79 0e                   jns    80a9efd <toplevel_fnc+0x9c>
 80a9eef:   b8 01 00 00 00          mov    $0x1,%eax
 80a9ef4:   89 c3                   mov    %eax,%ebx
 80a9ef6:   b8 01 00 00 00          mov    $0x1,%eax
 80a9efb:   cd 80                   int    $0x80              // for error exit exit(1)

 80a9efd:   89 f8                   mov    %edi,%eax          // restore saved result of
                                                                  // subroutine_fnc call
 80a9eff:   83 c4 20                add    $0x20,%esp
 80a9f02:   5b                      pop    %ebx
 80a9f03:   5e                      pop    %esi
 80a9f04:   5f                      pop    %edi
 80a9f05:   5d                      pop    %ebp
 80a9f06:   c3                      ret    

program data


build/program-x86:     file format elf32-i386

Contents of section my_data:
 80c7f00 64617461 00                          data.           

How can I, in general, learn what arguments does subroutine_fnc use? I am interested in general approach to this. I understand it may not always be 100% possible, but I'm interesting in learning the basics at least.

3

The basics

1. Requisite information: calling convention

In order to determine how arguments are passed to functions, the calling convention must be known.

Function calling conventions depend on the target architecture and the compiler1 (see also: Calling conventions for different C++ compilers and operating systems by Agner Fog). It is not so important that the compiler used to create the code being disassembled above is not explicitly stated because there is enough information in the output to determine target architecture and calling convention.

From the disassembly above, we observe that the instruction set is x86, and the calling convention is cdecl.

2. Identifying the calling convention

In this case we can deduce the calling convention from the above disassembly. We observe behavior that conforms to what is expected of callee functions in terms of saving and restoring registers according to the cdecl convention:

080a9e61 <toplevel_fnc>:
 80a9e61:   55                      push   %ebp
 80a9e62:   89 e5                   mov    %esp,%ebp
 80a9e64:   57                      push   %edi         
 80a9e65:   56                      push   %esi         
 80a9e66:   53                      push   %ebx         
 80a9e67:   83 ec 20                sub    $0x20,%esp
    .
    .
    .
 80a9eff:   83 c4 20                add    $0x20,%esp
 80a9f02:   5b                      pop    %ebx
 80a9f03:   5e                      pop    %esi
 80a9f04:   5f                      pop    %edi
 80a9f05:   5d                      pop    %ebp
 80a9f06:   c3                      ret  

and

080a9dfc <subroutine_fnc>:
 80a9dfc:   55                      push   %ebp
 80a9dfd:   89 e5                   mov    %esp,%ebp
 80a9dff:   57                      push   %edi
 80a9e00:   56                      push   %esi
 80a9e01:   53                      push   %ebx
 80a9e02:   83 ec 14                sub    $0x14,%esp
    .
    .
    .
 80a9e59:   83 c4 14                add    $0x14,%esp
 80a9e5c:   5b                      pop    %ebx
 80a9e5d:   5e                      pop    %esi
 80a9e5e:   5f                      pop    %edi
 80a9e5f:   5d                      pop    %ebp
 80a9e60:   c3                      ret

In both functions the conventional x86 function prologue is followed by saving the values in registers %edi, %esi and %ebx on the stack. These registers are referred to as callee-save registers (vs. caller-save registers %eax, %ecx and %edx). The previous values of these registers are then restored before ret is executed.

Note: the stack frame for function <toplevel_fnc> is clearly aligned to a 16-byte boundary, indicating that GCC is probably the compiler used to generate the code.

3. Passing arguments to functions in cdecl

To make a subrouting call, the caller should:

  1. Before calling a subroutine, the caller should save the contents of certain registers that are designated caller-saved. The caller-saved registers are EAX, ECX, EDX. Since the called subroutine is allowed to modify these registers, if the caller relies on their values after the subroutine returns, the caller must push the values in these registers onto the stack (so they can be restore after the subroutine returns.

  2. To pass arguments to the subroutine, push them onto the stack before the call. The arguments should be pushed in inverted order (i.e. last argument first). Since the stack grows down, the first argument will be stored at the lowest address (this inversion of arguments was historically used to allow functions to be passed a variable number of arguments).

  3. To call the subroutine, use the call instruction. This instruction places the return address on top of the arguments on the stack, and branches to the subroutine code. This invokes the subroutine, which should follow the callee rules below.2

The arguments to be passed to <subroutine_fnc> will be saved on the stack prior to the function being called:

080a9e61 <toplevel_fnc>:
 80a9e61:   55                      push   %ebp
 80a9e62:   89 e5                   mov    %esp,%ebp
 80a9e64:   57                      push   %edi
 80a9e65:   56                      push   %esi
 80a9e66:   53                      push   %ebx
 80a9e67:   83 ec 20                sub    $0x20,%esp         
 80a9e6a:   c6 45 f3 41             movb   $0x41,-0xd(%ebp)
 80a9e6e:   c7 44 24 04 0a 00 00    movl   $0xa,0x4(%esp)            <- arg 2
 80a9e75:   00 
 80a9e76:   c7 04 24 00 00 00 00    movl   $0x0,(%esp)               <- arg 1
 80a9e7d:   e8 7a ff ff ff          call   80a9dfc <subroutine_fnc>

How can I, in general, learn what arguments does subroutine_fnc use?

In simple cases (no optimization), if the calling convention is known, there is a good chance the function arguments can be discovered.

The not so basics: optimization

Let us examine disassembly of some object code produced from simple example source (see below) when gcc is executed with the -O3 argument (maximum optimization):

080482f0 <main>:
 80482f0:       b8 01 00 00 00          mov    $0x1,%eax
 80482f5:       c3                      ret

080483f0 <function>:
 80483f0:       8b 44 24 08             mov    0x8(%esp),%eax
 80483f4:       03 44 24 04             add    0x4(%esp),%eax
 80483f8:       03 44 24 0c             add    0xc(%esp),%eax
 80483fc:       c3                      ret    

What are the arguments to <main>? What are the arguments to <function>? What is the relationship between these two functions, if any?

We can see that quite a bit of information is simply not present in optimized code.

The difference between the optimized object code and unoptimized object code is huge. Unoptimized assembly of the source can be found here: https://godbolt.org/g/HS57Wp

Here is the source code (try to guess what is going on, then move the mouse cursor over the block below):

int function(int a, int b, int c); //prototype int main(void) { int a = 1; int b = 2; int c = 3; int k = function(a, b, c); return k / 6; }
int function(int a, int b, int c) { return a + b + c; }

As we can see from the very simple example above, optimization throws the calling convention out the window, leaving one hard-pressed to figure out what is really happening in the code. In the optimized code, there was no call intruction in <main>, which makes argument identification rather difficult.

More discussion of this can be found here: How many arguments are passed in a function call?

The even less basics: variable type recovery

How to figure out method argument sizes and types in elf32-i386 disasembly?

Deducing function argument types from disassembly of object code is referred to as type recovery, and is closely related to variable recovery. Both are difficult problems and the subject of research.

The notion of variables and types does not exist in object code. A variable name is a label that is given a memory address which corresponds to the data located at that address. While type information is necessary for the compiler to evaluate syntatical and semantic correctness of source code, object code that is executed directly by the CPU does not preserve this information (at least not directly).

The type recovery task, which gives a high-level type to each variable, is more challenging. Type recovery is challenging because high-level types are typically thrown away by the compiler early on in the compilation process. Within the compiled code itself we have byte-addressable memory and registers. For example, if a variable is put into eax, it is easy to conclude that it is of a type compatible with 32-bit register, but difficult to infer high-level types such as signed integers, pointers, unions, and structures.

Current solutions to type recovery take either a dynamic approach, which results in poor program coverage, or use unprincipled heuristics, which often given incorrect results. Current static-based tools typically employ some knowledge about well-known function prototypes to infer parameters, and then use proprietary heuristics that seem to guess the type of remaining variables such as locals. 3

Different decompilers take different approaches to this class of problems. Here is the approach taken by TIE (Type Inference on Executables):

TIE approach to type inference

Hex-rays discusses their approach in their whitepaper, Decompilers and beyond.


1. Calling convention (Wikipedia)

2. x86 Assembly Guide (It should be noted that the term "parameter" is used incorrectly - it should be "argument")

3. TIE: Principled Reverse Engineering of Types in Binary Programs

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.