Skip to content
ELF Loading & Process Image
step 1/5

Reading — step 1 of 5

Read

~3 min readFile Systems & Beyond

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; flags become PTE bits. R X code pages: present, not writable, executable. RW data: 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: vaddr and offset must 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…