_start and __libc_start_main
mainis not the entry point of a C program. The ELF entry point — the address ine_entrythat the kernel or the dynamic linker jumps to — is_start, and between_startand the first instruction ofmainsits__libc_start_main, the C library function that turns a raw execution environment into a working C program._start, supplied by the crt startup object (crt1.o/Scrt1.o), is thirteen instructions of hand-written assembly: it readsargcandargvoff the initial stack, zeroes the frame pointer, re-aligns the stack, and calls__libc_start_main, passingmainand a set of housekeeping pointers.__libc_start_maindoes the real work — it caches the auxiliary vector, parses tunables, probes CPU features, sets up thread-local storage and the thread pointer, derives the stack-smashing canary from the kernel’sAT_RANDOMbytes, registers teardown handlers, runs the executable’s constructors, callsmain(argc, argv, envp), and passesmain’s return value toexit(). It never returns to_start(start.S, glibc 2.43; libc-start.c, glibc 2.43). That ordering is the reason a constructor that touches a__threadvariable behaves differently from one that does not, and the reason a hand-rolled-nostdlibentry point segfaults the first time it reads%fs:0— both demonstrated with real output below. This note traces both halves; the crt object files that supply_startare covered in The C Runtime Startup Files crt.
Authority for this note. Every source-level claim is pinned to glibc 2.43 (read from sourceware.org at refs/tags/glibc-2.43 — the bminor/glibc GitHub mirror has no 2.43 tag) and to the x86-64 psABI. Every measured claim was produced on the host described in the Demonstrating It sections: Fedora 44, kernel 7.1.13, glibc 2.43, GCC 16.2.1, binutils 2.46.1, AMD Ryzen AI MAX+ 395. Where glibc’s own comments contradict each other, the note says so rather than picking one.
Mental Model
There is a chasm between what the kernel provides and what C code assumes. The kernel provides a stack with argc/argv/envp/auxv on it, an instruction pointer, and nothing else — no thread pointer, so errno and thread-local variables have nowhere to live; no stack canary, so the compiler’s stack-protector would read garbage; no initialized libc globals; and no registered destructors, so atexit would have nowhere to record handlers. __libc_start_main is the bridge across that chasm. _start is just the on-ramp: it adapts the kernel’s stack-based calling convention to the register-based C convention and hands off.
The single most important idea is the division of labor: _start is deliberately minimal and per-architecture (it must run before any C environment exists, so it cannot be C), while __libc_start_main is portable C that does everything that can be done in C. And the second most important idea is the hand-off to exit: main returning is not the end of the program — its return value flows into exit(), which runs the whole teardown path.
sequenceDiagram participant K as kernel execve participant D as ld.so (_dl_start) participant S as _start (Scrt1.o) participant L as __libc_start_main participant M as main participant X as exit K->>D: jump to PT_INTERP e_entry<br/>%rsp -> argc, %rdx -> rtld_fini D->>D: map DSOs, relocate,<br/>_dl_init: preinit_array + dependency ctors D->>S: jump to executable e_entry S->>S: endbr64, xor %ebp, %rdx -> %r9,<br/>pop argc, argv -> %rdx, realign %rsp S->>L: call __libc_start_main(main, argc, argv,<br/>NULL, NULL, rtld_fini, stack_end) Note over L: STATIC-ONLY BLOCK (#ifndef SHARED) L->>L: __environ, __libc_stack_end, _dl_aux_init(auxv) L->>L: __tunables_init, ARCH_INIT_CPU_FEATURES L->>L: _dl_relocate_static_pie, ARCH_SETUP_IREL L->>L: ARCH_SETUP_TLS: allocate TCB, set %fs base rect rgb(220, 245, 220) Note over L,M: TLS, errno and %fs:0x28 are live from here on end L->>L: _dl_setup_stack_chk_guard -> %fs:0x28 L->>L: _dl_setup_pointer_guard -> %fs:0x30 Note over L: BOTH BUILDS L->>L: __cxa_atexit(rtld_fini) L->>L: call_init: DT_INIT + DT_INIT_ARRAY<br/>of the MAIN EXECUTABLE L->>M: __libc_start_call_main -> main(argc, argv, envp) M-->>L: int return value L->>X: exit(return value) Note over X: __run_exit_handlers: dtors, _dl_fini,<br/>stdio flush, then the _exit syscall
The _start → __libc_start_main → main → exit ritual for a dynamically linked executable, per glibc 2.43 csu/libc-start.c. What it shows: the dynamic linker runs first and handles DT_PREINIT_ARRAY and every dependency’s constructors, then jumps to _start; _start adapts the stack to the register calling convention; __libc_start_main establishes TLS and the guards (in the static build only — in a dynamic build ld.so did it already), registers teardown, runs the main executable’s own constructors, and only then calls main. The insight to take: the green band is the whole point. Everything above it runs with %fs pointing at nothing useful; everything below it — including every __attribute__((constructor)) in your program — can touch errno, __thread variables, and the stack canary. Code that straddles that line is where “it crashes before main with no backtrace” comes from.
The same information as a checklist of what is and is not usable at each stage. The “not yet safe” column is the operational content: it is the list of things that will fail silently or crash if you call them from a hand-written entry point or from a constructor placed too early.
| Stage | Who runs it | What becomes available | What is still not safe to use |
|---|---|---|---|
kernel execve → e_entry | kernel load_elf_binary | stack with argc/argv/envp/auxv; %rdx = rtld_fini; [vdso] mapped | everything: no %fs base, no .data relocation for a PIE, no libc state |
ld.so relocation | _dl_start / _dl_main | all DSOs mapped and relocated; the initial TLS block and TCB allocated; %fs base set | the executable’s own constructors have not run |
_dl_init | elf/dl-init.c | DT_PREINIT_ARRAY of the executable, then each dependency’s DT_INIT/DT_INIT_ARRAY | the executable’s own .init_array (deliberately skipped here — see below) |
_start | Scrt1.o/crt1.o | argument registers loaded, stack re-aligned, %rbp zeroed, cfi_undefined(rip) set | nothing new; this is 13 instructions of plumbing |
__libc_start_main early (static build only) | csu/libc-start.c | __environ, __libc_stack_end, _dl_aux_init, tunables, CPU features, static-PIE self-relocation, IRELATIVE resolution | TLS is set up inside this block — before ARCH_SETUP_TLS() nothing thread-local works |
ARCH_SETUP_TLS() | __libc_setup_tls | TCB allocated, %fs base installed → errno, __thread, %fs:0x28 slot exist | the canary value is still zero for one more step |
| canary + pointer guard | _dl_setup_stack_chk_guard, _dl_setup_pointer_guard | %fs:0x28 and %fs:0x30 hold real random values from AT_RANDOM | stdio is not initialised yet |
__libc_early_init / __libc_init_first | csu/init-first.c | __libc_multiple_libcs, program name, stdio bootstrap | atexit handlers for the program are not registered yet |
__cxa_atexit(rtld_fini) (+ call_fini when static) | csu/libc-start.c | teardown path armed | — |
call_init | csu/libc-start.c (dynamic: walks the main map’s DT_INIT_ARRAY) | the executable’s __attribute__((constructor)) functions run, in .init_array order | main has not run; anything a constructor expects main to have set is still unset |
main | your code | everything | — |
exit | stdlib/exit.c | __run_exit_handlers walks the registered list newest-first: destructors, _dl_fini, _IO_cleanup | after _exit(2) nothing runs |
Stage-by-stage view of the same sequence. What it shows: which component owns each step, and — in the last column — the precondition each step establishes for the next. The insight to take: TLS availability is the sharp edge. It is established roughly two-thirds of the way through __libc_start_main’s static-build prologue, and everything before it (tunables parsing, CPU-feature probing, static-PIE self-relocation) is written in a deliberately TLS-free, relocation-free style precisely because it runs on the wrong side of that line.
_start — the psABI entry contract
The x86-64 psABI is unusually terse about process entry, and that terseness is the point: only three registers have defined values, everything else is garbage, and all the actual data arrives on the stack. Quoting the specification’s Initial Stack and Register State section directly (x86-64 psABI, low-level-sys-info.tex):
| Register / state | Value at process entry | Why _start cares |
|---|---|---|
%rsp | “the address of the byte with lowest address which is part of the stack. It is guaranteed to be 16-byte aligned at process entry.” In practice this is &argc. | Every argument to __libc_start_main except rtld_fini is dug out of here |
%rdx | “a function pointer that the application should register with atexit” — this is rtld_fini, the dynamic linker’s finalizer | Moved straight to %r9 (6th argument) before anything can clobber it |
%rbp | “unspecified … but the user code should mark the deepest stack frame by setting the frame pointer to zero” | xor %ebp, %ebp — makes frame-pointer unwinders stop here |
| every other GPR | unspecified | Cannot be read; %rax in particular is used as push-padding, not data |
rFLAGS | DF=0 (direction forward), CF=PF=AF=ZF=SF=OF=0 | String instructions may assume forward direction without a cld |
| x87 FPCW | RC=0 (round to nearest), PC=11 (double-extended), all exception masks set | libm can assume a sane default FPU environment |
MXCSR | FZ=0 (no flush-to-zero), RC=0, DAZ=0 (denormals are not zero) | SSE code gets IEEE-correct denormal handling by default |
The complete x86-64 process-entry contract. What it shows: the specification defines three registers, the floating-point control state, and the stack; nothing else. The insight to take: _start exists because C’s calling convention is register-based and the kernel’s hand-off is stack-based. Those seven lines of the psABI are the entire interface between the kernel and every userspace language runtime on the platform — Go, Rust and a -nostdlib C program all start from exactly this state.
The stack itself, per the psABI’s Initial Process Stack figure, is four null-terminated vectors laid end to end, growing toward higher addresses away from %rsp. Real offsets, measured on this box with ./stackmap one two (source in Demonstrating It below):
block-beta columns 3 a["LOW ADDRESSES<br/>(stack grows down into here)"]:3 b1["%rsp + 0x00"] b2["long argc"] b3["measured: 0x7ffd25a99f40, value 3"] c1["%rsp + 0x08"] c2["char *argv[0..argc-1]"] c3["-> 0x7ffd25a9ba3f "./stackmap""] d1["%rsp + 8+8*argc"] d2["NULL"] d3["argv terminator, at +0x20"] e1["%rsp + 8+8*(argc+1)"] e2["char *envp[0..n-1]"] e3["at +0x28, 117 entries here"] f1["after envp"] f2["NULL"] f3["envp terminator"] g1["%rsp + 0x3d8"] g2["Elf64_auxv_t auxv[]<br/>(a_type, a_un.a_val) pairs"] g3["22 entries x 16 bytes"] h1["end of auxv"] h2["AT_NULL entry"] h3["auxv terminator"] i["Information block: the argument strings, environment strings and<br/>AT_RANDOM / AT_PLATFORM bytes the pointers above point into"]:3 j["HIGH ADDRESSES (top of the [stack] VMA)"]:3 style a fill:#eee,stroke:#999 style j fill:#eee,stroke:#999 style b2 fill:#ffe8cc style g2 fill:#cce5ff
The initial process stack at _start, with real addresses from a run of stackmap one two on this host. What it shows: four consecutive NULL-terminated regions — argv, envp, auxv — anchored at %rsp, followed by the information block holding the strings they point into. There is no length field anywhere: argc bounds argv, and everything after it is found by walking to the next NULL. The insight to take: the auxiliary vector’s position is not a constant — it is &argc + 8 + 8*(argc+1) + 8*(nenv+1), which is why _dl_aux_init cannot be handed a pointer and must walk past envp’s NULL to find it (while (*evp++ != NULL); in csu/libc-start.c). At +0x3d8 in this run, the auxv sat almost a kilobyte past %rsp purely because the shell exported 117 environment variables.
_start translates this state into a call to __libc_start_main, whose signature in glibc 2.43 is:
STATIC int
LIBC_START_MAIN (int (*main) (int, char **, char ** MAIN_AUXVEC_DECL),
int argc, char **argv,
__typeof (main) init, /* legacy constructor runner — always NULL now */
void (*fini) (void), /* legacy destructor runner — completely unused */
void (*rtld_fini) (void), /* ld.so's finalizer, arrived in %rdx */
void *stack_end); /* see the note on "stack_end" below */Here is the actual x86-64 assembly at glibc 2.43, which differs from the description usually given in two ways that matter (start.S, glibc 2.43). Disassembled from a PIE binary built on this host with GCC 16.2.1:
0000000000000390 <_start>:
390: f3 0f 1e fa endbr64 # CET landing pad (indirect jump target)
394: 31 ed xor %ebp,%ebp # psABI: mark the outermost frame
396: 49 89 d1 mov %rdx,%r9 # rtld_fini -> arg 6
399: 5e pop %rsi # argc -> arg 2 (%rsp now &argv[0])
39a: 48 89 e2 mov %rsp,%rdx # argv -> arg 3
39d: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp # re-align: pop left %rsp at 8 mod 16
3a1: 50 push %rax # 8 bytes of deliberate garbage
3a2: 54 push %rsp # arg 7 (stack_end), passed on the stack
3a3: 45 31 c0 xor %r8d,%r8d # fini = NULL -> arg 5
3a6: 31 c9 xor %ecx,%ecx # init = NULL -> arg 4
3a8: 48 8d 3d d1 ff ff ff lea -0x2f(%rip),%rdi # &main -> arg 1 (relaxed from main@GOTPCREL)
3af: ff 15 0b 2c 00 00 call *0x2c0b(%rip) # __libc_start_main@GLIBC_2.34
3b5: f4 hlt # unreachable: the callee never returns
First correction: the alignment sequence is not a bare and. The psABI guarantees %rsp is 16-byte aligned at entry; pop %rsi then leaves it at 8 mod 16. and $-16 rounds it back down to 0 mod 16, push %rax pushes eight bytes of intentional garbage (the source comment is literally “Push garbage because we push 8 more bytes”), and push %rsp pushes the seventh argument. That leaves %rsp at 0 mod 16 immediately before the call, so that on entry to __libc_start_main — after the call has pushed an 8-byte return address — %rsp + 8 is 16-aligned, which is exactly what the ABI requires of a called function. Describing this as “masks %rsp to align it” skips the part that actually passes an argument.
Second correction: what stack_end means. push %rsp on x86-64 pushes the value of %rsp before the decrement, so stack_end is the address of the garbage %rax slot — i.e. essentially the entry %rsp, rounded. glibc’s two comments about it contradict each other: start.S says “Provide the highest stack address to the user code (for stacks which grow downwards)”, while csu/libc-start.c says “Store the lowest stack address.” They are describing the same number from opposite framings — it is the lowest address currently part of the in-use stack (the psABI’s own wording for %rsp) and simultaneously the highest address the stack will ever occupy, because it grows downward from there. Measured on this host, __libc_stack_end came out at 0x7ffdf14c05f0 inside a [stack] VMA spanning 0x7ffdf14a1000–0x7ffdf14c4000 — 14 KB below the top of the VMA (the information block sits above it) and 126 KB above the bottom. It is unambiguously near the high end.
The remaining details are as usually described. endbr64 is the Intel CET (Control-flow Enforcement Technology) landing pad required because e_entry is reached by an indirect transfer. init and fini are zeroed because, since glibc 2.34, they are vestigial: the header comment in libc-start.c states “The init and fini parameters are no longer used. fini is completely unused, init is still called if not NULL, but the current startup code always passes NULL.” Before 2.34 they were __libc_csu_init and __libc_csu_fini from csu/elf-init.c (elf-init.c at glibc 2.33 — the file is 404 at the 2.43 tag, deleted outright). The lea -0x2f(%rip),%rdi above is the linker having relaxed the source’s main@GOTPCREL(%rip) into a direct PC-relative address, legal because main is defined in the same link unit; in a non-PIE build on this same host the instruction is instead mov $0x400380,%rdi, an absolute immediate. The object-file details — crt1.o vs Scrt1.o, Mcrt1.o, gcrt1.o — live in The C Runtime Startup Files crt.
One more line the usual description omits: immediately before xor %ebp, %ebp the source emits cfi_undefined (rip), with the comment “Clearing frame pointer is insufficient, use CFI.” Zeroing %rbp stops frame-pointer-based unwinders; marking the return address column undefined in the DWARF Call Frame Information stops .eh_frame-based unwinders, which is the mechanism libgcc’s unwinder and backtrace(3) actually use. Both are needed to make _start a genuine terminator.
__libc_start_main — the initialization ritual, step by step
Inside __libc_start_main (which on modern glibc delegates the final main call to a helper, __libc_start_call_main), the operations run in a deliberate order, each establishing a precondition the next relies on (libc-start.c, glibc 2.40):
The first thing to understand about this function is that it is compiled twice, differently. csu/libc-start.c opens with the comment “Note: This code is only part of the startup code proper for statically linked binaries. For dynamically linked binaries, it resides in libc.so.” The whole early-initialisation block — environment, auxv, tunables, CPU features, static-PIE self-relocation, TLS, canary, pointer guard — sits inside #ifndef SHARED. In a dynamically linked program none of it runs here, because ld.so did all of it before _start was ever reached. Only the last third of the function is common to both builds.
With that established, here is the exact glibc 2.43 order, read top to bottom from LIBC_START_MAIN in csu/libc-start.c:
The #ifndef SHARED prologue — static builds only
-
Locate the environment and record the stack end.
char **ev = &argv[argc + 1]; __environ = ev;— the environment array begins one word pastargv’s NULL terminator (see The Initial Process Stack). Then__libc_stack_end = stack_end, with the comment “This is done inld.soif this is the code for the DSO” — an explicit statement that the dynamic path assigns the same variable elsewhere. -
Find and parse the auxiliary vector. Because the psABI does not hand anyone a pointer to the auxv, glibc walks to it:
char **evp = ev; while (*evp++ != NULL); auxvec = (ElfW(auxv_t) *) evp;. Then_dl_aux_init(auxvec)caches the entries libc needs — notably_dl_random(theAT_RANDOMpointer),AT_PAGESZ,AT_HWCAP,AT_PHDR— both for the steps below and for latergetauxval(3)calls.AT_PHDRmatters immediately: the source comment says “First process the auxiliary vector since we need to find the program header to locate an eventually presentPT_TLSentry.” Contents and semantics of the vector are The Auxiliary Vector’s subject. -
Tunables.
__tunables_init(__environ)parsesGLIBC_TUNABLESfrom the environment. This must precede CPU-feature detection because tunables such asglibc.cpu.hwcapscan mask features the CPU actually has. -
CPU features.
ARCH_INIT_CPU_FEATURES()(on x86-64,init_cpu_features()viaCPUID) fills thecpu_featuresstructure that IFUNC resolvers consult to pick an AVX2 or AVX-512memcpy— see GNU IFUNC. -
Static-PIE self-relocation.
_dl_relocate_static_pie(). The comment is the interesting part: “Do static pie self relocation after tunables and cpu features are setup for ifunc resolvers. Before this point relocations must be avoided.” A statically linked PIE has nold.so, so until this line runs, every absolute pointer in the binary’s own data is still un-rebased — which is why steps 1–4 are written to touch only stack values and immediates. See Relocation Processing. -
ARCH_SETUP_IREL(). AppliesR_X86_64_IRELATIVErelocations, running each IFUNC resolver and patching in the chosen implementation. -
ARCH_SETUP_TLS()— the line everything hinges on. Expands to__libc_setup_tls(), which finds thePT_TLSprogram header, allocates the initial thread’s TLS block plus the Thread Control Block (TCB), and sets the thread-pointer register (%fson x86-64, viaarch_prctl(ARCH_SET_FS)). The in-source comment gives the reason it is here and not later: “The stack guard goes into the TCB, so initialize it early.” Before this line,errno— which is__thread int— cannot be read or written, and any__threadaccess is a load from an unset segment base. TLS mechanics are Thread-Local Storage and The Thread Control Block. -
ARCH_APPLY_IREL(). A second IFUNC hook, deliberately after TLS, because on some architectures (the comment names powerpc) the resolvers want to read hwcap and platform fields that live in the TCB. -
The stack canary.
uintptr_t stack_chk_guard = _dl_setup_stack_chk_guard (_dl_random);thenTHREAD_SET_STACK_GUARD (stack_chk_guard), which on x86-64 stores it at%fs:0x28. The compiler’s-fstack-protectorprologue copies that word onto the frame and the epilogue compares it; a mismatch calls__stack_chk_fail. glibc zeroes the guard’s least-significant byte so that astrcpy-style overflow, which stops at a NUL, cannot cheaply leak or forge it. Measured on this host — the identity is confirmed, not assumed:$ ./guard %fs:0x00 (TCB self ptr) = 0x00007fcd4f5cb740 %fs:0x28 (stack canary) = 0xc2b4615d88987800 %fs:0x30 (pointer guard)= 0x2a5804728f9d4326 AT_RANDOM[0..7] LE = 0xc2b4615d8898787d (low byte zeroed -> 0xc2b4615d88987800) canary == AT_RANDOM[0..7] with low byte zeroed? YESNote that
%fs:0x30, the pointer guard, is not derived from the same eight bytes —_dl_setup_pointer_guard(_dl_random, stack_chk_guard)takes a second argument and draws on the remainingAT_RANDOMbytes. The relationship betweenAT_RANDOMand%fs:0x28is owned by The Auxiliary Vector; it is reproduced here only because it is the concrete output of this step. -
__pthread_initialize_minimal()if it is non-NULL, then the pointer guard (_dl_setup_pointer_guard→THREAD_SET_POINTER_GUARD,%fs:0x30), used byPTR_MANGLE/PTR_DEMANGLEto obfuscate function pointers stored in longjmp buffers andatexitrecords.
The common tail — both builds
-
Register the dynamic linker’s finalizer.
if (__glibc_likely (rtld_fini != NULL)) __cxa_atexit ((void (*) (void *)) rtld_fini, NULL, NULL);— this is outside the#ifndef SHAREDguard and runs in both builds. It is the first entry on the exit-handler list, which is walked newest-first, so it runs last. See atexit and Process Teardown. -
Early libc init (static only).
__libc_early_init (true), then__libc_init_first (argc, argv, __environ)— which on Linux sets__libc_multiple_libcs, stores__libc_argc/__libc_argv/__environ, and calls__init_miscto deriveprogram_invocation_name(csu/init-first.c, glibc 2.43). Then__cxa_atexit (call_fini, NULL, NULL)registers the static build’s.fini_arrayrunner, and if__libc_enable_secureis set,__libc_check_standard_fds()refuses to start a set-user-ID binary whose fds 0/1/2 are closed — a mitigation against the classic “open a file and it becomes stdout” privilege-escalation trick. -
Run the executable’s constructors — in both builds.
call_init (argc, argv, __environ). This is the step most descriptions get wrong, and it is the subject of the next section. -
Audit hook and hand-off.
_dl_audit_preinit(...)fires thela_preinitrtld-audit checkpoint, then__libc_start_call_main (main, argc, argv)callsmain(argc, argv, envp)and passes the result toexit().return 0;frommainis exactlyexit(0). Becauseexitends the process,__libc_start_mainis declared__attribute__((noreturn))and thehltafter thecallin_startis unreachable (exit.c, glibc 2.43).
Two orderings in that list are worth memorising because they are load-bearing and non-obvious: tunables before CPU features (a tunable can mask a feature), and static-PIE relocation before anything that dereferences a pointer from the binary’s own data (there is no ld.so to have done it).
Who actually runs your constructors
The widely repeated claim — including in the earlier revision of this note — is that __libc_start_main runs .init_array only for statically linked programs, and that for a dynamic binary _dl_init already ran everything. That is wrong at glibc 2.43, and it has been wrong since 2.34. The file defines call_init twice:
#ifdef SHARED
/* Initialization for dynamic executables. Find the main executable
link map and run its init functions. */
static void
call_init (int argc, char **argv, char **env)
{
struct link_map *l = GL(dl_ns)[LM_ID_BASE]._ns_loaded;
/* DT_PREINIT_ARRAY is not processed here. It is already handled in
_dl_init in elf/dl-init.c. ... */
if (ELF_INITFINI && l->l_info[DT_INIT] != NULL)
DL_CALL_DT_INIT(l, l->l_addr + l->l_info[DT_INIT]->d_un.d_ptr, argc, argv, env);
ElfW(Dyn) *init_array = l->l_info[DT_INIT_ARRAY];
if (init_array != NULL) { /* … call each pointer with (argc, argv, env) … */ }
}
#else /* !SHARED */
static void
call_init (int argc, char **argv, char **envp)
{
/* For static executables, preinit happens right before init. */
for (i = 0; i < __preinit_array_end - __preinit_array_start; i++)
(*__preinit_array_start[i]) (argc, argv, envp);
# if ELF_INITFINI
_init ();
# endif
for (size_t i = 0; i < __init_array_end - __init_array_start; i++)
(*__init_array_start[i]) (argc, argv, envp);
}
#endifThe two differ in where they find the array, not in whether they run it. The static version uses the linker-provided __init_array_start/__init_array_end boundary symbols. The dynamic version walks the main executable’s link_map and reads DT_INIT_ARRAY out of its dynamic section — exactly what the deleted __libc_csu_init used to do with boundary symbols, relocated into libc.
The other half of the mechanism is in elf/dl-init.c. _dl_init iterates the search list and calls its own call_init(l, …) for each object — but that function begins with an explicit skip:
/* Check for object which constructors we do not run here. */
if (__builtin_expect (l->l_name[0], 'a') == '\0'
&& l->l_type == lt_executable)
return;The main executable is the one link_map whose l_name is the empty string. _dl_init therefore handles DT_PREINIT_ARRAY and every dependency’s constructors and then deliberately declines to run the executable’s own — leaving that job to __libc_start_main. The division is clean, and directly observable. The test program below calls backtrace(3) + dladdr(3) from four places and prints which object each caller lives in:
$ ./exe
EXE preinit_array <- ?@exe <- ?@ld-linux-x86-64.so.2 <- ?@ld-linux-x86-64.so.2
SHLIB ctor <- ?@libdemo.so <- ?@ld-linux-x86-64.so.2 <- ?@ld-linux-x86-64.so.2
EXE ctor prio 101 <- ?@exe <- __libc_start_main@libc.so.6 <- _start@exe
EXE ctor no prio <- ?@exe <- __libc_start_main@libc.so.6 <- _start@exe
main <- main@exe <- ?@libc.so.6 <- __libc_start_main@libc.so.6
Read the object names in the right-hand columns. The preinit function and the shared library’s constructor are called from ld-linux-x86-64.so.2 — that is _dl_init. The executable’s own constructors, both of them, are called from __libc_start_main in libc.so.6, whose caller is _start in the executable. And main’s caller is an unnamed function in libc.so.6 — that is __libc_start_call_main, which readelf -sW /lib64/libc.so.6 confirms is a LOCAL symbol (4: 0000000000003600 271 FUNC LOCAL DEFAULT 4 __libc_start_call_main), which is why dladdr cannot name it.
flowchart TD START["execve, then ld.so takes control"] --> DLINIT["_dl_init(main_map, argc, argv, env)<br/>elf/dl-init.c"] DLINIT --> PRE["DT_PREINIT_ARRAY<br/>of the MAIN EXECUTABLE only"] PRE --> LOOP{"for each object in<br/>main_map->l_searchlist,<br/>dependencies first"} LOOP --> SKIP{"l_name[0] == 0<br/>AND l_type == lt_executable?"} SKIP -->|"yes: this IS the<br/>main executable"| RET["return — deliberately<br/>run NOTHING here"] SKIP -->|"no: a dependency .so"| DEP["DT_INIT then DT_INIT_ARRAY<br/>of that shared object"] DEP --> LOOP RET --> ENTRY["jump to executable e_entry = _start"] ENTRY --> LSM["__libc_start_main<br/>csu/libc-start.c"] LSM --> CI{"compiled with SHARED?"} CI -->|"yes (dynamic)"| CID["call_init: read DT_INIT +<br/>DT_INIT_ARRAY from the main map's<br/>dynamic section"] CI -->|"no (static)"| CIS["call_init: preinit_array, _init,<br/>then __init_array_start..end<br/>via linker boundary symbols"] CID --> MAIN["main(argc, argv, envp)"] CIS --> MAIN style RET fill:#ffd9d9,stroke:#c33 style CID fill:#d9f0d9,stroke:#3a3 style CIS fill:#d9f0d9,stroke:#3a3
Who runs which constructor array, per glibc 2.43. What it shows: _dl_init owns DT_PREINIT_ARRAY and every dependency’s constructors, then hits an explicit early-return for the main executable (red); __libc_start_main picks that job up afterwards (green), in both the static and dynamic builds, differing only in where it finds the array. The insight to take: the red box is the whole subtlety. The main executable is visited by _dl_init’s loop like every other object and is then deliberately skipped, which is why “the dynamic linker runs all the constructors” is a plausible-sounding claim that the source contradicts in three lines.
The practical consequence: if you set a breakpoint in a constructor of the main executable and walk up the stack, you will land in libc, not in ld.so. A profiler that attributes your executable’s constructor time to libc.so rather than to the dynamic linker is behaving correctly. The reverse is true for anything in a .so you link against.
.preinit_array, .init_array, .fini_array and constructor priority
The three arrays are ELF-standard: DT_PREINIT_ARRAY, DT_INIT_ARRAY and DT_FINI_ARRAY, each with a matching …SZ giving the byte size (System V gABI, ch. 5 Dynamic Section; elf(5)). Their contract:
| Section / tag | When it runs | Run by (dynamic exe) | Argument signature | Order | Allowed in a shared object? |
|---|---|---|---|---|---|
.preinit_array / DT_PREINIT_ARRAY | before any constructor, including dependencies’ | _dl_init (elf/dl-init.c) | (int argc, char **argv, char **envp) | array order | No — the gABI restricts it to the executable |
.init / DT_INIT (legacy _init) | after preinit, before .init_array | __libc_start_main’s call_init via DL_CALL_DT_INIT | (argc, argv, envp) | single function | yes |
.init_array / DT_INIT_ARRAY | after DT_INIT | __libc_start_main’s call_init | (int argc, char **argv, char **envp) | forward, index 0 first | yes (run by _dl_init) |
.fini_array / DT_FINI_ARRAY | at exit, before .fini | _dl_fini (dynamic) / call_fini registered with __cxa_atexit (static) | (void) | reverse, last index first | yes |
.fini / DT_FINI (legacy _fini) | last | same | (void) | single function | yes |
Two details in that table are routinely missed. First, .init_array entries are called with argc, argv and envp — a constructor declared void f(void) works only because the extra arguments sit in registers it ignores; declare it static void f(int argc, char **argv, char **envp) and it receives the real thing. Second, .fini_array runs backwards, which is what makes constructor/destructor pairs nest correctly.
Priority is a GCC feature layered on top, implemented purely through section naming and a linker sort. Per the GCC manual, the priority “is a constant integral expression bounded between 101 and 65535 inclusive; priorities 0-100 are reserved for use by the compiler and its runtime libraries. A constructor with a smaller priority number runs before a constructor with a larger priority number; the opposite relationship holds for destructors… without an argument, the priority argument is equivalent to 65535” (GCC Common Function Attributes). Compiling a file with five constructors at different priorities on GCC 16.2.1 and reading back the section headers confirms the mechanism exactly:
$ readelf -SW prio.o | grep init_array
[ 4] .init_array.00101 INIT_ARRAY … 000008 … constructor(101)
[ 6] .init_array.01000 INIT_ARRAY … 000008 … constructor(1000)
[ 8] .init_array.65534 INIT_ARRAY … 000008 … constructor(65534)
[10] .init_array INIT_ARRAY … 000010 … constructor(65535) AND bare constructor
| What you write | Section GCC emits | Effect |
|---|---|---|
__attribute__((constructor(100))) | rejected | warning: constructor priorities from 0 to 100 are reserved for the implementation [-Wprio-ctor-dtor] |
__attribute__((constructor(101))) | .init_array.00101 | runs first among prioritised |
__attribute__((constructor(1000))) | .init_array.01000 | sorted numerically by the zero-padded suffix |
__attribute__((constructor(65534))) | .init_array.65534 | last prioritised entry |
__attribute__((constructor(65535))) | plain .init_array | identical to no priority — 65535 is GCC’s default |
__attribute__((constructor)) | plain .init_array | runs after every prioritised entry, in link order |
The sort is done by the linker script, not the loader. ld -Map on the demo binary shows the two input patterns in the order the default script applies them:
.init_array 0x0000000000402d98 0x20
*(SORT_BY_INIT_PRIORITY(.init_array.*) SORT_BY_INIT_PRIORITY(.ctors.*))
.init_array.00101 0x0000000000402d98 0x8 /tmp/ccLUevi3.o
*(.init_array EXCLUDE_FILE(*crtend?.o *crtbegin?.o) .ctors)
.init_array 0x0000000000402da0 0x8 /usr/lib/gcc/x86_64-redhat-linux/16/crtbegin.o
.init_array 0x0000000000402da8 0x10 /tmp/ccLUevi3.o
All .init_array.NNNNN inputs, sorted numerically, come first; then everything in plain .init_array, in link order. That is why an unprioritised constructor can never beat a prioritised one, and why crtbegin.o’s frame_dummy lands between them. Dumping the finished array confirms it:
$ readelf -x .preinit_array ctor
0x00402d90 47064000 00000000 G.@..... -> 0x400647 preinit
$ readelf -x .init_array ctor
0x00402d98 c9054000 00000000 10054000 00000000 -> 0x4005c9 c101, 0x400510 frame_dummy
0x00402da8 da054000 00000000 eb054000 00000000 -> 0x4005da c65535, 0x4005eb cdef
$ readelf -x .fini_array ctor
0x00402db8 25064000 00000000 e0044000 00000000 -> 0x400625 d101, 0x4004e0 __do_global_dtors_aux
0x00402dc8 36064000 00000000 -> 0x400636 ddef
…and running it confirms both the forward init order and the reverse fini order:
$ ./ctor
PREINIT array fn caller chain: <- ? <- ? <- ?
ctor prio 101 caller chain: <- ? <- __libc_start_main <- _start
ctor prio 65535 caller chain: <- ? <- __libc_start_main <- _start
ctor no prio caller chain: <- ? <- __libc_start_main <- _start
TLS readable in ctor? tlsvar=0x1234 &tlsvar=0x7fde59d9577c
main() __libc_stack_end = 0x7ffdf14c05f0
[stack] VMA: 7ffdf14a1000-7ffdf14c4000 rw-p 00000000 00:00 0 [stack]
dtor no prio
dtor prio 101
c65535 was declared constructor(65535) and ran after c101 but in the same plain-.init_array bucket as the unprioritised one — the measurement that proves 65535 really is “no priority” rather than “lowest priority”. And the destructors mirror: ddef (later in the array) ran before d101 (earlier in the array), because .fini_array is walked backwards.
Note also the line TLS readable in ctor? tlsvar=0x1234 — a __thread variable read successfully from inside a constructor. That is the payoff of TLS being established before step 13: your constructors are on the safe side of the line. The next section shows what happens on the other side. .init_array semantics in their own right are Init Array Constructors and Destructors’s subject.
Demonstrating It: a _start with no libc underneath it
The cleanest way to see what __libc_start_main provides is to remove it. This program is built -nostdlib -nostartfiles -static: no crt1.o, no libc, no dynamic linker, its own _start, and raw syscall instructions for write and exit. It reads the initial stack exactly as _start does, walks past argv and envp to the auxiliary vector, prints a few entries — and then reads %fs:0.
static __thread int tls_probe = 0xABCD; /* needs a thread pointer nobody set up */
__asm__(".globl _start\n"
"_start:\n"
" xor %ebp, %ebp\n"
" mov %rsp, %rdi\n" /* pass the raw stack pointer as arg 1 */
" and $-16, %rsp\n"
" call _start_c\n"
" hlt\n");
void _start_c(long *sp) {
long argc = sp[0];
char **argv = (char **)&sp[1];
char **e = argv + argc + 1; while (*e) e++;
Elf64_auxv_t *av = (Elf64_auxv_t *)(e + 1);
/* … walk av, print AT_RANDOM / AT_SYSINFO_EHDR / AT_PAGESZ / AT_HWCAP / AT_ENTRY … */
unsigned long fs0;
__asm__ volatile ("mov %%fs:0, %0" : "=r"(fs0)); /* the moment of truth */
/* … never reached … */
}$ gcc -O1 -nostdlib -nostartfiles -static -o bare bare.c && ./bare ; echo "exit status: $?"
hand-written _start, no libc:
argc = 0x0000000000000001
argv[0] @ 0x00007fff73562a53
&argc(%rsp on entry) = 0x00007fff73560f10
AT_RANDOM = 0x00007fff73561449
AT_SYSINFO_EHDR = 0x00007f1756ef8000
AT_PAGESZ = 0x0000000000001000
AT_HWCAP = 0x00000000178bfbff
AT_ENTRY = 0x00000000004001c0
exit status: 139
Everything the kernel provides works perfectly: argc, argv, the environment, the whole auxiliary vector, the [vdso] base in AT_SYSINFO_EHDR, and the write(2)/exit(2) system calls used to print it. What is missing is everything libc provides. Exit status 139 is 128 + 11 — SIGSEGV — raised at the single instruction mov %fs:0, %rax, because arch_prctl(ARCH_SET_FS, …) was never called and the %fs base is zero. The program never reached the tls_probe read on the next line.
That one fault is the whole thesis of this note in a single data point. The kernel hands you a complete process. It does not hand you a C environment. errno is __thread int, so it lives behind %fs; the stack canary lives at %fs:0x28; pthread_self() is %fs:0x10. All three are the same fault. Add __libc_setup_tls() — or just link against crt1.o and let __libc_start_main run — and every one of them starts working at the same instant.
Uncertain
Verify: the behaviour of the static-only prologue (steps 1–10 above) as executed, rather than as read. Reason: this host has no
glibc-staticpackage (/usr/bin/ld.bfd: cannot find -lc: have you installed the static version of the c library?), so every static-linkage claim in this note is read from glibc 2.43 source rather than observed. The dynamic-linkage claims — who calls the constructors, priority ordering, the canary identity, the%fsfault — are all measured. To resolve: installglibc-staticand re-run the./exebacktrace demo asgcc -static, expecting the constructor callers to become__libc_start_mainwith nold.soframes at all. uncertain
The __libc_start_call_main split (glibc 2.34+)
Since glibc 2.34 the call to main has been factored out of __libc_start_main into __libc_start_call_main, defined per-sysdeps in libc_start_call_main.h. The generic version is a one-liner:
_Noreturn static __always_inline void
__libc_start_call_main (int (*main) (int, char **, char ** MAIN_AUXVEC_DECL),
int argc, char **argv MAIN_AUXVEC_DECL)
{
exit (main (argc, argv, __environ MAIN_AUXVEC_PARAM));
}The version actually used on Linux is sysdeps/nptl/libc_start_call_main.h, and it does something the usual “it exists for unwinding correctness” explanation understates. It allocates a struct pthread_unwind_buf on the stack, setjmps into it, installs it as the main thread’s cancellation buffer, and then calls main:
struct pthread_unwind_buf unwind_buf;
not_first_call = setjmp ((struct __jmp_buf_tag *) unwind_buf.cancel_jmp_buf);
if (__glibc_likely (! not_first_call))
{ struct pthread *self = THREAD_SELF; /* … chain unwind_buf, call main … */ }
else
{
__nptl_deallocate_tsd ();
/* … */
if (atomic_fetch_add_relaxed (&__nptl_nthreads, -1) != 1)
while (1) INTERNAL_SYSCALL_CALL (exit, 0); /* exit the THREAD, not the process */
}
exit (result);The else branch is reached by longjmp, and the thing that jumps there is pthread_exit() called from main. That is a legal and slightly exotic operation: it must terminate the initial thread without terminating the process if other threads are still running. __libc_start_call_main is the frame that makes it work — it is the landing pad for the unwind, it releases thread-specific data, decrements __nptl_nthreads, and only if the count hits zero does it fall through to exit(result). If other threads remain it issues the raw exit system call (which ends one thread) in a loop instead. A C++ exception escaping main terminates at this frame for the same structural reason.
The function is deliberately not exported. readelf -sW /lib64/libc.so.6 on this host (glibc 2.43) shows:
4: 0000000000003600 271 FUNC LOCAL DEFAULT 4 __libc_start_call_main
1838: 0000000000003710 336 FUNC GLOBAL DEFAULT 4 __libc_start_main@@GLIBC_2.34
1840: 0000000000003710 336 FUNC GLOBAL DEFAULT 4 __libc_start_main@GLIBC_2.2.5
4365: 0000000000003710 336 FUNC LOCAL DEFAULT 4 __libc_start_main_impl
Note the two versions of __libc_start_main at the same address: @@GLIBC_2.34 is the default that new binaries link against, and @GLIBC_2.2.5 is the compat alias for binaries built before the init/fini parameters became vestigial. (On x86-64 the base version is GLIBC_2.2.5, not the GLIBC_2_0 that libc-start.c’s SHLIB_COMPAT macro nominally names — the architecture’s oldest version wins.) Both alias __libc_start_main_impl, which is itself LOCAL.
Failure Modes and Common Misunderstandings
“main is the entry point.” main is late. _start → __libc_start_main → call_init → your constructors all precede it. A crash inside a constructor produces a core dump whose backtrace never reaches main, and a gdb start command (which breaks on main) will sail straight past the bug. Use starti, which breaks at e_entry, or break _dl_init.
Uninitialized TLS/errno in a hand-rolled _start. Demonstrated above: exit status 139 at mov %fs:0, %rax. A -nostdlib program that provides its own _start but skips __libc_setup_tls will crash the moment any code touches errno or a __thread variable. The symptom is a SIGSEGV at an address suspiciously close to zero (%fs base 0 plus a small offset), which looks like a null-pointer dereference and is diagnosed as one for an embarrassingly long time.
Stack-protector false failures. If the canary slot is never initialised, the -fstack-protector epilogue compares against whatever is at %fs:0x28 and the program aborts in __stack_chk_fail for no apparent reason. Because the canary is per-process (derived once from AT_RANDOM, not re-rolled per thread), a clone(2) that forgets to copy the TCB produces the same failure in the child only.
Expecting __libc_csu_init/__libc_csu_fini. Tooling or tutorials that look up these symbols are describing pre-2.34 glibc. On glibc ≥ 2.34 they do not exist — csu/elf-init.c, the file that defined them, is 404 at the 2.43 tag, and _start passes zeros for init/fini. Setting a breakpoint on __libc_csu_init on a modern system simply never fires, and exploit write-ups that rely on a __libc_csu_init gadget chain (the “ret2csu” technique) do not apply to binaries linked against glibc 2.34 or newer.
“__libc_start_main does not run constructors for a dynamic binary — _dl_init did.” The most common error about this function, and measurably false: see Who actually runs your constructors above. _dl_init explicitly skips the main executable (if (l->l_name[0] == '\0' && l->l_type == lt_executable) return;) and __libc_start_main’s own SHARED build of call_init runs it. What _dl_init does own is DT_PREINIT_ARRAY and every dependency.
Assuming stack_end is the bottom of the stack. It is the high end. glibc’s own two comments disagree about the wording; the measurement above settles it. Code that treats __libc_stack_end as a lower bound and walks upward will walk off the end of the stack VMA.
Assuming .init_array entries take no arguments. They are called with (argc, argv, envp). A constructor that needs the command line does not have to stash it from main — it is already in the registers.
Alternatives and When This Differs
musl. musl’s split is cleaner than glibc’s and differs in one way that can bite you. Its crt/crt1.c is C, not assembly — the per-architecture shim in crt_arch.h does nothing but hand the raw stack pointer to _start_c(long *p), which unpacks it:
void _start_c(long *p)
{
int argc = p[0];
char **argv = (void *)(p+1);
__libc_start_main(main, argc, argv, _init, _fini, 0);
}Note that musl still passes _init and _fini, but __libc_start_main’s parameters for them are literally named init_dummy and fini_dummy and are never used; the real work is done by libc_start_init, which calls the weak file-scope _init() and then walks the array (musl 1.2.5 src/env/__libc_start_main.c). Its __init_libc performs the same sequence glibc does — cache auxv, __init_tls(aux), __init_ssp(aux[AT_RANDOM]), then the set-user-ID standard-fd check — in about forty lines. The portability trap: musl calls .init_array entries with no arguments:
for (; a<(uintptr_t)&__init_array_end; a+=sizeof(void(*)()))
(*(void (**)(void))a)();A constructor written as static void f(int argc, char **argv, char **envp), which is well-defined on glibc, reads three garbage registers under musl. Write constructors as void f(void) if the binary may ever be built against musl. See glibc vs musl and The C Runtime Startup Files crt.
Other language runtimes replace the upper half, not the lower. A Go binary’s _start is _rt0_amd64_linux, which transfers to the runtime’s rt0_go bootstrap rather than to __libc_start_main; it reads the same stack per the same psABI contract and then builds goroutine stacks, the g0/m0 pair and the scheduler instead of a single-threaded C environment (see Static and Dynamic Linking in Go). Rust keeps glibc’s _start and hooks in at lang_start, a function the main shim calls, so the entire ritual described here runs unchanged beneath it.
Freestanding. A -nostartfiles -nostdlib program supplies its own _start and does none of this — no TLS, no canary, no constructors, no atexit, no stdio — and must call the exit system call directly, as the bare demo above does. Appropriate for OS kernels, shellcode, init-less containers and very small static tools; the price is the SIGSEGV shown above the first time anything touches %fs.
Production Notes
__libc_start_main is one of the most-interposed functions in the ecosystem. LD_PRELOAD tooling and several fuzzers (notably AFL’s forkserver and libFuzzer’s driver) hook it to get control before main while still having a fully initialised libc underneath — a strictly better place to hook than a constructor, because by the time __libc_start_main is entered the dynamic linker has finished and every dependency’s constructor has run, but the executable’s own constructors have not.
Diagnosing “it crashes before main.” Because the whole ritual precedes main, the standard debugger reflex is wrong. gdb’s start command breaks on main and therefore runs straight through the bug; use starti, which stops at e_entry — the first instruction of _start — or break on _dl_init to catch the dependency constructors. LD_DEBUG=all (which drives the GLRO(dl_debug_mask) & DL_DEBUG_IMPCALLS branches visible in the source above) prints calling init:, calling preinit:, initialize program: and transferring control: lines that bracket each stage, and is usually faster than stepping. The three common root causes are a constructor that blocks on something not yet available (a lock, a network socket, another library’s constructor), a TLS-model mismatch (an initial-exec __thread variable in a dlopened library), and a static-PIE relocation issue.
The GLIBC_2.34 version wall. A binary linked on a glibc-2.34-or-newer host records a dependency on __libc_start_main@GLIBC_2.34 — visible in the disassembly above. Copied to a host with an older libc it fails at load time with version 'GLIBC_2.34' not found (required by ./prog), before a single instruction of the program runs, and no amount of LD_LIBRARY_PATH fixes it. This is the single most common “works on my machine” failure when shipping a dynamically linked binary built on a recent distribution, and it is a direct consequence of the csu rework described here. See Versioned Symbols in glibc.
Reverse engineering. _start is the reliable anchor in a stripped binary: it is at e_entry, it is always a dozen-odd instructions, and the value it loads into %rdi immediately before call *__libc_start_main is the address of main. In the PIE disassembly above that is lea -0x2f(%rip),%rdi; in a non-PIE build it is mov $0x400380,%rdi. Every decompiler’s “find main” heuristic is this pattern. The __libc_csu_init gadget chain (“ret2csu”), long a staple of return-oriented-programming write-ups for controlling %rdx on x86-64, relies on a function that no longer exists in glibc ≥ 2.34 — a good example of a mitigation arriving as a side effect of a cleanup rather than as a security change.
Constructors are not free and not ordered across translation units. Priority only orders constructors within a link, and only relative to other prioritised ones; across shared objects the order is the dependency order _dl_init computes, and circular dependencies are explicitly unspecified — elf/dl-init.c’s comment on the matter (“Stupid users forced the ELF specification to be changed… Stupidity rules!”) is unusually candid about how load-bearing and how fragile this is. Static initialisation order problems in C++ are the same bug wearing different clothes; the robust fix is a function-local static (lazy init) rather than a priority number.
See Also
- The C Runtime Startup Files crt — the
crt1.o/Scrt1.o/gcrt1.oobjects that supply_start, andcrtbegin.o/crtend.o’sframe_dummy - The Initial Process Stack — the stack
_startreadsargc/argvfrom, in its own right - The Auxiliary Vector — parsed by
_dl_aux_init; owns theAT_RANDOM→%fs:0x28relationship this note only reproduces as output - Init Array Constructors and Destructors —
.init_arraysemantics as a topic; this note covers only who calls it and when - atexit and Process Teardown — where
rtld_finiandcall_finiare registered, and whatexitdoes with them - Thread-Local Storage · The Thread Control Block — the TLS block and TCB that
ARCH_SETUP_TLS()establishes - The Dynamic Linker —
_dl_start,_dl_mainand_dl_init, which run before_startin a dynamic binary - The Dynamic Section — where the dynamic
call_initreadsDT_INIT_ARRAYfrom - Relocation Processing — what
_dl_relocate_static_pie()does, and why it must precede any pointer dereference - GNU IFUNC — the
IRELATIVErelocationsARCH_SETUP_IREL()/ARCH_APPLY_IREL()resolve - Versioned Symbols in glibc — why
__libc_start_main@GLIBC_2.34is a hard runtime dependency - The vDSO Virtual Dynamic Shared Object — the
AT_SYSINFO_EHDRentry thebaredemo printed, and what it points at - Process Startup Sequence — the whole path this ritual sits inside
- Compilers and Toolchains MOC · Linux Userspace Runtime MOC — parent maps (§5, Process Startup and the Initial Stack)