Back to Technology

x86 Assembly Series Part 15: Linking & Object Files

February 6, 2026 Wasil Zafar 37 min read

Master the ELF format, understand sections (.text, .data, .bss), symbol tables, relocation entries, and the complete linking process from object files to executables and shared libraries.

Table of Contents

  1. ELF Format Overview
  2. Sections
  3. Symbol Tables
  4. Relocation
  5. Static Linking
  6. Dynamic Linking
  7. Creating Shared Libraries

ELF Format Overview

ELF: Executable and Linkable Format is the standard binary format on Linux/Unix. It describes object files (.o), executables, and shared libraries (.so).

x86 Assembly Mastery

Your 25-step learning path • Currently on Step 16
Development Environment, Tooling & Workflow
IDEs, debuggers, build tools, workflow setup
Assembly Language Fundamentals & Toolchain Setup
Syntax basics, assemblers, linkers, object files
x86 CPU Architecture Overview
Instruction pipeline, execution units, microarchitecture
Registers – Complete Deep Dive
GPRs, segment, control, flags, MSRs
Instruction Encoding & Binary Layout
Opcode bytes, ModR/M, SIB, prefixes, encoding schemes
NASM Syntax, Directives & Macros
Sections, labels, EQU, %macro, conditional assembly
Complete Assembler Comparison
NASM vs MASM vs GAS vs FASM, syntax differences
Memory Addressing Modes
Direct, indirect, indexed, base+displacement, RIP-relative
Stack Internals & Calling Conventions
Push/pop, stack frames, cdecl, System V ABI, fastcall
Control Flow & Procedures
Jumps, loops, conditionals, CALL/RET, function design
Integer, Bitwise & Arithmetic Operations
ADD, SUB, MUL, DIV, AND, OR, XOR, shifts, rotates
Floating Point & SIMD Foundations
x87 FPU, IEEE 754, SSE scalar, precision control
SIMD, Vectorization & Performance
SSE, AVX, AVX-512, data-parallel processing
System Calls, Interrupts & Privilege Transitions
INT, SYSCALL, IDT, ring transitions, exception handling
Debugging & Reverse Engineering
GDB, breakpoints, disassembly, binary analysis, IDA
16
Linking, Relocation & Loader Behavior
ELF/PE formats, symbol resolution, dynamic linking, GOT/PLT
You Are Here
17
x86-64 Long Mode & Advanced Features
64-bit extensions, RIP addressing, canonical addresses
18
Assembly + C/C++ Interoperability
Inline assembly, calling C from ASM, ABI compliance
19
Memory Protection & Security Concepts
DEP, ASLR, stack canaries, ROP, mitigations
20
Bootloaders & Bare-Metal Programming
BIOS/UEFI, MBR, real mode, protected mode transition
21
Kernel-Level Assembly
Context switching, interrupt handlers, TSS, GDT/LDT
22
Complete Emulator & Simulator Guide
QEMU, Bochs, instruction-level simulation, debugging VMs
23
Advanced Optimization & CPU Internals
Pipeline hazards, branch prediction, cache optimization, ILP
24
Real-World Assembly Projects
Shellcode, drivers, cryptography, signal processing
25
Assembly Mastery Capstone
Final project, comprehensive review, advanced techniques
# View ELF header
readelf -h program

# View sections
readelf -S program

# View program headers
readelf -l program

# View symbols
readelf -s program
ELF file format structure showing header, section headers, and program segments
Overview of the ELF binary format: ELF header, program headers, section headers, and their relationships

ELF Sections

ELF section layout showing .text, .data, .bss, and .rodata memory regions
Memory layout of ELF sections: .text (code), .data (initialized), .bss (uninitialized), and .rodata (constants)

.text Section (Code)

section .text
    global _start
_start:
    ; Executable code goes here
    ; Read-only at runtime

.data & .rodata Sections

section .data           ; Initialized read-write data
    counter dq 0
    message db "Hello", 0

section .rodata         ; Initialized read-only data
    const_pi dq 3.14159
    fmt db "Result: %d", 10, 0

.bss Section

section .bss            ; Uninitialized data (zero-filled)
    buffer resb 1024    ; Reserve 1024 bytes
    array resq 100      ; Reserve 100 quadwords

How BSS Gets Zeroed: p_filesz vs p_memsz

A common question is: if .bss variables are "zero-filled", who actually writes those zeros? The answer lies in the ELF program headers — the structures the OS loader reads to map an executable into memory.

Each loadable segment has a PT_LOAD program header containing two critical size fields:

ELFProgram Headers
p_filesz vs p_memsz in PT_LOAD Segments
FieldMeaningExample
p_offsetOffset into the ELF file where segment data begins0x1000
p_vaddrVirtual address where the segment is loaded0x601000
p_fileszNumber of bytes in the file (on disk)0x120 (288 bytes)
p_memszNumber of bytes in memory (at runtime)0x920 (2336 bytes)
p_flagsPermissions (R / W / X)RW (read-write)

The key insight: when p_memsz > p_filesz, the loader reads p_filesz bytes from the file (the initialised .data variables) and then zero-fills the remaining p_memsz − p_filesz bytes. That zero-filled region is exactly where .bss lives. Because .bss contains no data in the file, it adds zero bytes to the ELF on disk — saving space while still guaranteeing zeroed memory at runtime.

ELF File on Disk              Memory at Runtime
┌───────────────────┐         ┌───────────────────┐
│   ELF Header      │         │                   │
├───────────────────┤         │   .text (code)    │
│   .text (code)    │────────▶│                   │
├───────────────────┤         ├───────────────────┤
│   .data (init'd)  │────────▶│   .data (init'd)  │  ← p_filesz bytes
├───────────────────┤         ├───────────────────┤
│                   │         │   .bss (zeroed)   │  ← loader writes zeros
│   (not in file!)  │         │   p_memsz-p_filesz│
│                   │         ├───────────────────┤
└───────────────────┘         └───────────────────┘

You can inspect these fields with readelf -l:

# Examine program headers — look at the DATA segment
$ readelf -l program

#   Type   Offset   VirtAddr         FileSiz  MemSiz   Flg
#   LOAD   0x001000 0x0000000000601000 0x000120 0x000920 RW
#                                      ^^^^^^^^ ^^^^^^^^
#                                      p_filesz p_memsz
#   Difference (0x800 = 2048 bytes) is .bss, zero-filled by the loader
Why it matters: This design means a program with a 1 MB .bss array adds exactly zero bytes to the executable file. The linker simply sets p_memsz = p_filesz + bss_size, and the OS kernel's load_elf_binary() function (or the dynamic linker ld.so) handles zero-filling when it maps the segment via mmap + anonymous pages.

Symbol Tables

# View symbol table
nm program
objdump -t program

# Symbol types:
# T/t = text (code)
# D/d = initialized data
# B/b = BSS (uninitialized)
# U   = undefined (external reference)

Relocation

Relocation is how the linker patches addresses in object files to create a runnable executable:

Relocation process showing how the linker patches addresses in object files
The linker relocation process: resolving symbol references and patching addresses across object files
# View relocation entries
readelf -r program.o
objdump -r program.o

# Common relocation types (x86-64):
# R_X86_64_64      - Absolute 64-bit address
# R_X86_64_PC32    - PC-relative 32-bit (RIP-relative)
# R_X86_64_PLT32   - PLT entry for function call
# R_X86_64_GOTPCREL - GOT entry, PC-relative

Position-Independent Code (PIC)

; Non-PIC (absolute addressing)
mov rax, [my_var]           ; Needs relocation at runtime

; PIC (RIP-relative) - preferred for shared libraries
mov rax, [rel my_var]       ; Uses RIP-relative addressing
lea rdi, [rel my_var]       ; Get address of my_var (PIC)

; For external/global data in shared libraries, use GOT:
mov rax, [rel external_var wrt ..got]  ; Load via GOT
Why PIC? Position-independent code can load at any address, enabling shared libraries, ASLR (Address Space Layout Randomization), and memory sharing between processes.

Static Linking

Static linking combines all object files into one standalone executable:

Static linking workflow combining object files and libraries into a single executable
Static linking: merging object files and static libraries into one self-contained executable
# Create static library from object files
ar rcs libmylib.a file1.o file2.o file3.o

# Link with static library
ld -o program main.o -L. -lmylib
# Or with gcc:
gcc -static -o program main.o -L. -lmylib

# View archive contents
ar -t libmylib.a

# Extract member
ar -x libmylib.a file1.o

Static Link Process

1. Collect all object files and libraries
2. Resolve symbol references
3. Merge sections (.text, .data, .bss)
4. Apply relocations (patch addresses)
5. Write executable with all code embedded

Pros: No runtime dependencies, faster startup
Cons: Larger executables, no library updates without recompile

Dynamic Linking

Dynamic linking defers symbol resolution until runtime:

Dynamic linking with GOT and PLT showing lazy symbol resolution at runtime
Dynamic linking: GOT and PLT mechanism for lazy symbol resolution at runtime
GOT (Global Offset Table):
- Table of pointers to global data/functions
- Filled in by dynamic linker at load time
- Code references GOT entries, not absolute addresses

PLT (Procedure Linkage Table):
- Stub code for calling external functions
- First call: resolve symbol, patch GOT, call function
- Subsequent calls: jump directly via GOT (fast path)

call printf@PLT      ; First call flow:
  └─→ PLT stub: jmp [GOT entry]
        └─→ GOT initially points back to PLT
              └─→ PLT: push index; jmp dynamic_linker
                    └─→ Linker resolves, patches GOT
                          └─→ Function runs
; Calling external function (dynamic)
section .text
    extern printf
global _start
_start:
    lea rdi, [rel fmt]
    xor eax, eax              ; No float args
    call printf wrt ..plt     ; Call via PLT
    
; Or let NASM figure it out:
    call printf               ; NASM generates PLT reference
# View dynamic symbols
readelf -d program        # Dynamic section
readelf --dyn-syms prog   # Dynamic symbol table
objdump -R program        # Dynamic relocations
ldd program               # List shared library dependencies

Creating Shared Libraries

# Assemble as PIC (position-independent)
nasm -f elf64 -o mylib.o mylib.asm

# Create shared library (.so)
ld -shared -o libmylib.so mylib.o
# Or with gcc (adds libc):
gcc -shared -o libmylib.so mylib.o

# Link against shared library
ld -o program main.o -L. -lmylib -dynamic-linker /lib64/ld-linux-x86-64.so.2

# Run with library path
LD_LIBRARY_PATH=. ./program

# Install system-wide
sudo cp libmylib.so /usr/local/lib/
sudo ldconfig    # Update library cache

Exported Symbols

; mylib.asm - Shared library source
global my_function:function    ; Export as function
global my_variable:data        ; Export as data

section .data
my_variable: dd 42

section .text
my_function:
    mov eax, [rel my_variable]
    ret

Exercise: Create and Use a Shared Library

  1. Write mathlib.asm with add_numbers and multiply_numbers functions
  2. Create libmath.so shared library
  3. Write a main program that calls both functions
  4. Link and run with LD_LIBRARY_PATH