ELF (Executable and Linkable Format) is the most common object and executable file format on Unix-like systems.

Most people don’t need to care about ELF until production bites:

  • ldd looks fine, but the binary still fails with not found
  • you hit undefined symbol after a library upgrade
  • you get wrong ELF class when a 64-bit binary tries to load a 32-bit library (or vice versa)
  • you’re handed a random binary and need to answer: static or dynamic? which loader? which segments?

Once you understand the ELF structure, the chain of compile -> link -> load -> run becomes debuggable.

A 3-minute triage path for dynamic linking issues

Rule of thumb: ldd is a simulation under the current environment, not a perfect guarantee of runtime behavior.

When you see “library not found / wrong version loaded”, walk this path:

  1. Confirm platform/arch:
file ./a.out
readelf -h ./a.out | head
  1. Check what the binary declares as dependencies (NEEDED):
readelf -d ./a.out | grep NEEDED
  1. Inspect runtime search hints (RPATH/RUNPATH):
readelf -d ./a.out | grep -E 'RPATH|RUNPATH'
  1. Ask the loader to explain itself:
LD_DEBUG=libs,files ./a.out 2>&1 | head -n 80
  1. Compare with system cache and env:
ldconfig -p | grep <libname>
echo $LD_LIBRARY_PATH

The rest of the post explains why these knobs exist and how sections/segments relate to what the loader does.

Three ELF types

  • Relocatable: Object files (.o) emitted by the compiler/assembler, waiting for the linker to merge and fix addresses.
  • Executable: A program that can be loaded and run directly.
  • Shared Object: A shared library (.so) linked at runtime.

From source to execution: a clear chain

Source/Assembly
  ↓ Compile/Assemble
Object file (.o, with Sections)
  ↓ Link
Executable (ELF, with Segments)
  ↓ Loader
Mapped into memory and executed

Two perspectives: Section vs Segment

ELF provides two views:

  • Linker view: ELF is a set of Sections that store code, data, symbols, relocations, etc.
  • Loader view: ELF is a set of Segments that describe memory mappings and permissions (R/W/X).

A simplified correspondence:

Section view (linker)               Segment view (loader)
[ELF Header]                        [ELF Header]
[Section Header Table]              [Program Header Table]
  .text                               LOAD (R-X) <- .text
  .data                               LOAD (RW-) <- .data + .bss
  .bss
  .symtab/.strtab
  .rel.*

Relocatable file example: ELF Header + Sections

A relocatable object (.o) header excerpt from readelf -h:

ELF Header:
  Class:                             ELF64
  Data:                              2's complement, little endian
  Type:                              REL (Relocatable file)
  Machine:                           Advanced Micro Devices X86-64
  Entry point address:               0x0
  Start of section headers:          0x2c0 (bytes into file)
  Number of section headers:         12

A readelf -S excerpt (key columns only):

[Nr] Name      Type      Addr   Off    Size   ES Flg Lk Inf Al
[ 1] .text     PROGBITS  0000   0040   0038   00 AX  0  0  16
[ 2] .rel.text REL       0000   0210   0018   08     6  1  8
[ 3] .data     PROGBITS  0000   0080   0020   00 WA  0  0  8
[ 4] .bss      NOBITS    0000   00a0   0010   00 WA  0  0  8
[ 5] .symtab   SYMTAB    0000   0240   00f0   18     6  8  8
[ 6] .strtab   STRTAB    0000   0330   0048   00     0  0  1

Field notes:

  • Addr: Load address (often 0 in relocatable files; fixed by the linker).
  • Off/Size: File offset and size.
  • Flg: Permissions (A=alloc, X=exec, W=write).

Object file layout (simplified)

0x0000  ELF Header
0x0040  .text
0x0080  .data
0x00a0  .bss (no file bytes)
0x0210  .rel.text
0x0240  .symtab
0x0330  .strtab
0x02c0  Section Header Table

Executable example: Program Headers and Segments

After linking, an executable includes Program Headers:

Program Headers:
  Type   Offset  VirtAddr  FileSiz MemSiz  Flg Align
  LOAD   0x0000  0x400000  0x0800  0x0800  R E 0x1000
  LOAD   0x1000  0x601000  0x0200  0x0300  RW  0x1000

Explanation:

  • First LOAD: Contains .text, permissions R-X.
  • Second LOAD: Contains .data + .bss, permissions RW-.
  • MemSiz > FileSiz usually means .bss occupies memory only.

Use readelf -l to view Section to Segment mapping and see how Sections are merged into Segments.

Relocation: turning placeholders into real addresses

Object files often contain placeholder addresses that the linker fixes using .rel.* entries.

A simplified example (pseudo-assembly):

mov    data_items(%rip), %rax   ; access a global array

In the object file the encoding may contain placeholders:

8b 04 bd 00 00 00 00

After linking it becomes a real address:

8b 04 bd a0 90 04 08

Corresponding relocation entry (excerpt):

Relocation section '.rel.text' contains 1 entry:
  Offset  Info   Type       Sym.Name
  0x0008  ...    R_X86_64_32 data_items

Key idea: the linker patches specific offsets based on relocation tables.

Shared libraries and PIC / GOT / PLT

Shared objects must load at arbitrary addresses, so they use PIC (position-independent code):

  • GOT (Global Offset Table): Stores real addresses of variables/functions.
  • PLT (Procedure Linkage Table): A jump stub used for lazy binding.

A typical PLT entry (simplified):

push@plt:
  jmp *GOT[push]
  pushq $reloc_index
  jmp plt0

The first call enters the dynamic linker; subsequent calls jump directly via the GOT entry.

One more real-world gotcha: “I installed the library, why can’t it be found?”

Three common causes:

  1. The file exists but isn’t in default search paths (for example /usr/local/lib without ldconfig / ld.so.conf.d).
  2. Multiple copies of the same SONAME: RUNPATH/RPATH and LD_LIBRARY_PATH may load a different one than you expect.
  3. ABI/arch mismatch: 64-bit vs 32-bit (wrong ELF class).

If you just want the truth fast, LD_DEBUG=libs is usually quicker than guessing.

Summary

The key to ELF is: linkers care about Sections, loaders care about Segments. Relocatable files emphasize linkability, executables emphasize loadability, and shared objects emphasize relocation and dynamic linking.

FAQ

Q1: Why keep the Section Header Table in executables?
A: Loaders do not need it, but debuggers and analysis tools rely on it.

Q2: Why does .bss not occupy file space?
A: It only records the size; memory is allocated and zeroed at load time.

Q3: Why are so many addresses 0 in object files?
A: They are placeholders patched later using relocation records.

Q4: Why do shared libraries require PIC?
A: They must load at different addresses across processes, so absolute addresses cannot be hard-coded.

Q5: Why are segment permissions page-based?
A: The MMU enforces permissions per page, so code and data are typically mapped into separate pages.

References