While sections organize a binary for the linker, program headers (segments) tell the kernel how to load the binary into memory. The kernel ignores section names entirely — it only reads the program header table.
Memory Map
PT_LOAD — The Workhorse
PT_LOAD segments are the only ones that actually get mapped into memory. A typical executable has two or three PT_LOAD segments:
Code Segment
Contains .text, .rodata, and other read/execute sections. Mapped as readable and executable but not writable.
Data Segment
Contains .data, .bss, .got, and other writable sections. Mapped as readable and writable.
Read-Only Segment
Contains the ELF header, program headers, and other metadata. Mapped as read-only.
PT_INTERP — Dynamic Linker Path
This segment contains a single null-terminated string: the path to the dynamic linker (usually /lib64/ld-linux-x86-64.so.2). When the kernel loads a dynamically-linked executable, it reads this path and hands control to the dynamic linker before your program's main() runs.
PT_DYNAMIC — Dynamic Section Pointer
Points to the .dynamic section, which contains the roadmap for dynamic linking: which shared libraries to load, where to find the symbol table, relocation entries, and more. The dynamic linker reads this to set up the process.
p_memsz > p_filesz — The BSS Gap
When a segment's memory size exceeds its file size, the extra bytes are zero-filled by the kernel. This is how .bss data gets included in a PT_LOAD segment without wasting disk space. The segment covers both initialized data (from the file) and uninitialized data (zero-filled by the kernel).
Program Header Table
Why might p_memsz be larger than p_filesz?