Home CS Fundamentals Linkers and Loaders: ELF, PE, and Mach-O Deep Dive
Advanced 3 min · July 13, 2026

Linkers and Loaders: ELF, PE, and Mach-O Deep Dive

Master linkers and loaders: understand ELF, PE, and Mach-O formats, symbol resolution, relocation, and debugging production issues.

N
Naren Founder & Principal Engineer

20+ years shipping production systems from the metal up. Lessons pulled from things that broke in production.

Follow
Production
production tested
July 13, 2026
last updated
2,165
articles · all by Naren
Before you start⏱ 20-25 min read
  • Basic understanding of C/C++ compilation
  • Familiarity with command-line tools
  • Knowledge of memory management concepts
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Linkers combine object files into executables, resolving symbols and performing relocation.
  • ELF (Linux), PE (Windows), and Mach-O (macOS) are the three major executable formats.
  • Loaders map executables into memory, resolve dynamic libraries, and start execution.
  • Common issues include undefined symbols, relocation overflow, and dynamic linking errors.
  • Debugging involves tools like objdump, readelf, dumpbin, and otool.
✦ Definition~90s read
What is Linkers and Loaders?

A linker combines object files into an executable, resolving symbols and performing relocation, while a loader loads the executable into memory and starts execution.

Think of a linker as a librarian who takes several chapters (object files) written by different authors, resolves cross-references (symbols), and binds them into a single book (executable).
Plain-English First

Think of a linker as a librarian who takes several chapters (object files) written by different authors, resolves cross-references (symbols), and binds them into a single book (executable). The loader is like a reader who opens the book, finds the page numbers for each chapter, and starts reading from the beginning.

When you compile a C++ program, the compiler produces object files (.o on Linux, .obj on Windows). These contain machine code but with placeholders for symbols defined in other files. The linker's job is to resolve these symbols, assign final addresses, and produce an executable. The loader then loads that executable into memory and starts it.

Understanding linkers and loaders is crucial for debugging cryptic errors like 'undefined reference', 'segmentation fault', or 'library not loaded'. This article dives into the three major executable formats: ELF (Linux), PE (Windows), and Mach-O (macOS). We'll explore their structures, symbol resolution, relocation, and dynamic linking. You'll learn how to inspect binaries, diagnose linking issues, and apply best practices in production.

Real-world incidents often stem from subtle linking problems: mismatched calling conventions, symbol visibility, or library versioning. By the end, you'll be equipped to handle these challenges with confidence.

Executable Formats Overview

Executable formats define how code and data are organized in a binary file. The three major formats are ELF (Executable and Linkable Format) used on Linux and Unix-like systems, PE (Portable Executable) used on Windows, and Mach-O (Mach Object) used on macOS and iOS. Each format has its own header structures, section organization, and dynamic linking mechanisms.

ELF is the most flexible, supporting multiple architectures and advanced features like thread-local storage. PE is derived from COFF and includes a DOS stub for backward compatibility. Mach-O is designed for the Mach kernel and uses a two-level namespace for symbols.

Understanding these formats helps when debugging issues like 'file not recognized' or 'bad executable' errors. Tools like 'file' can identify the format: 'file myprogram' might output 'ELF 64-bit LSB executable, x86-64'.

check_format.shBASH
1
2
file myprogram
# Output: myprogram: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=..., not stripped
Output
myprogram: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=..., not stripped
🔥Format Identification
📊 Production Insight
When cross-compiling, ensure the target format matches the host system. A common mistake is compiling for x86_64 on an ARM machine without specifying the correct target.
🎯 Key Takeaway
ELF, PE, and Mach-O are the three major executable formats, each with unique structures and conventions.

ELF File Structure

An ELF file begins with an ELF header that contains magic number (\x7fELF), class (32/64-bit), endianness, OS/ABI, and entry point. Following the header are program headers (for loading) and section headers (for linking).

Key sections include
  • .text: executable code
  • .data: initialized data
  • .bss: uninitialized data
  • .rodata: read-only data
  • .symtab: symbol table
  • .strtab: string table
  • .rela.text: relocation entries for .text

Dynamic linking uses .dynamic, .dynsym, .dynstr, .got, and .plt sections. The Global Offset Table (GOT) and Procedure Linkage Table (PLT) enable position-independent code.

Example: Inspecting ELF sections with readelf.

elf_sections.cppCPP
1
2
3
4
5
6
7
8
9
#include <iostream>
int global_var = 42;
int uninit_var;
const int const_var = 100;
int main() {
    static int static_var = 0;
    std::cout << "Hello, ELF!" << std::endl;
    return 0;
}
Output
Hello, ELF!
💡Inspecting ELF
📊 Production Insight
Position-independent executables (PIE) are now default on many Linux distributions. They use ASLR and require careful handling of GOT entries.
🎯 Key Takeaway
ELF uses sections for linking and segments for loading. The GOT and PLT are crucial for dynamic linking.

PE File Structure

PE files start with a DOS header (MZ) and a DOS stub, followed by the PE signature and COFF header. The optional header contains image base, entry point, and data directories. Sections include .text, .data, .rdata, .idata (import), .edata (export), and .reloc (base relocations).

PE uses Import Address Table (IAT) and Export Address Table (EAT) for dynamic linking. The loader resolves imports by walking the IAT and patching addresses.

Example: Dumping PE headers with dumpbin.

pe_headers.batBASH
1
2
dumpbin /headers myprogram.exe
# Output includes DOS header, PE signature, COFF header, optional header, and section headers.
Output
Dump of file myprogram.exe
PE signature found
File Type: EXECUTABLE IMAGE
... (truncated)
⚠ PE and ASLR
📊 Production Insight
On Windows, DLL Hell occurs when multiple versions of a DLL are installed. Use side-by-side assemblies or manifests to avoid conflicts.
🎯 Key Takeaway
PE format uses IAT for imports and EAT for exports. The .reloc section is essential for ASLR.

Mach-O File Structure

Mach-O files have a header with magic (MH_MAGIC_64), followed by load commands that describe segments and sections. Segments include __TEXT (code), __DATA (data), __LINKEDIT (symbols, strings).

Mach-O uses two-level namespace for symbols: each symbol is associated with its defining library. This avoids symbol clashes but can cause 'symbol not found' errors if libraries are missing.

Example: Using otool to inspect Mach-O.

macho_otool.shBASH
1
2
3
4
otool -L myprogram
# Output: myprogram:
#   /usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 400.9.0)
#   /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1226.10.1)
Output
myprogram:
/usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 400.9.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1226.10.1)
🔥Mach-O Tools
📊 Production Insight
macOS uses dynamic libraries (.dylib) and frameworks. Ensure correct install names and rpaths to avoid library loading issues.
🎯 Key Takeaway
Mach-O uses two-level namespace and load commands. Segments group sections with similar permissions.

Symbol Resolution and Relocation

Symbol resolution is the process of matching symbol references to definitions across object files. Relocation adjusts addresses in code and data to reflect the final memory layout.

There are two types of relocation: static (at link time) and dynamic (at load time). Static relocation modifies the binary directly; dynamic relocation uses GOT/PLT.

Example: A simple C++ program with external symbol.

symbol_resolution.cppCPP
1
2
3
4
5
6
// file1.cpp
int foo();
int main() { return foo(); }

// file2.cpp
int foo() { return 42; }
💡Inspecting Relocations
📊 Production Insight
Weak symbols allow overriding. Use '__attribute__((weak))' in GCC to provide default implementations that can be overridden.
🎯 Key Takeaway
Symbol resolution matches references to definitions; relocation adjusts addresses. Dynamic linking defers resolution to load time.

Dynamic Linking and Loading

Dynamic linking allows executables to use shared libraries at runtime. The dynamic linker/loader (ld.so on Linux, dyld on macOS, Ldr on Windows) resolves symbols and loads libraries.

On Linux, the dynamic linker uses LD_LIBRARY_PATH, rpath, and ldconfig cache. On macOS, dyld uses DYLD_LIBRARY_PATH and install names. On Windows, the loader searches the application directory, system directories, and PATH.

Example: Setting rpath during linking.

rpath_example.shBASH
1
2
3
g++ -o myprogram main.cpp -L. -lmylib -Wl,-rpath,'$ORIGIN/lib'
# This sets rpath to a 'lib' subdirectory relative to the executable.
# $ORIGIN expands to the directory containing the executable.
⚠ Security and rpath
📊 Production Insight
On Linux, 'ldd' shows library dependencies. Missing libraries cause 'cannot open shared object file' errors. Use 'ldconfig -p' to check cache.
🎯 Key Takeaway
Dynamic linking defers symbol resolution to load time. Use rpath or LD_LIBRARY_PATH to control library search paths.

Best Practices and Debugging Tools

To avoid linking issues
  • Always link libraries in the correct order (dependent libraries first).
  • Use '--as-needed' to avoid unnecessary dependencies.
  • Use version scripts to control symbol visibility.
  • For C++, use 'extern "C"' to prevent name mangling when interfacing with C.
  • Use 'nm', 'objdump', 'readelf', 'dumpbin', 'otool' to inspect binaries.

Example: Using version script to hide internal symbols.

version_script.mapCPP
1
2
3
4
5
6
{
    global:
        public_function;
    local:
        *;
};
💡Debugging with strace
📊 Production Insight
In production, use static linking for critical binaries to avoid library version conflicts. However, dynamic linking saves memory and allows updates without recompilation.
🎯 Key Takeaway
Use version scripts and visibility attributes to control symbol exports. Debug with strace, ldd, and objdump.
● Production incidentPOST-MORTEMseverity: high

The Case of the Missing VTable: A C++ ABI Disaster

Symptom
After upgrading the compiler from GCC 7 to GCC 9, the application crashed with 'pure virtual function call' errors on startup.
Assumption
The developer assumed the crash was due to a bug in the new compiler's optimization.
Root cause
The project had a shared library compiled with GCC 7 and the main executable compiled with GCC 9. The C++ ABI changed between versions, causing the vtable layout to differ. The linker silently accepted the mismatch because both were compiled with the same soname.
Fix
Recompile all libraries with the same compiler version. Add a version script to enforce symbol visibility and prevent accidental ABI mismatches.
Key lesson
  • Always recompile all dependencies when upgrading the compiler.
  • Use version scripts or visibility attributes to control symbol exports.
  • Test with a clean build from source in CI/CD pipelines.
  • Be aware of C++ ABI compatibility across compiler versions.
  • Use tools like 'objdump -T' to inspect dynamic symbols.
Production debug guideSymptom to Action4 entries
Symptom · 01
Undefined reference at link time
Fix
Check symbol spelling, ensure all object files are linked, and verify library order.
Symptom · 02
Segmentation fault on startup
Fix
Check for ABI mismatches, use 'ldd' to verify library versions, and inspect relocation entries.
Symptom · 03
Library not loaded error
Fix
Set LD_LIBRARY_PATH or use rpath; check library search paths with 'ldconfig -p'.
Symptom · 04
Symbol defined multiple times
Fix
Use 'nm' to find duplicates, check for static and dynamic linking conflicts, and use '--whole-archive' carefully.
★ Quick Debug Cheat Sheet for LinkersCommon symptoms and immediate commands to diagnose linking issues.
Undefined reference
Immediate action
Check symbol name and library order
Commands
nm -u myprogram.o
objdump -T mylib.so | grep symbol
Fix now
Add missing library or correct symbol name.
Multiple definition+
Immediate action
Find duplicate symbols
Commands
nm -g *.o | grep symbol
readelf -s mylib.so | grep symbol
Fix now
Remove duplicate object or use '--allow-multiple-definition' with caution.
Library not found+
Immediate action
Check library search path
Commands
ldd myprogram
ldconfig -p | grep libname
Fix now
Set LD_LIBRARY_PATH or install missing library.
Segfault in dynamic linking+
Immediate action
Check for ABI mismatch
Commands
objdump -p mylib.so | grep SONAME
readelf -h mylib.so | grep Class
Fix now
Recompile all libraries with same compiler.
FeatureELFPEMach-O
Magic number\x7fELFMZ (DOS header)MH_MAGIC_64
Dynamic linkingGOT/PLTIAT/EATTwo-level namespace
Section naming.text, .data.text, .data__TEXT, __DATA
Relocation typeStatic and dynamicBase relocations (.reloc)Rebase and bind
Common OSLinux, UnixWindowsmacOS, iOS
⚙ Quick Reference
7 commands from this guide
FileCommand / CodePurpose
check_format.shfile myprogramExecutable Formats Overview
elf_sections.cppint global_var = 42;ELF File Structure
pe_headers.batdumpbin /headers myprogram.exePE File Structure
macho_otool.shotool -L myprogramMach-O File Structure
symbol_resolution.cppint foo();Symbol Resolution and Relocation
rpath_example.shg++ -o myprogram main.cpp -L. -lmylib -Wl,-rpath,'$ORIGIN/lib'Dynamic Linking and Loading
version_script.map{Best Practices and Debugging Tools

Key takeaways

1
Linkers resolve symbols and perform relocation; loaders map executables into memory.
2
ELF, PE, and Mach-O have distinct structures but share concepts like sections, segments, and dynamic linking tables.
3
Use tools like readelf, objdump, dumpbin, and otool to inspect binaries and debug linking issues.
4
Always recompile all dependencies when upgrading compilers to avoid ABI mismatches.

Common mistakes to avoid

3 patterns
×

Linking libraries in wrong order

×

Forgetting to export symbols in shared libraries

×

Mixing 32-bit and 64-bit object files

INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
Explain the difference between static and dynamic linking.
Q02SENIOR
How does lazy binding work in ELF?
Q03SENIOR
What is the role of the .reloc section in PE files?
Q01 of 03JUNIOR

Explain the difference between static and dynamic linking.

ANSWER
Static linking copies library code into the executable at link time, resulting in larger binaries but no runtime dependencies. Dynamic linking uses shared libraries loaded at runtime, reducing binary size and allowing updates.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is the difference between a linker and a loader?
02
How do I fix 'undefined reference' errors?
03
What is the GOT and PLT?
04
How do I inspect a binary's dependencies?
05
What is position-independent code (PIC)?
N
Naren Founder & Principal Engineer

20+ years shipping production systems from the metal up. Lessons pulled from things that broke in production.

Follow
Verified
production tested
July 13, 2026
last updated
2,165
articles · all by Naren
🔥

That's Compiler Design. Mark it forged?

3 min read · try the examples if you haven't

Previous
Compiler Optimization and Auto-Vectorization
13 / 13 · Compiler Design
Next
SDLC: Software Development Life Cycle Explained