Syscall / ELF · Linux userspace / kernel ABI

ELF loading and _start

Examines the sequence after execve through ELF program headers, the interpreter, initial stack, _start, __libc_start_main, and main.

Series
04 / 38
Build
cc -std=c17 -Wall -Wextra -O2 elf_start.c -o elf_start
Run
./elf_start one two
Kernel
Linux 6.18.37 LTS

Where does the code that runs before main() come from?

The ELF loader does not load an executable by consulting section names. It creates VMAs from the file offsets, virtual addresses, and permissions specified by PT_LOAD program headers, and also loads the dynamic linker when PT_INTERP is present.

The first userspace instruction of a new process image is the ELF entry point, not main. In a typical glibc executable, _start from crt1 extracts argc and argv from the initial stack and passes them to __libc_start_main.

Structure diagram

Figure 1. Virtual-address layout of an ELF process immediately after execve
high virtual addresseslow virtual addresses
user stackargc · argv · envp · auxv
vDSO / vvaruserspace helpers supplied by the kernel
ld-linux + shared objectsPT_INTERP · relocation · libc
mmap areaanonymous/file mappings
heapbrk and allocator arenas
main ELF PT_LOADR-- headers · R-X text · RW- data/bss

PT_LOAD creates runtime mappings; the section table does not directly determine this layout. ASLR can change addresses on each execution.

Call path

Figure 2. From userspace code to observable results
execve pathname/argv/envp
ELF phdr PT_LOAD/PT_INTERP
ld-linux relocations and dependencies
_start crt startup
main C runtime ready

The section table is primarily link/debug information; program headers determine runtime mappings. Match readelf -l output against /proc/PID/maps.

Figure 3. Major points along the kernel-internal path
do_execveat_common prepare linux_binprm
search_binary_handler select format
load_elf_binary VMAs and stack
start_thread set IP/SP
EL0 execute ELF entry

This diagram is not for memorizing function names. Follow which return value, file descriptor, memory mapping, or wait queue is passed to the next stage.

Linux 6.18.37 LTS source locations

Go beyond the glibc function to the files where the syscall implementation meets kernel objects. Each link points to the original file at the same tag.

FileFunction / structureWhat to inspect
fs/exec.c do_execveat_common(), begin_new_exec() The point that discards the old address space and commits to the new binary
fs/binfmt_elf.c load_elf_binary(), create_elf_tables() PT_LOAD mappings and initial-stack construction
arch/x86/include/asm/processor.h start_thread() Set the new instruction pointer and stack pointer

Complete runnable example

The code below is not pseudocode with explanatory lines omitted. It is a minimal example that can be built and run as a file.

Buildcc -std=c17 -Wall -Wextra -O2 elf_start.c -o elf_start
01#include <stdio.h>
02#include <stdlib.h>
03
04static void before_main(void) __attribute__((constructor));
05static void after_main(void) __attribute__((destructor));
06
07static void before_main(void)
08{
09    puts("constructor: runtime is ready");
10}
11
12static void after_main(void)
13{
14    puts("destructor: normal exit path");
15}
16
17int main(int argc, char **argv, char **envp)
18{
19    printf("main: argc=%d argv0=%s\n", argc, argv[0]);
20    printf("argv=%p envp=%p\n", (void *)argv, (void *)envp);
21    return EXIT_SUCCESS;
22}

Code notes

Source line 4__attribute__((constructor))

The linker places the function address in .init_array, and the C runtime walks the array before main. The ELF entry point itself is not changed to this function.

Source line 5__attribute__((destructor))

Called through .fini_array on the normal exit path. It does not run after _exit(), a fatal signal, or power loss.

Source line 9puts("constructor

Dynamic relocations and libc initialization are complete by this point, so stdio is available.

Source line 17int main(int argc

The original argc/argv/envp begin on the initial stack built by the kernel, but the C runtime reconstructs the call to main according to the ABI.

Source line 21return EXIT_SUCCESS

Returning from main leads to exit() inside __libc_start_main, which runs atexit handlers and flushes stdio.

Detailed behavior

01

PT_LOAD is the blueprint for a VMA

p_offset and p_vaddr must have matching page offsets. The loader maps the file-backed range and fills the tail where p_memsz exceeds p_filesz with 0 to create .bss.

Check W^X policy in the program-header permissions and the mprotect operations performed for relocations.

02

The initial stack contains more than strings

The auxiliary vector follows argc, the argv pointer array, and the envp pointer array. libc and the dynamic linker consume entries such as AT_PHDR, AT_ENTRY, AT_RANDOM, and AT_SYSINFO_EHDR.

getauxval() reads these values without requiring you to parse /proc/self/auxv directly.

03

A successful exec has no return path

When execve succeeds, it replaces the calling process's code, data, and stack, so execution never returns to the next instruction in the old image. -1 and errno are returned only on failure.

When called from a multithreaded process, all other threads disappear and only the calling thread becomes the initial thread of the new image.

Objects and lifetimes

ObjectCreation and releaseValues to inspect
linux_binprmExists temporarily during exec preparation and is consumed by the binary handlerfile, buf, argc/envc
PT_LOAD VMACreated by exec and retained until munmap, the next exec, or process exitoffset, protection, p_filesz/p_memsz
initial stackBuilt by create_elf_tables and consumed by _start and libcargc, argv, envp, auxv

Failure conditions and common misconceptions

Observed symptomLikely causesHow to verify
ENOEXECELF magic, architecture, or format mismatchfile, readelf -h, kernel log
ENOENT even though the file existsThe dynamic linker named by PT_INTERP is absentInspect the interpreter reported by readelf -l
A crash occurs before mainA relocation, constructor, stack, or ABI problemLD_DEBUG, gdb starti, core dump

Verify it yourself

  1. Compare the LOAD addresses from readelf -l with /proc/$PID/maps at runtime for PIE and non-PIE builds.
  2. Use starti in gdb to inspect the stack at the first _start instruction, and use x/32gx $rsp to find argc and the pointer arrays.
  3. Change the program to call _exit(0) and verify that destructors and stdio flushing do not run.
Run./elf_start one two
Tracereadelf -h -l ./elf_start && strace -f -e execve,mmap,mprotect ./elf_start

Primary sources