The Kernel Loader

20 minadvancedModule 8: Loading & ExecutionContinues from ld-so

Why This Matters

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.

A Simple Model

The kernel's ELF loading process has three steps:

  1. Validate — Read ELF header, check magic number, verify architecture matches
  2. Map — For each PT_LOAD segment, mmap() it into the process at the specified virtual address
  3. Set up — Process PT_INTERP (dynamic linker), set up stack with argc/argv/envp, apply ASLR offsets

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.

The Actual Detail

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.

A Real Example

$ 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.

Try It

# 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)

Check Your Understanding

Why are PT_LOAD segments mapped page-aligned, even if the file data doesn't fill the whole page?

Apply It

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?

Connect

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.