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:
lddlooks fine, but the binary still fails withnot found- you hit
undefined symbolafter a library upgrade - you get
wrong ELF classwhen 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:
- Confirm platform/arch:
file ./a.out
readelf -h ./a.out | head
- Check what the binary declares as dependencies (NEEDED):
readelf -d ./a.out | grep NEEDED
- Inspect runtime search hints (RPATH/RUNPATH):
readelf -d ./a.out | grep -E 'RPATH|RUNPATH'
- Ask the loader to explain itself:
LD_DEBUG=libs,files ./a.out 2>&1 | head -n 80
- 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 > FileSizusually means.bssoccupies 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:
- The file exists but isn’t in default search paths (for example
/usr/local/libwithoutldconfig/ld.so.conf.d). - Multiple copies of the same SONAME:
RUNPATH/RPATHandLD_LIBRARY_PATHmay load a different one than you expect. - 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.