You've written main() a thousand times. But what happens before it? The startup sequence is the bridge between ld.so finishing its work and your code running. Understanding it explains why global constructors run before main, why atexit() works, and how the C runtime is initialized. It's the final piece of the ELF puzzle.
The complete startup sequence for a dynamically-linked program:
The entry point is not main() — it's _start, a small assembly stub:
# Simplified _start (x86-64): # _start: # xor %rbp, %rbp -- Clear frame pointer (marks end of stack) # pop %rsi -- argc (from stack) # mov %rsp, %rdx -- argv (stack pointer) # and $-16, %rsp -- Align stack to 16 bytes (ABI requirement) # push %rax -- Push garbage for alignment # push %rsp -- Push stack pointer (for __libc_start_main) # lea __libc_start_main@GOTPCREL(%rip), %rax # mov (%rax), %rax -- Load address of __libc_start_main # lea main(%rip), %rdi -- First arg: pointer to main() # call *%rax -- Call __libc_start_main
Key observations:
Global constructors (.init_array) run before main:
# Global constructors run before main: # __attribute__((constructor)) void my_init() -- runs before main # __attribute__((destructor)) void my_fini() -- runs after main returns
$ cat startup.c # startup.c: # __attribute__((constructor)) before_main() -- prints "[constructor]" # __attribute__((destructor)) after_main() -- prints "[destructor]" # int main() -- prints "[main]" # void cleanup() -- prints "[atexit]" # __attribute__((constructor)) register_cleanup() -- calls atexit(cleanup) $ gcc -o startup startup.c $ ./startup [constructor] runs before main [main] argc=1 [atexit] registered cleanup [destructor] runs after main returns # Trace the actual calls LD_DEBUG=all ./startup | grep -E "init|constructor|main"
# See the actual entry point readelf -e /bin/ls | grep "Entry point" # Trace the full startup LD_DEBUG=all ls | tail -30 # See init/fini arrays readelf -d /bin/ls | grep -E "INIT|FINI" # Check your program's constructors gcc -o test test.c; LD_DEBUG=all ./test | grep -i init
What is the relationship between _start and main()?
You write a program with a constructor that calls printf("init"), but the output appears before "init" is printed. What's happening?
Congratulations — you've completed the ELF course! You now understand the complete journey from source code to running program: the ELF format, sections, segments, symbols, relocations, dynamic linking, and the startup sequence. This knowledge is the foundation for understanding compilers, linkers, loaders, and system-level debugging.
Next steps: Apply this knowledge by debugging real programs with GDB, reading compiler-generated assembly, and exploring how the linker resolves symbols across translation units.