Reading — step 1 of 5
Read
ELF Loading & Process Image
exec() is the moment a process sheds its old program and becomes a new one — and the contract that makes it possible is a file format. ELF (Executable and Linkable Format) is how a compiled program tells the kernel: put these bytes at these addresses with these permissions, then jump here. Loading it correctly is pure bookkeeping — which is why your exercise is exactly that bookkeeping, done by hand until it's reflex.
Two views of one file
An ELF file carries two parallel tables of contents:
- Section headers (
.text,.data,.rodata,.bss…) — fine-grained, for the linker and debuggers. The kernel ignores them entirely. - Program headers — coarse segments, for the loader. This is the kernel's shopping list, and the only part that matters at exec time.
readelf -l shows the program headers. The ones that matter are type LOAD — "map me into memory" — and each carries five numbers the loader lives by:
offset where the bytes start inside the file
vaddr where the segment must appear in virtual memory
filesz how many bytes the file provides
memsz how many bytes the segment occupies in memory
flags R / W / X permissions
The two-segment idiom (and the BSS trick)
Virtually every compiled program has the same shape:
LOAD off 0x0000 vaddr 0x400000 filesz 0x4000 memsz 0x4000 R X ← code + rodata
LOAD off 0x4000 vaddr 0x600000 filesz 0x0800 memsz 0x2400 RW ← data + bss
Look at the second segment: memsz > filesz. The file provides 0x800 bytes (initialized globals), but the segment occupies 0x2400 bytes in memory. The difference — 0x1C00 bytes — is the BSS: your zero-initialized globals. The file doesn't waste space storing a run of zeroes; the loader promises to provide zeroed memory for the gap [vaddr+filesz, vaddr+memsz). Every static int counter; in every C program lives in bytes that were never written to disk.
Your exercise: given a program-header dump, report each LOAD segment's occupied region [vaddr, vaddr+memsz) — including that zero-fill tail. It's the exact computation the kernel does before touching a single page table.
Segments become page mappings
Now connect it to chapter 1. The loader doesn't copy segments into memory — it maps them:
- Each LOAD segment becomes a range of page-table entries;
flagsbecome PTE bits.R Xcode pages: present, not writable, executable.RWdata: writable, and NX — no modern kernel maps anything writable and executable (W^X), because that combination is shellcode's front door. - The mapping is demand-paged: the kernel records "these pages come from that file range" and maps nothing. First instruction fetch at the entry point → page fault → the fault handler (paging lesson!) reads the page in and retries. Programs start fast because loading is lazy.
- Alignment:
vaddrandoffsetmust be congruent modulo the page size, or the file page can't be mapped directly — the loader rounds regions to page boundaries, which is why your computed regions land on 0x1000-multiples in practice.
The rest of the image
Segments mapped, the kernel finishes the process image: a stack (with argv, envp, and the auxiliary vector laid out at its top — how main gets its arguments), a heap start for brk/mmap to grow, and registers set so the first iret into ring 3 lands on the ELF header's entry point.
One honest omission: dynamically-linked programs (most of them) carry an INTERP header naming /lib64/ld-linux-x86-64.so.2, and the kernel actually maps and enters that — the dynamic linker — which then maps your libraries and calls your main. Static or dynamic, though, the LOAD-segment arithmetic you just practiced is the first thing that runs; get memsz vs filesz wrong and no program on the machine starts.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…