Every Linux binary — every .o, shared library, and executable you've ever run — is a plain sequence of bytes arranged to a spec. No magic, just a header pointing at some tables, pointing at your code. We're going to take one apart.
The same bytes look different depending on who's reading them. The compiler wrote it linearly. The loader reads it as a set of segments to map into memory. The linker reads it as a set of sections for symbols and relocations. This strip is the file's actual byte order — everything below refers back to it.
Exactly 64 bytes on a 64-bit system, always at offset zero. It's the file's table of contents — everything else is found by following an offset written in here. Hover a row to see which bytes belong to it.
When the kernel execve()s a binary, it doesn't care about functions or variables — it cares about segments: contiguous chunks to map into virtual memory, each with an offset, a virtual address, and a permission set. A handful of PT_LOAD entries is usually all it takes.
Segments are for running the program; sections are for building and inspecting it. The linker uses them to place code, and tools like objdump and gdb use them to make sense of a binary long after it left the compiler.
| Name | Type | Flags | Relative size |
|---|---|---|---|
| .text | SHT_PROGBITS | ALLOC · EXEC | |
| .rodata | SHT_PROGBITS | ALLOC | |
| .data | SHT_PROGBITS | ALLOC · WRITE | |
| .bss | SHT_NOBITS | ALLOC · WRITE | |
| .symtab | SHT_SYMTAB | — | |
| .strtab | SHT_STRTAB | — | |
| .rela.text | SHT_RELA | — | |
| .shstrtab | SHT_STRTAB | — |
Run ./a.out and this is roughly what execve() does before your first line of main() ever runs.
Read the first 4 bytes. If they aren't 7F 45 4C 46, this isn't an ELF file — refuse immediately.
Pull e_entry, e_phoff, and e_phnum from the fixed 64-byte header.
Jump to e_phoff and read each Elf64_Phdr entry to find every PT_LOAD segment.
Map every segment at its p_vaddr with its permission bits; zero-fill the gap where memsz exceeds filesz — that's your .bss.
Write argv, envp, and the auxiliary vector onto a fresh stack for the new process.
Set the instruction pointer to the entry address from the header. The kernel's job is done — your code is now running.