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
Leave a comment
Your email address will not be published. Required fields are marked *