When we write a simple C
program like the following:
#incldue <stdio.h>
int main () {
printf("Hello World!\n");
return 0;
}
When we compile it and run it with the following commands:
// Compilation
gcc hello-world.c -o hello-world
// Execute
./hello-world
When we execute it, we might assume that our function main()
is the first piece of code to execute. It should prints “Hello World!
” to the screen, and then exists.
However behind the scenes a lot of thing happened before executing the main()
function. There are actually a few special pieces of code that run before or after main()
, preparing the environment for the program. These pieces of code are part of the the C Runtime (CRT)
startup objects. Among them you may have encountered files like crt0.o
, crt1.o
, crti.o
and crtn.o
.
Consider it with the a real-life example, Imagine you are throwing a party before guests arrive main()
, you need to set up chairs, snacks, and music. The crt
files are like invisible helpers that do this setup and cleanup for us.
We can call C Runtime as the hidden code that runs before and after the
main()
function.
What We Think Happens
- The compiler translates
main()
into machine code. - The linker bundles necessary libraries and creates an executable.
- The operating system loads the executable into memory and starts executing
main()
.
While this explanation makes sense at a high level, it completely ignores the runtime initialization that must happen before main()
is ever called.
What Actually Happens
Before main()
functions runs, the operating system hands control over to a C Runtime startup file
that sets up the execution environment. This is where crt0
, crt1
, crti
, and crtn
comes into play.
The Role of the C Runtime (CRT)
The CRT (C Runtime) is responsible for:
- Setting up the memory environments (stack, heap, BSS)
- Initializing global variables and static data.
- Handling program arguments (
argc
,argv
, andenvp
) - Calling global constructors (for C++) and destructors
- Preparing the environment for
main()
- Exiting the program cleanly after
main()
returns.
Compilation and Linking
When we link the program, the linker automatically pulls in startup object files from the standard C library (libc
, glibc
or musl
) or from the compiler toolchain and links with our program. This happens behind the scenes unless we explicitly disable it with compiler flag like -nostartupfiles
.
When we compile our program:
gcc hello.c -o hello
What actually happens is:
gcc
compileshello.c
into an object file (hello.o
).gcc
automatically links it with startup files.- The linker resolves all dependencies and creates an executable.
To see this in action, we can inspect the linked objects:
gcc -v hello.c -o hello
This will show that the linker includes startup object files before and after hello.o
in the final executabl.
CRT0
Purpose: Prepares the program's execution environment (memory, stack, global variables etc.).
Think of it as the stage crew
that sets up lights, props, and seating before the play (main()
) begins.
What Does crt0
do?
1 Set Up the Stack
Programs need memory for local variables and function calls. So it initializes the stack pointer (ESP/RSP
on x86/64
).
; Example (x86 Assembly):
movl $stack_top, %esp ; Set stack pointer to the top of the stack
2 Initialize the .bss
Section
The .bss
section, the memory for uninitialized global variables (e.g., int global_var;
).
These variables must be set to 0
before use.
; Zero the .bss section
movl $__bss_start, %edi ; Start address of .bss
movl $__bss_end, %ecx ; End address of .bss
subl %edi, %ecx ; Calculate size of .bss
xorl %eax, %eax ; Fill with zeros
rep stosb ; Repeat: store EAX (0) into [EDI++]
3 Copy Initialized Data to RAM
Global variables initialized with values (e.g., int x = 7'
)
These values are stored in ROM (Read-Only Memory) and need to be copied to RAM.
; Copy .data section from ROM to RAM
movl $__data_start_rom, %esi ; Source (ROM)
movl $__data_start_ram, %edi ; Destination (RAM)
movl $__data_size, %ecx ; Size of the section
rep movsb ; Copy byte by byte
4 Prepare Command-Line Arguments
Passes arguments from the OS to main()
.
Which are main(int argc, char **argv)
; Example (Linux x86):
pop %eax ; Get argc (number of arguments)
pop %ebx ; Get argv (pointer to arguments)
5 Call main()
Transfers control to the program.
call main ; Jump to the main() function
6 Handle Program Exit
Clean up after main()
returns (e.g., call `exit() or return to the OS).
; Example (Linux x86 exit syscall):
movl $1, %eax ; Syscall number for exit
movl $0, %ebx ; Exit code (0 = success)
int $0x80 ; Trigger the syscall