CLOSE

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

  1. The compiler translates main() into machine code.
  2. The linker bundles necessary libraries and creates an executable.
  3. 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, and envp)
  • 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:

  1. gcc compiles hello.c into an object file (hello.o).
  2. gcc automatically links it with startup files.
  3. 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