DEV Community

ddupard
ddupard

Posted on • Edited on

X86_64 From Source Code to Binary: What Does GCC Actually Do?

When we write a C program, we work with high-level concepts like functions, variables, and loops. However, your computer's processor only understands raw binary instructions. The compiler, gcc (GNU Compiler Collection), acts as a universal translator, transforming your human-readable code into machine-executable logic.

To illustrate this, we use a simple program:

#include <stdio.h>

int main() {
    printf("Hello world\n") ;
    printf("multiple parameters %s %i %i\n","test",1,2) ;
}
Enter fullscreen mode Exit fullscreen mode

1. The Invisible Compilation Journey

gcc is not just a single tool; it is an orchestrator that runs several sequential stages:

The Preprocessor: It handles directives like #include . It replaces this line with the entire content of the header file, preparing the code for compilation.

Compilation: The C code is transformed into assembly language, which is a textual representation of machine instructions.

Assembly: The assembler translates these instructions into binary format (object files .o).

Linking: This is the crucial final step. Your code uses printf, a function belonging to the standard C library (libc). The linker connects your code to the actual addresses of these functions within the system library.

2. Anatomy of a Binary: From Source to Execution

After the linking process, gcc generates an ELF File which contains several sections and several compiler-generated functions

A. Binary Sections: The Anatomy of the ELF File

  • .init & .fini: These sections contain code executed before and after your main function. They handle global setup and teardown.
  • .plt (Procedure Linkage Table): This section acts as a "trampoline" for external library functions (like printf). Since the exact memory address of libc is determined at runtime, the .plt enables dynamic linking.

  • .text: This is where your actual code—the main function and compiler-generated support functions—resides. It is marked as Read-Only and Executable to prevent tampering.

B. Compiler-Generated Functions

gcc adds several functions to your binary that you never explicitly wrote. These are essential for the program's lifecycle:

  • _start: This is the real entry point of your program. It is called by the OS kernel. It initializes the environment (arguments, environment variables) and then calls __libc_start_main.
  • __libc_start_main: A standard library function that prepares the environment for your main function and calls it
  • register_tm_clones / deregister_tm_clones: These functions handle Transactional Memory management. They are part of the GCC runtime overhead.
  • __do_global_dtors_aux: This function is responsible for calling destructors for global objects before the program exits.
  • frame_dummy: A helper function used to register frame information for exception handling.

C. Execution Flow: What Happens When You Run ./hello?

When you launch the program from your shell, the following sequence occurs:

a. OS Kernel Loading: The Linux kernel maps the binary into memory and hands control to the dynamic linker (ld-linux.so).
b. _start: The kernel jumps to _start. It cleans up the stack, sets up the initial registers, and invokes the C library's initialization logic.
c. Initialization (.init): The code in the .init section runs, setting up the runtime environment.
d. main Invocation: __libc_start_main calls your main function.
e. printf Execution: Inside main, your code calls printf. It jumps to the .plt entry, which resolves the library address and executes the actual print command.
f. Termination: After main returns, the program invokes __do_global_dtors_aux (to clean up globals) and the .fini section, then exits by calling a kernel system call.

Summary Table
Stage Function/Section Responsibility
Setup _start Prepare registers and invoke libc
Init .init Global initializations
Logic main Your custom code
Linkage .plt Resolving library functions (printf)
Cleanup .fini Final program shutdown

3. The Mechanics of Parameter Passing (System V AMD64 ABI Convention)

In the C programming language, passing arguments between functions does not rely on a "magic" stack, but on a strict convention called the System V AMD64 ABI. To optimize for speed, the gcc compiler prioritizes using CPU registers before resorting to memory (the stack).

When a function is called, the parameters are placed in the following specific registers, in this exact order:

rdi: 1st argument
rsi: 2nd argument
rdx: 3rd argument
rcx: 4th argument
r8: 5th argument
r9: 6th argument

If your function requires more than 6 arguments, the additional parameters are then pushed onto the stack, which is significantly slower.

See below for some examples of the System V AMD64 ABI mechanism

In the objdump analysis of the main function, you could observe this sequence:

0000000000401156 <main>:
  401156:   f3 0f 1e fa             endbr64
  40115a:   55                      push   %rbp
  40115b:   48 89 e5                mov    %rsp,%rbp
  40115e:   48 8d 05 9f 0e 00 00    lea    0xe9f(%rip),%rax        # 402004 <_IO_stdin_used+0x4>
  401165:   48 89 c7                mov    %rax,%rdi
  401168:   e8 e3 fe ff ff          call   401050 <puts@plt>
  40116d:   b9 02 00 00 00          mov    $0x2,%ecx
  401172:   ba 01 00 00 00          mov    $0x1,%edx
  401177:   48 8d 05 92 0e 00 00    lea    0xe92(%rip),%rax        # 402010 <_IO_stdin_used+0x10>
  40117e:   48 89 c6                mov    %rax,%rsi
  401181:   48 8d 05 8d 0e 00 00    lea    0xe8d(%rip),%rax        # 402015 <_IO_stdin_used+0x15>
  401188:   48 89 c7                mov    %rax,%rdi
  40118b:   b8 00 00 00 00          mov    $0x0,%eax
  401190:   e8 cb fe ff ff          call   401060 <printf@plt>
  401195:   b8 00 00 00 00          mov    $0x0,%eax
  40119a:   5d                      pop    %rbp
  40119b:   c3                      ret
Enter fullscreen mode Exit fullscreen mode

One of the most interesting aspects revealed by disassembly is GCC's optimization logic. For the simple string 'Hello world\n', the compiler identifies that no formatting is required and replaces the call to the more complex printf with a call to the more efficient puts function. This demonstrates that GCC is not just a translator, but an optimizer that actively refines your code for better performance.

For the first instruction the compiler fills the register rdi.

When calling the second printf, we can observe the compiler strictly adhering to the System V ABI. It populates the registers in the following order:

rdi: Holds the address of the format string.
rsi: Holds the address of the string argument 'test'.
rdx: Holds the first integer (1).
rcx: Holds the second integer (2).

By analyzing the assembly, we see exactly how the compiler maps your C variables to the CPU's 'highways'."

4. Epilogue

In this very short article we explained what gcc does when it compiles a C source code.
By disassembling your own code (with objdump), you precisely see what gcc creates during the compilation process. gcc adds control code (prologues/epilogues) and linking mechanisms (.plt, .got) that are essential for your program to communicate with the operating system. Moreover it follows a strict mechanism for passing arguments.

Top comments (3)

Collapse
 
algorhymer profile image
algorhymer

This was nice! Thanks!

You could also show bugs introduced by clang's aggressive optimization hihi!
Or, to make it beginner friendly to people like me, it might be cool to see a return oriented programming hello world!

I looked you up on GH.
Joined Github on 2016. September 18th.
First public activity on 2026.
But then I read your bio in the welcome thread, which gave some context to who you might be.
Then I opened the repo and it was written in Luc Besson language, but the README was in English, so the README had this strange déja vu vibe.

I find your area of interest really important.
I mean fundamentally important: No fixes in higher layers can help, unless the thing you mentioned is confronted by the community, and dealt with.
Oh, and also your other point that hardware is compromised (even willingly by manufacturers) is of course true too.
But, in my opinion, despite how crucially important this topic is, we have to face reality: It'll not be fixed, ever. I mean look around... what can we expect in a post-truth post-left-pad society? 🤣

This paints a somewhat bleaky picture of the future etc. so my coping mechanism / compulsive behavior is:
programming puzzles 😁
I have some puzzles which were fatfingered by Peter Norvig, Uncle Bob, Jonathan Pulson, ThePrimeagen, but there might better ways than just going blindly for the low hanging fruit.
My puzzles are about constraining people from low hanging solutions, to make the problem more enjoyable and funny.
So, in case - in the future - you want a short puff break from your main interest, just message me.
I mean... I guess smoking / Camus is still chic in Île-de-France?! (That was me attempting to sound like cool French café talk... while eating Haggis. Reality: I don't understand Coq and I cannot do OCaml.)

Collapse
 
ddupard profile image
ddupard • Edited

I am preparing a list of articles on how to find zero days automatically on any os and any CPU. Long time ago, I did the same semi manually on windows by disassembling ntoskernel, but now I created my own tools which do a lot of the job for me, and the more I confront them to hard challenges, the more I improve them. I don't use tools like Angr or r2 or softice, or ... I use my own tools except for disassembling. In my articles I will explain some of the challenges you have to solve in order to industrialize the process, but I won't push my tools to GH. As usual it will be basic articles in C and assembly which show one aspect of the problem. But before that I am going to finish the articles on what GCC does and then I will do the same for Windows, Android, and Linux ARM to show the similarities and differences of the compilation process

Collapse
 
roghayem profile image
Roghaye Mohammadi

Excellent write-up! The explanation of the ELF sections and the System V AMD64 ABI was especially interesting. I also liked the example showing how GCC optimizes printf into puts when formatting isn't needed. It was a great reminder that compilers do much more than just translate source code. Thanks for sharing!