When you type ./myprog, the kernel reads the ELF header, creates the process, and maps your code into memory — all before a single instruction of yours runs. Understanding the kernel loader explains why some ELF files are executable and others aren't, why you can't run a 32-bit binary on a 64-bit kernel without multilib, and how ASLR randomizes your program's memory layout.
The kernel's ELF loading process has three steps:
The key insight: the kernel doesn't understand relocations or symbols. It only maps raw bytes. The dynamic linker (ld.so) does the real setup work.
Each PT_LOAD segment specifies a virtual address range, file offset, and permissions:
# Elf64_Phdr structure (56 bytes): # p_type (Elf64_Word) PT_LOAD (1) # p_flags (Elf64_Word) PF_R (4), PF_W (2), PF_X (1) # p_offset (Elf64_Off) Offset in file # p_vaddr (Elf64_Addr) Virtual address to map # p_paddr (Elf64_Addr) Physical address (usually = vaddr) # p_filesz (Elf64_Xword) Size in file # p_memsz (Elf64_Xword) Size in memory (more than p_filesz, BSS is zero-filled) # p_align (Elf64_Xword) Alignment (usually page size)
Typical PT_LOAD segments:
The kernel calls mmap() for each segment, which is how shared libraries can share their code pages across processes — they're mapped read-only and marked copy-on-write.
$ readelf -l /bin/ls Elf file type is DYN (Position-Executable) Entry point 0x5a40 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg PHDR 0x000040 0x0000000000000040 0x0000000000000040 0x0002d8 0x0002d8 R E INTERP 0x000318 0x0000000000000318 0x0000000000000318 0x00001c 0x00001c R LOAD 0x000000 0x0000000000000000 0x0000000000000000 0x004000 0x004000 R E LOAD 0x004000 0x0000000000004000 0x0000000000004000 0x001a00 0x001a00 R LOAD 0x006000 0x0000000000006000 0x0000000000006000 0x001200 0x001200 R E LOAD 0x008000 0x0000000000008000 0x0000000000008000 0x000600 0x000600 R LOAD 0x009000 0x0000000000009000 0x0000000000009000 0x000200 0x000200 RW LOAD 0x00a000 0x000000000000b000 0x000000000000b000 0x000200 0x000200 RW
Notice the gaps: LOAD segments are page-aligned (0x1000 = 4KB), so the file and memory offsets may differ. The kernel only maps the portions it needs.
# See program headers (kernel loading info) readelf -l /bin/ls # See which segments are executable readelf -l /bin/ls | grep LOAD # Compare static vs dynamic entry points readelf -l static_binary | grep Entry # Entry point = 0x400xxx (direct entry into main) readelf -l dynamic_binary | grep Entry # Entry point = 0x5xxx (ld.so entry point) # Check if ASLR is enabled cat /proc/sys/kernel/randomize_va_space # 0 = disabled, 1 = stack+mmap, 2 = full (default)
Why are PT_LOAD segments mapped page-aligned, even if the file data doesn't fill the whole page?
If a PT_LOAD segment has p_flags = PF_R | PF_X but your code writes to a global variable in that segment, what happens?
The kernel maps ELF segments into memory at specific virtual addresses. But what does the final memory layout look like — where are the stack, heap, and mmap regions? That's the next concept.