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) ;
}
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 yourmainfunction. They handle global setup and teardown. .plt(Procedure Linkage Table): This section acts as a "trampoline" for external library functions (likeprintf). Since the exact memory address oflibcis determined at runtime, the.pltenables dynamic linking..text: This is where your actual code—themainfunction 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 yourmainfunction 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
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 (0)