Standalone · Systems

The hidden complexity of hello world

When you write the absolute simplest C program, one that does nothing but exit successfully, you might expect the compiled output to be trivial. Executing it involves a complex sequence of steps coordinated between the compiler, the linker, the C runtime, and the OS loader.

When you write the absolute simplest C program, one that does nothing but exit successfully, you might expect the compiled output to be trivial:

int main() {
    return 0;
}

However, executing this program involves a complex sequence of steps coordinated between the compiler, the linker, the C runtime (CRT), and the operating system's loader. Let's peel back the abstraction layers to understand exactly what happens before and after main executes.

The true entry point: _start

Contrary to popular belief, main is not the first thing executed when you run a C program. When the linker stitches your program together, it includes startup code provided by the C standard library (e.g., crt1.o in glibc). This object file defines a symbol named _start.

The linker uses a default linker script (which contains a directive like ENTRY(_start)). During the final linking phase, it resolves the virtual memory address of the _start symbol and explicitly writes this address into the e_entry field of the resulting ELF file's header. You can inspect this yourself by running readelf -h a.out | grep "Entry point address".

When the operating system (specifically the execve syscall) loads your binary into memory, it parses the ELF header, extracts the e_entry address, and sets the CPU's instruction pointer (%rip on x86-64) to that exact address. Thus, execution officially begins at _start, not main. The _start routine itself is written in pure assembly because it has to deal with the raw state of the machine exactly as the kernel left it.

Setting up the stack pointer and environment

Before _start is executed, the OS kernel (via the execve syscall) has already set up the initial execution environment: the stack pointer, argc/argv, and the environment block. _start's job is to take that raw state and turn it into the state __libc_start_main expects, then call it.

__libc_start_main initializes the C runtime: TLS (thread-local storage), constructors, and the stdio buffers. Only then does it call main. When main returns, exit() runs the destructors and calls the sys_exit syscall, and the kernel terminates the process.

The full chain: OS kernel / execve → _start → __libc_start_main → main → exit → kernel terminates process.

Mental model

main is the guest of honor at a dinner party it did not plan. The kernel sets the table (stack, argc/argv), _start greets everyone at the door (raw machine state), __libc_start_main runs the kitchen (TLS, constructors), and only then is main served. After the meal, exit cleans up and the kernel locks the door.

Contrary to popular belief, main is not the first thing executed when you run a C program. Execution officially begins at _start.

Back to the blog