First Post: Fun with Compilers

For the first post on my new blog, I thought I’d be random and show what happens when you take the same small piece of code and compile it with both gcc and g++.  You might be surprised that, while identical for the most part, there are some small differences in the assembly code output when targeting for C and C++.

The test program was the same for both compilers, except I saved it as both main.c and main.cpp. Note that I could have kept the same file but I was lazy and didn’t feel like forcing the compiler to output pure C code (gcc will compile for C++ when it sees a .cpp extension).

   int main(int argc, char* argv[])
   {
     int i = 0;
     int loop;
  
     for (loop = 0; loop < 50; loop++)
       ++i;


     return 0;
   }

A quick gcc main.c -o main_c and g++ main.cpp -o main_cpp took care of the output.  After that, I used objdump -DCslx -M intel to fully disassemble the programs.

Ignoring the differences (for now) in the ELF output, we examine the main functions in assembler.

Output using gcc:

8048394

:
main():
 8048394: 55                   push   ebp
 8048395: 89 e5                 mov    ebp,esp
 8048397: 83 ec 10             sub    esp,0x10
 804839a: c7 45 fc 00 00 00 00 mov    DWORD PTR [ebp-0x4],0x0
 80483a1: c7 45 f8 00 00 00 00 mov    DWORD PTR [ebp-0x8],0x0
 80483a8: eb 08                 jmp    80483b2
 80483aa: 83 45 fc 01           add    DWORD PTR [ebp-0x4],0x1
 80483ae: 83 45 f8 01           add    DWORD PTR [ebp-0x8],0x1
 80483b2: 83 7d f8 31           cmp    DWORD PTR [ebp-0x8],0x31
 80483b6: 7e f2                 jle    80483aa
 80483b8: b8 00 00 00 00       mov    eax,0x0
 80483bd: c9                   leave  
 80483be: c3                   ret    
 80483bf: 90                   nop

Output using g++:
080483f4

:

main():
 80483f4: 55                   push   ebp
 80483f5: 89 e5                 mov    ebp,esp
 80483f7: 83 ec 10             sub    esp,0x10
 80483fa: c7 45 fc 00 00 00 00 mov    DWORD PTR [ebp-0x4],0x0
 8048401: c7 45 f8 00 00 00 00 mov    DWORD PTR [ebp-0x8],0x0
 8048408: eb 08                 jmp    8048412
 804840a: 83 45 fc 01           add    DWORD PTR [ebp-0x4],0x1
 804840e: 83 45 f8 01           add    DWORD PTR [ebp-0x8],0x1
 8048412: 83 7d f8 31           cmp    DWORD PTR [ebp-0x8],0x31
 8048416: 0f 9e c0             setle  al
 8048419: 84 c0                 test   al,al
 804841b: 75 ed                 jne    804840a
 804841d: b8 00 00 00 00       mov    eax,0x0
 8048422: c9                   leave  
 8048423: c3                   ret    
 8048424: 90                   nop
 8048425: 90                   nop
 8048426: 90                   nop
 8048427: 90                   nop
 8048428: 90                   nop
 8048429: 90                   nop
 804842a: 90                   nop
 804842b: 90                   nop
 804842c: 90                   nop
 804842d: 90                   nop
 804842e: 90                   nop
 804842f: 90                   nop

The first part of main() from both C and C++ targeted code is the same.  We’ll go through it here to discuss what is happening for those who are more familiar with higher-level languages than Intel x86 assembly.  These lines are pretty much standard for any subroutine in assembly language across different compilers.  They set up a the new stack frame for the subroutine and save pointers from the old routine so it can be recovered upon exiting.

   push   ebp      Push the base pointer to the stack
   mov    ebp,esp  Base and top of stack pointer equal
   sub    esp,0x10 Allocate space for local variables in our new stack frame.

Now we get to the meat of the program.  If you’ll recall, I had two local variables in the program declared as:

     int i = 0;
     int loop;

In the assembler output, both the variables (referenced through their aliases on the stack) are initialized to zero.

  mov    DWORD PTR [ebp-0x4],0x0 – explicity set to zero
  mov    DWORD PTR [ebp-0x8],0x0 – gcc sets loop to zero implicity

We then jump to the comparison line where we test that loop (again referenced off the stack) is compared to the value 49 (0x31 in hex).  This is where we see the differences in the output from targeting pure C vs. C++.  I’ll copy them below from both the C and C++ target since I’ve scrolled quite a bit from up top 😉

Loop in C target:

  jmp    80483b2
  add    DWORD PTR [ebp-0x4],0x1
  add    DWORD PTR [ebp-0x8],0x1
  cmp    DWORD PTR [ebp-0x8],0x31
  jle    80483aa

Loop in C++ target:

  jmp    8048412
  add    DWORD PTR [ebp-0x4],0x1
  add    DWORD PTR [ebp-0x8],0x1
  cmp    DWORD PTR [ebp-0x8],0x31
  setle  al
  test   al,al
  jne    804840a

Now for the most part they’re identical except for how we do our test to break out of the loop.  In the C version, gcc uses the jle (jump if less than or equal) instruction after the comparison (cmp) to go up and add one to i and loop.  In the C++ version, we first use setle (Set if less than or equal) to set the al register to 1 if the comparison matched.  We then test if al is true and then we  use jne to move back up in the function to do the additions.  Once loop is equal to 49, we then put 0 into the EAX register and then exit the routine.

Until I go into WHY there’s this slight difference in output, for now let’s just say that gcc and g++ use some similar and some different code paths when generating machine code.  Have fun and Happy Father’s Day everyone.