1

I have following C++ code:

int main(){

  int a = 1;
  double d = 1.2;

  return 0;
}

and get the following assembly using GCC 6.2 -m32:

main:
        lea     ecx, [esp+4]
        and     esp, -8
        push    DWORD PTR [ecx-4]
        push    ebp
        mov     ebp, esp
        push    ecx
        sub     esp, 20
        mov     DWORD PTR [ebp-12], 1
        fld     QWORD PTR .LC0
        fstp    QWORD PTR [ebp-24]
        mov     eax, 0
        add     esp, 20
        pop     ecx
        pop     ebp
        lea     esp, [ecx-4]
        ret
.LC0:
        .long   858993459
        .long   1072902963

and using MS CL 19:

_d$ = -12                                         ; size = 8
_a$ = -4                                                ; size = 4
_main   PROC
        push     ebp
        mov      ebp, esp
        sub      esp, 12              ; 0000000cH
        mov      DWORD PTR _a$[ebp], 1
        movsd    xmm0, QWORD PTR __real@3ff3333333333333
        movsd    QWORD PTR _d$[ebp], xmm0
        xor      eax, eax
        mov      esp, ebp
        pop      ebp
        ret      0
_main   ENDP

I have several questions.

  1. what's mean first three lines in GCC version?

    lea ecx, [esp+4]

    and esp, -8

    push DWORD PTR [ecx-4]

  2. MS CL version allocates 12 bytes, 4 for int and 8 for double:

    sub esp, 12 // that's great.

    But why GCC allocates 24?

    push ecx

    sub esp, 20

1 Answer 1

4

Given that you didn't specify any optimization flag and used -m32, GCC performed no optimization on your code. The -m32 flag specifies the generation of a 32 bit code for a compiler configured to generate 64 bit code by default. In 32 bit mode, even with optimizations activated, GCC will generate a sub-optimal code given that the only way to do floating point computations in 32 bit mode on Intel machines is through x87 instructions. If you remove the -m32 flag and add -O3 (third level of optimization in GCC) you'll obtain the following assembly code (quite similar to the one generated by Microsoft's CL) :

.LC1:
        .string "%d %lf\n"
        .section   .text.startup,"ax",@progbits
        .p2align 4,,15
        .globl  main
        .type   main, @function
main:
.LFB0:
        .cfi_startproc
        subq    $8, %rsp
        .cfi_def_cfa_offset 16
        movl    $1, %esi
        movl    $.LC1, %edi
        movsd   .LC0(%rip), %xmm0
        movl    $1, %eax
        call    printf
        xorl    %eax, %eax
        addq    $8, %rsp
        .cfi_def_cfa_offset 8
        ret
        .cfi_endproc
.LFE0:
        .size   main, .-main
        .section   .rodata.cst8,"aM",@progbits,8
        .align 8
.LC0:
        .long   858993459
        .long   1072902963 

Note : I added a printf to the code because if the GCC optimization pass sees no use of the two variables, they will be removed (dead code elimination). I invite you to check out my post on the subject of optimized vs. non optimized code (Assembly Code - GCC optimized vs not).

You can also notice that CL used an XMM register to store the 64 bit double element stored in .LC0. XMM registers are part of the SSE (Streaming SIMD Extensions) instruction set used mainly for floating point scalar & vector operations. Its implementation is much cleaner and faster than the x87 instruction set.

Q1 :

lea     ecx, [esp+4]      //load the content of [esp + 4] into ecx
and     esp, -8           //align the stack pointer to 8 bytes (same as esp & ~7)
push    DWORD PTR [ecx-4] //push the content of [ecx - 4] on the stack 

[ecx - 4] = [[esp + 4] - 4]

Let's suppose the stack is in this state :

     |       main      |
     |      return     |
     |      address @  |
     +-----------------+  <--- esp + 4 ---> ecx
     |    some value   |
     +-----------------+  <--- esp = ebp

The first instruction puts the existing stack content (main return address @) in ecx. It is equivalent to this :

mov ecx, esp
sub ecx, 4
mov ecx, [ecx]

You can see that the lea instruction does in one take what these instructions do in three takes.

The second instruction aligns esp on an 8 byte boundary. What that means is that the lower 3 order bits of the address pointed by esp will be 0. Memory accesses are faster on Intel machines when aligned on a power of 2 boundary.

The third instruction changes the state of the stack to the following :

     |         @       |
     +-----------------+  <--- esp + 4 ---> ecx
     |    some value   |
     +-----------------+  <--- ebp
     |       @ - 4     |  
     +-----------------+  <--- esp

Therefore, when the main function is done, it will return to @ - 4.

Q2 :

Let's reason mathematically :

         We have : EBP = ESP0 
         push ecx implies ESP1 = ESP0 - 4 
         then : ESP2 = ESP1 - 20 
         therefore : ESP0 = ESP2 - 24
         mov DWORD PTR [ebp-12], 1 implies x = EBP - 12 = ESP0 - 12
         We know that ESP0 = ESP2 - 24
         Therefore x = ESP2 - 24 - 12 = ESP2 - 36
         fstp    QWORD PTR [ebp-24] implies y = EBP - 24 = ESP0 - 24
         Therefore y = ESP2 - 24 - 24 = ESP2 - 48

Now, from this demonstration we extracted the location of the integer x = ESP2 - 36, and the location of the double y = ESP2 - 48. To compute the distance between both variables, we subtract y from x and obtain the following : x - y = ESP2 - 36 - ESP2 + 48 = 48 - 36 = 12. And that's the amount of bytes used by GCC for storing both of your 32 bit/4 byte and 64 bit/8 byte variables.

2
  • Thanks, why we have this three lines in GCC 32-bit mode and don't have in 64-bit mode. What is the purpose of this three lines? Dec 13, 2016 at 5:44
  • 1
    Check the Q1 update
    – yaspr
    Dec 13, 2016 at 6:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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