C Programming Under The Hood    

30.10.2010 - Dr. Erhard Henkes          


The IDE tool 

We start with downloading and installing the open-source integrated development environment (IDE) code::blocks (current version 10.05, May 27, 2010). As the compiler we use GNU gcc compiler (default).

In front of the curtain

The minimum we have to enter as source code to compile and link the program is:

 
  main(){}



But this is a coding style that has to be assisted by the compiler, dependent on the compiler, sometimes without, sometimes with warnings.
Therefore, we use the correct style that uses the so-called return value of a function in C:

 
  int main(){return 0;}



With our IDE code::blocks we perform the following steps (Windows XP):

1) Start code::blocks



2) File - New - Empty File



3) Enter    int main(){return 0;}   and save the file as  step1_001.c (e.g. C:\temp\step1_001.c)



4) "Build and run" (F9) and get the following window:



As you see, we receive the return value 0. Without this "return 0;" you receive the return value 1, meaning error.

After pressing a key the window vanishes. What have we done?

Behind the curtain

If you saved your source code file at C:\temp\step1_001.c, then you find the following files in this subdirectory C:\temp:

step1_001.c          23 byte
step1_001.o         372 byte
step1_001.exe    24.402 byte

In the next chapters we will investigate these files.

Source Code File

In our c-file we entered 21 characters. Why does it need 23 byte? The answer is given, when you open the file with notepad++ showing all characters:

 

There are two invisible ASCII characters: Carriage Return (0x0D) and Line Feed (0x0A)
Now we understand every byte of our source code file.

Object File

To open the the object file we need a specialized tool within our IDE code::blocks.
You find it here: ...\CodeBlocks\MinGW\bin\objdump.exe

objdump.exe is part of the GNU binutils. It displays various information about object files.
We will use it as a disassembler to view the produced executable code in assembly form.
The object files are the input for the linker that builds executable exe (Windows) or elf (Linux) files from it.

Now, let's have a look into the object file:

In the cmd (command) console we enter:

...\CodeBlocks\MinGW\bin\objdump -DsM intel c:\temp\step1_001.o

c:\temp\step1_001.o:     file format pe-i386

Contents of section .text:
0000 5589e583 e4f0e800 000000b8 00000000  U...............
0010 c9c39090                             ....


Disassembly of section .text:

00000000 <_main>:
   0:   55                      push   ebp
   1:   89 e5                   mov    ebp,esp
   3:   83 e4 f0                and    esp,0xfffffff0
   6:   e8 00 00 00 00          call   b <_main+0xb>
   b:   b8 00 00 00 00          mov    eax,0x0
  10:   c9                      leave
  11:   c3                      ret
  12:   90                      nop
  13:   90                      nop




First, we explain the used parameters of objdump.exe:

-D
-s
-M intel     
Display assembler contents of all sections
Display the full contents of all sections requested
Display the assembler code in Intel syntax (instead of AT&T syntax)

Our object file has a size of 372 byte. We look only at the part of our little C function main.
Let's open the object file with an hex editor that provides every byte:



I high-lighted the region of our code for the C start function "main".

Executable File

If you open the exe- or elf-file with a hex editor you get a lot more of information.
In an exe file you find ASCII-readable parts like "This program cannot be run in DOS mode".
The program code can be found by searching the characteristic hex-code:



The ELF-format or PE-format is specified, and our program is only a minor part of the information provided in these executable files.


Compilation and Linking Process

Your compiler is the file gcc, and your linker is the file ld.

The compiler transforms the source code written in C into the object code.  

step1_001.c -->
step1_001.o

The linker transforms the object code files into an executable program. 

step1_001.o --> step1_001.exe

Compiler Settings

With "Settings - Compiler and debugger - Compiler Flags" you find this check box list for selecting appropiate compiler settings.



Thus, let us try to test speed optimizations from -O, over -O1, -O2 to -O3 and watch out for changes:

No speed optimization, speed optimization -O, -O1 :

00000000 <_main>:
   0:   55                      push   ebp
   1:   89 e5                   mov    ebp,esp
   3:   83 e4 f0                and    esp,0xfffffff0
   6:   e8 00 00 00 00          call   b <_main+0xb>
   b:   b8 00 00 00 00          mov    eax,0x0
  10:   c9                      leave
  11:   c3                      ret
Speed optimization -O2, -O3, size optimization -Os:

00000000 <_main>:
   0:   55                      push   ebp
   1:   89 e5                   mov    ebp,esp
   3:   83 e4 f0                and    esp,0xfffffff0
   6:   e8 00 00 00 00          call   b <_main+0xb>
   b:   31 c0                   xor    eax,eax
   d:   c9                      leave
   e:   c3                      ret  



C Function - Prologue, Body, Epilogue

The corresponding assembler instructions can be found here.

push and pop are the instructions to transfer data to or to get data from the stack.

leave is the combined instruction for:
mov esp, ebp
pop ebp

If a C Functionis called, there are two sides: the caller and the callee.
After calling the callee, there are the prologue, the body and the epilogue:

; prologue of the callee
push ebp      ; save ebp of the caller
mov ebp, esp  ; establish ebp of the callee

...           ; body of the callee        

; epilogue of the callee
mov esp, ebp  ;
pop ebp       ; re-establish ebp of the caller
ret           ; return to the saved eip of the caller 


Let's analyze that with a practical example:

We define a little function doSomething() without a return value (this type is called "void"):



Without optimization the object code looks like this:

...\CodeBlocks\MinGW\bin\objdump -DsM intel c:\temp\step1_002.o

c:\temp\step1_002.o:     file format pe-i386

Contents of section .text:
 0000 5589e590 c9c35589 e583e4f0 e8000000  U.....U.........
 0010 00e8eaff ffffb800 000000c9 c3909090  ................

Disassembly of section .text:

00000000 <_doSomething>:
   0:   55                      push   ebp
   1:   89 e5                   mov    ebp,esp
   3:   90                      nop
   4:   c9                      leave
   5:   c3                      ret

00000006 <_main>:
   6:   55                      push   ebp
   7:   89 e5                   mov    ebp,esp
   9:   83 e4 f0                and    esp,0xfffffff0
   c:   e8 00 00 00 00          call   11 <_main+0xb>
  11:   e8 ea ff ff ff          call   0 <_doSomething>
  16:   b8 00 00 00 00          mov    eax,0x0
  1b:   c9                      leave
  1c:   c3                      ret 

At this example you see the prologue, body, and epilogue of the callee:

   push   ebp
   mov    ebp,esp
   nop
   leave
   ret

C Function - Stack

The callee's stack looks like this with reference to its ebp (stack frame pointer):

local variable 3      [ebp - 12]
local variable 2      [ebp -  8]
local variable 1      [ebp -  4]
ebp of the caller     [ebp +  0]
return address        [ebp +  4]
argument 1            [ebp +  8]
argument 2            [ebp + 12]

How can we check this? We need a debugger.
Code::blocks provides this feature to us, but we need to use a project.
Thus, let's start a new project and embed our source code file there.
We will change our function. It will have a return value and one argument:

We start with File - New - Project - Console Application - C.
Then we have to fill in some forms:





Finish.

In our project directory you will find now the file main.c.
Delete the source code of main.c, and change it to this code:

int doSomething(int value)
{
    asm("nop");
    return value;
}

int main()
{
    doSomething(42);
    return 0;
}

Check the correctness of the code with "Build and run" (F9).

Then we are going to debug the code stepwise. What do we need?
The debugger instructions and debugging windows can be found at the menu item "Debug".



Please try a little bit around with all the instructions and windows to get a complete feeling for this tool.
We will use the debugger as an eye-opener for our programs. Now our IDE tool helps us to look under the hood:



The breakpoints in the source code are set / unset (toggled) with F5 or by mouse clicks behind the line number.
Progressing the assembler instructions works fine with Alt+F7.

The debugging window "Disassembly " gives us the similar information as the program objdump.
Above you look at the object code of the function "main" in AT&T syntax.
Here you find the same code in Intel syntax produced by objdump:

00000000 <_doSomething>:
   0:   55                      push   ebp
   1:   89 e5                   mov    ebp,esp
   3:   90                      nop
   4:   8b 45 08                mov    eax,DWORD PTR [ebp+0x8]
   7:   c9                      leave
   8:   c3                      ret

00000009 <_main>:
   9:   55                      push   ebp
   a:   89 e5                   mov    ebp,esp
   c:   83 e4 f0                and    esp,0xfffffff0
   f:   83 ec 10                sub    esp,0x10
  12:   e8 00 00 00 00          call   17 <_main+0xe>
  17:   c7 04 24 2a 00 00 00    mov    DWORD PTR [esp],0x2a
  1e:   e8 dd ff ff ff          call   0 <_doSomething>
  23:   b8 00 00 00 00          mov    eax,0x0
  28:   c9                      leave
  29:   c3                      ret

The first argument of the function is located at ebp + 8.

a
rgument 1  [ebp +  8]  
leads to the object code       mov eax,DWORD PTR [ebp+0x8]

In this step the (first) argument of the function is moved to the register EAX. This is the return value of the function to the caller.

The argument (42 or 0x2A) is passed in "main" to the function "doSomething" in this line:
mov DWORD PTR [esp],0x2a

If you do not like AT&T syntax, you can switch the AT&T syntax to Intel syntax in the debugging window "Disassembly" bei Settings - Global compiler settings - Debugger settings - Choose disassembly flavor (GDB only).
Just switch the drop down list to Intel. I recommend to get used to both assembler syntax methods. Here you find an overview on AT&T syntax.

The debugging window "CPU registers " shows the values of the CPU registers:



C Function - Use of Registers

As you have seen, EAX is used for providing the return value of the function to the caller. The arguments of the function are located at the stack.
For the temporary storage of data in registers there exists an important convention about the usage of registers for caller and callee:

Callee uses EAX, ECX, EDX: Scratch registers can be used for temporary storage without restrictions, also called caller-save or volatile registers.
Caller uses EBX, ESI, EDI: Callee-save or non-volatile registers have to be saved before using them and restored after using them.
You can rely on these registers having the same value after a call as before the call:  EBX, ESI, EDI, EBP

Now we make things more complicated. Our next function uses three arguments and a local variable (stack). This is our new source code:

int doSomething(int val1, int val2, int val3)
{
    int value = 0;
    asm("nop");

    value = val1+val2+val3;
    asm("nop");
    return value;
}

int main()
{
    doSomething(42,-12,-14);
    return 0;
}

The debugging window "Disassembly" shows the following object code (Intel syntax):

0040133B    push   ebp
0040133C    mov    ebp,esp
0040133E    and    esp,0xfffffff0
00401341    sub    esp,0x10
00401344    call   0x401760 <__main>
00401349    mov    DWORD PTR [esp+0x8],0xfffffff2   
00401351    mov    DWORD PTR [esp+0x4],0xfffffff4
00401359    mov    DWORD PTR [esp],0x2a
00401360    call   0x401318 <doSomething>
00401365    mov    eax,0x0
0040136A    leave
0040136B    ret

As type signed integer the value 0xfffffff2 is -14, 0xfffffff4 is -12, and 0x2a is 42.
These arguments are placed at the stack with the last argument at the top of it.

1st argument
0x0000002a
2nd argument
0xfffffff4
3rd argument
0xfffffff2

After calling "doSomething" we procede to the situation after the instruction "sub esp,0x10":

The position of the three arguments at the stack can be visualized by using the debugging window "Examine memory".
There you enter "$ebp" as the beginning memory address. By reading the values, remember that they are put in the memory according to little endian.
This means that the lowest significant byte comes first and the most significant byte cames last. 

At ebp + 4 you find the return address to main(), and at ebp + 0 you find the ebp value of the caller (in our case that is "main").

...                   [ebp - 16]                  <--- esp of the callee (0x0022FF28)   
local variable 3      [ebp - 12]
local variable 2      [ebp -  8]
local variable 1      [ebp -  4]
ebp of the caller
     [ebp +  0] 0x0022FF58       <--- ebp of the callee (0x0022FF38)

return address        [ebp +  4] 0x00401365
argument 1            [ebp +  8] 0x0000002A (42)
argument 2            [ebp + 12] 0xFFFFFFF4 (-12)
argument 3            [ebp + 16] 0xFFFFFFF2 (-14)



Now let us look into the object code of the body of our function "doSomething":

0040131B    sub    esp,0x10                                                 
0040131E    mov    DWORD PTR [ebp-0x4],0x0   <---
local variable 1 [ebp - 4]    int value = 0;
00401325    nop
00401326    mov    eax,DWORD PTR [ebp+0xc]   <--- move argument 2 to EAX
00401329    mov    edx,DWORD PTR [ebp+0x8]   <--- move argument 1 to EDX
0040132C    lea    eax,[edx+eax*1]           <--- addition of argument 1 and argument 2 (LEA = Load Effective Address)
0040132F    add    eax,DWORD PTR [ebp+0x10]  <--- add  argument 3 to EAX
00401332    mov    DWORD PTR [ebp-0x4],eax   <--- move EAX to local variable 1
00401335    nop
00401336    mov    eax,DWORD PTR [ebp-0x4]   <--- move local variable 1 to EAX (return value of the function)

Proceed step by step and watch the register EAX and the instruction pointer EIP during the instructions.