7

Consider the following C code:

#include <stdint.h>

void func(void) {
   uint32_t var = 0;
   return;
}

The unoptimized (i.e.: -O0 option) assembly code generated by GCC 4.7.2 for the code above is:

func:
    pushl %ebp
    movl %esp, %ebp
    subl $16, %esp
    movl $0, -4(%ebp)
    nop
    leave
    ret

According to the stack alignment requirements of the System V ABI, the stack must be aligned by 16 bytes before every call instruction (the stack boundary is 16 bytes by default when not changed with the option -mpreferred-stack-boundary). Therefore, the result of ESP modulo 16 has to be zero prior to a function call.

Bearing in mind these stack alignment requirements, I assume the following stack's status representation just before executing the leave instruction to be right:

Size (bytes)       Stack          ESP mod 16      Description
-----------------------------------------------------------------------------------

             |     . . .      |             
             ------------------........0          at func call
         4   | return address |
             ------------------.......12          at func entry
         4   |   saved EBP    |
     ---->   ------------------........8          EBP is pointing at this address
     |   4   |      var       |
     |       ------------------........4
 16  |       |                |
     |  12   |                |
     |       |                |
     ---->   ------------------........8          after allocating 16 bytes

With this representation of the stack in mind, there are two points that puzzle me:

  1. var is obviously not aligned on the stack to 16 bytes. This issue seems to contradict what I have read in this answer to this question (the emphasis is of my own):

    -mpreferred-stack-boundary=n where the compiler tries to keep items on the stack aligned to 2^n.

    In my case -mpreferred-stack-boundary wasn't provided, so it is set by default to 4 (i.e.: 2^4=16 bytes boundary) according to this section of GCC's documentation (I got indeed the same results with -mpreferred-stack-boundary=4).

  2. The purpose of allocating 16 bytes on the stack (i.e.: the subl $16, %esp instruction) instead of allocating just 8 bytes: after allocating 16 bytes neither the stack is aligned by 16 bytes nor any memory space is spared. By allocating just 8 bytes instead, the stack gets aligned by 16-bytes and no additional 8 bytes are wasted.

JFMR
  • 23,265
  • 4
  • 52
  • 76
  • This has very little to do with C, and very much to do with "the System V ABI" on "x86" post compilation to machine code. – autistic Nov 21 '17 at 11:04
  • @Sebivor are you suggesting me to edit the tags and choose [tag:abi] over [tag:c]? I am limited to 5 tags. – JFMR Nov 21 '17 at 11:06
  • Well, as source code you've provided something so basic it could be ported to virtually any language and generate the same machine code, so... what I'm recommending is that you remove the C tag, or find some citation in n1570 which speaks of things like "stack alignment" and "System V ABI"... – autistic Nov 21 '17 at 11:10
  • 2
    See [this](https://stackoverflow.com/questions/27371820/understanding-mpreferred-stack-boundary-number): `-m-preferred-stack-boundary` doesn't align single variables. See [this](https://godbolt.org/g/M4cGfb) for you second point. – Margaret Bloom Nov 21 '17 at 11:34
  • 1
    Also keep in mind the C compiler is not obliged to produce optimal code in any kind of metric, including stack space usage. While it will try hard (and from playing around with gcc 4.7.2 on godbolt it looks good, the junk space is result only of the alignment), there's no language-breaking problem if it would fail and allocate 16B more junk than truly needed (especially in unoptimized code). What it obeys (due to *platform specific* option) is having the `esp` properly aligned upon next `call` instruction. From C language point of view even stack existence is not mandatory, nor some alignment. – Ped7g Nov 21 '17 at 11:52

2 Answers2

5

Looking at -O0-generated machine code is usually a futile exercise. The compiler will emit whatever works, in the simplest possible way. This often leads to bizarre artifacts.

Stack alignment only refers to alignment of the stack frame. It is not directly related to the alignment of objects on the stack. GCC will allocate on-stack objects with the required alignment. This is simpler if GCC knows that the stack frame already provides sufficient alignment, but if not, GCC will use a frame pointer and perform explicit alignment.

Florian Weimer
  • 32,022
  • 3
  • 48
  • 92
  • In your last sentence, do you mean the typical `andl $-16, %esp` to assure that the stack is properly aligned to 16 bytes? (preserving the original `esp` by means of `ebp`). – JFMR Nov 21 '17 at 14:28
  • 1
    Yes, this is one way to do it. But GCC will not do this by default because it assumes the stack is already aligned, you will need to pass an option like `-mrealignstack`, and GCC will only do it if needed. – Florian Weimer Nov 21 '17 at 14:31
  • 1
    Thanks, I got it. The attribute `force_align_arg_pointer` will also do for individual functions. – JFMR Nov 21 '17 at 14:33
0

This answer aims to further develop some of the comments written above.


First, based on Margaret Bloom's comment, consider the following modification of the func() function that was originally posted:

#include <stdint.h>

void bar(void);    

void func(void) {
   uint32_t var = 0;
   bar(); // <--- function call
   return;
}

Unlike the original func() function, the redefined one contains a function call to bar().

The generated assembled code is this time:

func:
    pushl %ebp
    movl %esp, %ebp
    subl $24, %esp
    movl $0, -12(%ebp)
    call bar
    nop
    leave
    ret

Note that, the instruction subl $24, %esp does align the stack by 16 bytes (the subl $16, %esp instruction in the original func() function didn't).

Since the redefined func() contains a function call now (i.e.: call bar), the stack has to be aligned by 16 bytes just before executing the call instruction. The previous func() called no function at all, therefore there was no need for the stack to be aligned by 16 bytes.


It is clear, that, at least, 4 bytes must be allocated on the stack for the var variable. Allocating 4 additional bytes would be needed in order to align the stack by 16 bytes.

Someone may ask why 24 bytes are being allocated in order to align the stack, when allocating just 8 bytes would do. Well, by paraphrasing part of Ped7g's comment, this question is also answered:

Also keep in mind the C compiler is not obliged to produce optimal code in any kind of metric, including stack space usage. While it will try hard (and from playing around with gcc 4.7.2 on godbolt it looks good, the junk space is result only of the alignment), there's no language-breaking problem if it would fail and allocate 16B more junk than truly needed (especially in unoptimized code).

JFMR
  • 23,265
  • 4
  • 52
  • 76
  • Use `volatile int var = 1;` to get the compiler to still do a store with `-O3`. Looking at `-O0` code is silly; it's not even trying to be optimal. Or without `volatile`, forcing the compiler to save something across a function call is another way to make it use stack space. (With register args (like in 64-bit code, or with a regparm calling convention), use a function arg after a call to a function it can't see, like you're doing here with `bar()`). Actually nvm, that will push/pop `ebx` or `rbx` and keep the value there. I was thinking `void foo(int a) { bar(); return a+1; }`, but NVM. – Peter Cordes Nov 21 '17 at 14:54
  • It is aligned to 32. Don't overlook the return address, saved ebp, stack canary. Modern C compilers favor aligning to 16 or 32 so they can generate optimal SIMD code, using SSE2 or AVX. – Hans Passant Nov 21 '17 at 14:58
  • @HansPassant AFAIK, at the moment of performing the call the stack is aligned to 16. Then, both the return address and the `ebp` register are pushed on the stack. After that, 24 bytes are allocated on the stack. In total, `esp` is decreased by 32. However subtracting 32 from a 16-byte-aligned address does not necessarily result in an address that is aligned to 32 bytes. How do you know it is aligned to 32 bytes? – JFMR Nov 21 '17 at 15:40
  • @HansPassant Of course, if it is aligned to 32 it will be also aligned to 16, since the latter requirement is weaker. – JFMR Nov 21 '17 at 15:42