The Startup Sequence

20 minadvancedModule 8: Loading & ExecutionContinues from memory-layout

Why This Matters

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.

A Simple Model

The complete startup sequence for a dynamically-linked program:

  1. Kernel loads ld.so — Maps the program and the dynamic linker
  2. ld.so initializes — Processes .dynamic, maps all DT_NEEDED libraries, applies relocations
  3. ld.so calls _start — The program's true entry point (set by the linker)
  4. _start calls __libc_start_main — The C runtime setup function
  5. __libc_start_main:
    • Processes environment variables (PATH, LD_*, etc.)
    • Initializes stdio (stdin/stdout/stderr)
    • Calls .init_array constructors
    • Sets up atexit() handler chain
    • Calls main(argc, argv, envp)
  6. main() returns — Control returns to __libc_start_main
  7. Cleanup — Calls atexit() handlers, .fini_array destructors, calls exit()

The Actual Detail

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

A Real Example

$ 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"

Try It

# 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

Check Your Understanding

What is the relationship between _start and main()?

Apply It

You write a program with a constructor that calls printf("init"), but the output appears before "init" is printed. What's happening?

Connect

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.