QUESTION
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
Structure diagram
PT_LOAD creates runtime mappings; the section table does not directly determine this layout. ASLR can change addresses on each execution.
CALL PATH
Call path
The section table is primarily link/debug information; program headers determine runtime mappings. Match readelf -l output against /proc/PID/maps.
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.
SOURCE COORDINATES
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.
| File | Function / structure | What 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 PROGRAM
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.
cc -std=c17 -Wall -Wextra -O2 elf_start.c -o elf_start01#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
Code notes
__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.
__attribute__((destructor))Called through .fini_array on the normal exit path. It does not run after _exit(), a fatal signal, or power loss.
puts("constructorDynamic relocations and libc initialization are complete by this point, so stdio is available.
int main(int argcThe 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.
return EXIT_SUCCESSReturning from main leads to exit() inside __libc_start_main, which runs atexit handlers and flushes stdio.
DETAILS
Detailed behavior
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.
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.
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
Objects and lifetimes
| Object | Creation and release | Values to inspect |
|---|---|---|
linux_binprm | Exists temporarily during exec preparation and is consumed by the binary handler | file, buf, argc/envc |
PT_LOAD VMA | Created by exec and retained until munmap, the next exec, or process exit | offset, protection, p_filesz/p_memsz |
initial stack | Built by create_elf_tables and consumed by _start and libc | argc, argv, envp, auxv |
FAILURE PATH
Failure conditions and common misconceptions
| Observed symptom | Likely causes | How to verify |
|---|---|---|
| ENOEXEC | ELF magic, architecture, or format mismatch | file, readelf -h, kernel log |
| ENOENT even though the file exists | The dynamic linker named by PT_INTERP is absent | Inspect the interpreter reported by readelf -l |
| A crash occurs before main | A relocation, constructor, stack, or ABI problem | LD_DEBUG, gdb starti, core dump |
LAB
Verify it yourself
- Compare the LOAD addresses from readelf -l with /proc/$PID/maps at runtime for PIE and non-PIE builds.
- 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.
- Change the program to call _exit(0) and verify that destructors and stdio flushing do not run.
./elf_start one tworeadelf -h -l ./elf_start && strace -f -e execve,mmap,mprotect ./elf_startPRIMARY REFERENCES