I do not understand the following:

in a very simple dummy C function:

void vulnerable_function(char* string) { 
    char buffer[100];
}

When I use gdb to disassemble it, I obtain:

0x08048464 <+0>:  push   %ebp
0x08048465 <+1>:  mov    %esp,%ebp
0x08048467 <+3>:  sub    $0x88,%esp

I really do not understand why the stack pointer is decremented by 0x88.

I believed that it would be 0x64 instead of 0x88. Can you explain it please?

Thank you very much!

  • can you add more info about your executable like file ./executable – Vido Mar 11 '17 at 22:18

There are several factors involved in how much space is allocated by the compiler for a function's stack frame on the process runtime stack:

  • space required for saving copies of arguments to the function in the stack frame
  • space required for storing local variables in the stack frame
  • Stack alignment to a 16-byte boundary (GCC default for i386 architecture)

Background

The i386 ABI

Stack Frame Specification

The specification for the stack frame on x86 machines is given in Chapter 3: "Low-Level System Information" in the System V Application Binary Interface Intel386 Architecture Processor Supplement, Fourth Edition in the section titled "Function Calling Sequence".

Note:

Within this specification, the term halfword refers to a 16-bit object, the term word refers to a 32-bit object, and the term doubleword refers to a 64-bit object.

Here are relevant excerpts:

  • The stack is word aligned. Although the architecture does not require any alignment of the stack, software convention and the operating system requires that the stack be aligned on a word boundary.

  • An argument’s size is increased, if necessary, to make it a multiple of words. This may require tail padding, depending on the size of the argument.

  • Other areas depend on the compiler and the code being compiled. The standard calling sequence does not define a maximum stack frame size, nor does it restrict how a language system uses the ‘‘unspecified’’ area of the standard stack frame.

The "unspecified" area in a stack frame is the space created for local variables and where arguments to the function are copied to. This space is managed by the compiler.

Here is a diagram from the ABI: Standard Stack Frame, i386

Alignment

It is the compiler that manages stack frames, and in order for a stack frame to be aligned the alignment of variables within the stack frame must also be known.

Alignment of variables depends on their type and the architecture of the CPU. This is also specified in the ABI: Fundamental types, i386

There are conventions that pertain specifically to the alignment of arrays, structures and unions:

Aggregates (structures and arrays) and unions assume the alignment of their most strictly aligned component. The size of any object, including aggregates and unions, is always a multiple of the object’s alignment. An array uses the same alignment as its elements. Structure and union objects can require padding to meet size and alignment constraints. The contents of any padding is undefined.

However, on i386 architecture systems, GCC aligns the stack to a 16-byte boundary by default:

-mpreferred-stack-boundary=num
Attempt to keep the stack boundary aligned to a 2 raised to num byte boundary. If -mpreferred-stack-boundary is not specified, the default is 4 (16 bytes or 128 bits).

This means that the compiler allocates 16 bytes of space on the stack frame for variables whose type sizes are less than 16 bytes. For example, even though an int is 4 bytes on an i386 system, the compiler would still allocate 16 bytes of space on the stack frame for it.

The stack frame for vulnerable_function()

Let us analyze how the compiler allocates space on a function's stack frame with 2 simple examples: a function with a char pointer local variable and a function with a 100-byte char array.

A function called pointer_test with char pointer local variable:

void pointer_test(void) {
    char *i = "test";
}

Assembly code generated by gcc + as:

Dump of assembler code for function pointer_test:
   0x080483db <+0>:     push   %ebp
   0x080483dc <+1>:     mov    %esp,%ebp
   0x080483de <+3>:     sub    $0x10,%esp  <-- 16 bytes of space created for 4-byte pointer
   0x080483e1 <+6>:     movl   $0x8048480,-0x4(%ebp)
   0x080483e8 <+13>:    nop
   0x080483e9 <+14>:    leave  
   0x080483ea <+15>:    ret  

Here we see that 16 bytes of space were allocated for a 4-byte pointer.

A function called char_array_test with a char array local variable:

void char_array_test(void) {
    char buffer[100];
}

Assembly code generated by gcc + as:

Dump of assembler code for function char_array_test:
   0x0804844b <+0>:     push   %ebp
   0x0804844c <+1>:     mov    %esp,%ebp
   0x0804844e <+3>:     sub    $0x78,%esp  <-- 120 bytes of space created for 100-byte array
   0x08048451 <+6>:     mov    %gs:0x14,%eax
   0x08048457 <+12>:    mov    %eax,-0xc(%ebp)
   0x0804845a <+15>:    xor    %eax,%eax
   0x0804845c <+17>:    nop
   0x0804845d <+18>:    mov    -0xc(%ebp),%eax
   0x08048460 <+21>:    xor    %gs:0x14,%eax
   0x08048467 <+28>:    je     0x804846e <char_array_test+35>
   0x08048469 <+30>:    call   0x8048310 <__stack_chk_fail@plt>
   0x0804846e <+35>:    leave  
   0x0804846f <+36>:    ret

Here we see that 120 bytes of space were allocated for a 100-byte array.

In the case of void vulnerable_function(char *string), space in the stack frame must be allocated by gcc for a 4-byte pointer and a 100-byte array.

  • As we observed above in pointer_test(), since gcc aligns allocated space to 16-byte boundaries by default, 16 bytes of space are also allocated on the stack frame for the 4-byte pointerchar *string, the argument to the function.
  • We observed above in char_array_test() that gcc allocates 120 bytes of space for a 100 byte array (120 is not a multiple of 16, so this is not aligned with a 16-byte boundary. I do not know why the compiler does this). Likewise, the compiler allocates 120 bytes of space for char buffer[100] in vulnerable_function().

0x10 bytes for string + 0x78 bytes for buffer[100] = 0x88

Resources

Compiler Explorer is an interactive compiler that runs in your browser. Playing around with it is much faster than constantly recompiling code and disassembling it.

The System V Application Binary Interface Intel386 Architecture Processor Supplement, Fourth Edition

Intel 386 and AMD x86-64 Options for GCC

cdecl and x86 calling conventions discusses calling conventions in x86 compilers

Poke-a-hole and friends is an article that discusses how structures are padded in order to maintain alignment and how this changes across architectures.

related SO questions

Stack allocation, padding, and alignment

what is “stack alignment”?

As SYS_V correctly cites in his answer, the GCC documentation states that GCC will work to align the stack pointer to 16-byte boundaries by default.

-mpreferred-stack-boundary=num

Attempt to keep the stack boundary aligned to a 2 raised to num byte boundary. If -mpreferred-stack-boundary is not specified, the default is 4 (16 bytes or 128 bits).

We find some reasoning as to why this is done as well (NB: On the 64-bit architecture, 16-byte alignment is mandatory):

[A different value] leads to wrong code when functions compiled with 16 byte stack alignment (such as functions from a standard library) are called with misaligned stack. In this case, SSE instructions may lead to misaligned memory access traps [and] variable arguments are handled incorrectly for 16 byte aligned objects [...] You must build all modules [with the same value]. This includes the system libraries and startup modules.

Note, however, that this is mostly about the stack frame (boundary), not necessarily the individual objects on the stack. This frame alignment happens not inside the function, but at the call site where you will see something like this (note the extra subtraction from %esp):

 sub    $0xc,%esp                # pad stack by 12 bytes
 push   %eax                     # push 4-byte argument
 call   vulnerable_function

Nevertheless it makes sense to keep (some) objects aligned as well.

In your example, you encounter 0x88 (=136) bytes allocated for the 100-byte buffer while SYS_V got 0x78 (=120) for the same. Note that both these values are congruent 8 modulo 16. This is chosen because at this point, your stack frame already includes two 4-byte values: the return address and the saved frame pointer. With these combined, you end up 16-byte-aligned after the allocation.

Your Answer

 

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

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