Assemblers, Linkers, Object Files, and Relocations

Assembly source is only the start of a program artifact. An assembler turns selected ISA syntax into a relocatable object; a linker combines objects and libraries, resolves eligible symbol references, lays out sections, and produces an executable or shared library; a loader maps that result for a process. This pipeline explains why a call may not yet contain a final address and why assembly source is not portable merely because its algorithm is simple.

Source becomes an object

An assembler parses mnemonics, directives, labels, numeric constants, and expressions for a particular target. It encodes instructions whose operands are known and records unresolved or layout-dependent references as relocations. A compiler often emits assembly or directly emits an object; either route must obey the target ISA, object format, and ABI. Preprocessing and compilation can also introduce generated symbols, debug data, and optimization choices before an assembler sees anything.

Relocatable objects are not normally runnable. They contain machine code, initialized data, section descriptions, a symbol table, relocation entries, and perhaps debug information. Their addresses are provisional. A local label may be resolved within one section, while a reference to a function defined in another object remains for the linker. An object file is therefore structured data, not simply “a binary executable.”

FormatCommon environmentsImportant caution
ELFLinux and many Unix-like systemsArchitecture and OS ABI still vary
COFF/PEWindows object files/executablesPE builds on COFF conventions
Mach-OApple platformsDifferent load commands and tooling

Format is not an ISA. ELF can carry x86-64, AArch64, RISC-V, and other machine code; an x86-64 ELF object cannot execute as ARM64 code. Likewise, syntax, endianness, ABI, and relocation rules must all match. “Binary” is too broad to say what a tool accepts.

Sections and symbols

Sections group bytes and metadata with similar purpose. Typical names include .text for executable code, .rodata for read-only initialized data, .data for writable initialized data, and .bss for zero-initialized storage represented without bytes in the file. Names are conventions, not a complete security policy. The linker may merge, split, discard, align, or rename sections according to scripts and platform rules. At load time, segments—not necessarily the same units as sections—carry permissions and mapping information.

A symbol gives a name, binding, type-like classification, section association, and value or address-related quantity. Local symbols support internal bookkeeping; global or external symbols can satisfy references across objects subject to language and linker rules. A symbol name is not necessarily a source identifier: C++ name mangling, compiler-generated temporaries, stripped symbols, and aliases all complicate inspection. A symbol table is excellent evidence, but read it with the source language and build options in view.

ItemPurposeExample question
SectionGroups content for layoutWhere is initialized read-only data?
SymbolNames a location or definitionIs helper defined here?
RelocationDescribes a value to fix upWhich instruction refers to helper?
SegmentDescribes loadable mappingWhich pages are executable?

Relocations make separate compilation possible

Consider an object containing a call to external helper. The assembler can encode a call-shaped instruction but does not yet know where the final linked helper will reside. It records a relocation naming the symbol and relocation type. The linker selects a definition, computes the required absolute, PC-relative, or other value, and patches the appropriate field or arranges a runtime mechanism. The relocation type defines arithmetic, field width, overflow limits, and sometimes instruction-specific constraints; it is not merely “replace these bytes with an address.”

; Architecture-neutral source intent
extern helper
entry:
    CALL helper        ; object records a relocation if helper is elsewhere
    RETURN

For x86-64, calls and data references commonly use RIP-relative encodings. For AArch64, a branch-immediate has a target-specific range and external data addresses are frequently materialized through page-relative sequences. Both may need paired or specialized relocations. The source intent is similar; the instruction bytes and relocation records are not. Never copy an x86 relocation recipe into ARM64 assembly or assume object inspection commands report all formats identically.

Static linking, dynamic linking, and loading

Static linking incorporates needed library object code into a final executable, subject to licensing and linker selection rules. It can simplify deployment but increases duplication and does not make a program independent of its operating system or kernel interface. Dynamic linking records dependencies on shared libraries. At program startup or on demand, a dynamic loader locates compatible libraries, maps them, resolves symbols under platform rules, and may apply dynamic relocations. Shared-library interfaces require ABI compatibility, not just matching function names.

Lazy binding may defer resolving an imported function until its first call; eager binding resolves earlier. Procedure linkage tables and global offset tables are common ELF terms, while Windows and Mach-O use their own mechanisms and vocabulary. Do not call every trampoline a PLT entry. Loader search paths and environment variables have security implications, so use documented deployment settings and avoid running untrusted executables merely to observe loader behavior.

Position independence and PIC

Position-independent code (PIC) avoids baking in assumptions that its code and data will appear at one fixed virtual address. This enables shared libraries and works with address-space layout randomization. PC-relative addressing is a common building block: a code location can refer to a nearby target by a displacement even after both move together. External or interposable data and functions may require indirection through tables or relocations. PIC is a property of generated code and relocation model, not a claim that all addresses vanish.

On x86-64, RIP-relative addressing is prominent. On ARM64, compilers often use page-relative address construction followed by an offset or load. These patterns are architecture-specific and may change with code model, visibility, linker relaxation, and optimization. Text relocations can be undesirable because writable patching of code pages conflicts with sharing and protection goals. Let compiler and linker options chosen for the target control PIC unless you are deliberately following that platform’s ABI documentation.

Link-time choices and terminology cautions

The linker resolves one definition among candidates according to format and command-line rules, extracts members from static archives when needed, assigns addresses, emits tables, and can perform garbage collection or link-time optimization. Link order and symbol visibility can change results. A successful link proves only that the selected toolchain found a compatible-enough set of definitions; it does not prove semantic correctness or safety.

Distinguish an archive (often a collection of object files) from a shared library, an object file from an executable, and a loader from a linker. “Library” alone is ambiguous. Also distinguish relocation records in a relocatable object from dynamic relocations processed at load time. Tool output may use virtual addresses, file offsets, or section-relative offsets; read the column heading before comparing numbers.

Safe inspection exercises

  1. Build two tiny source files you own, one defining a function and one calling it. Compile with -c; inspect symbols and relocations with readelf -s -r or llvm-readobj --symbols --relocations. Do not execute anything.
  2. Link the objects, then compare the caller’s disassembly before and after linking with objdump -dr or llvm-objdump -dr. Identify the formerly unresolved reference.
  3. Use a platform viewer appropriate to a file you built—readelf for ELF, dumpbin for PE/COFF, or otool for Mach-O—to list sections or load information. Record format-specific names rather than forcing one vocabulary onto another.

Return to Assembly Language, Machine Code, and the CPU when reviewing the ISA boundary, or continue to the existing x86 Assembly and Calling Conventions lesson for a target-specific ABI discussion.

A repeatable inspection workflow

Start with a source file you created and compile it without linking, commonly using -c. Identify its target and format before choosing a viewer. List sections, symbols, and relocations, then disassemble with relocations displayed. The relocation table connects a symbolic reference to a byte field far more reliably than guessing from a zero displacement. After linking, inspect the final artifact again and distinguish a static resolution from a dynamic import. Preserve the exact compiler, target, and flags in notes because changing any one can change output.

Debug information is separate metadata that links machine addresses back to source locations, types, and variables. It is valuable for authorized debugging but is not required for an executable to run, and release builds may strip it. Conversely, an executable may retain exported dynamic symbols while omitting most local names. A missing source name in a listing does not establish that the behavior was absent; optimizations and stripping can remove the convenient label while preserving the machine instructions.

Linker diagnostics are often meaningful design feedback. An undefined symbol can mean a missing object, an incorrect library order, a spelling or mangling mismatch, a visibility restriction, or an architecture mismatch. A duplicate definition can indicate two competing implementations. Do not “fix” such failures by copying arbitrary libraries or suppressing warnings. Check the target triple, ABI, library provenance, and documented build command. Cross-linking x86-64 objects with ARM64 objects is not a relocation problem a normal linker can solve.

Loading is also not execution. File-format inspection parses metadata; running a program invokes its code with ambient privileges and inputs. For learning, object and executable readers provide ample evidence about sections, symbols, dependencies, and relocations without treating unknown files as safe. Use sandboxing and organizational procedures if analysis must progress beyond static inspection.

References