Linkers

Technology — James William Steven Parker

A linker is a tool that combines separately generated object files and libraries into a larger executable or another linked binary while resolving references between independently compiled or assembled components. A linker determines where sections of each input object should be placed, matches symbol references with symbol definitions, applies relocation adjustments and constructs the output according to the target platform's binary format.

Linking is essential because source files are normally compiled independently, allowing programmers to divide programs into modules and reuse precompiled libraries. A linker may produce an executable, shared library, dynamically linked library or another form of binary. The exact responsibilities vary across platforms and toolchains, but symbol resolution and relocation are fundamental linking operations.

Object files contain sections holding code and data, together with symbol tables and relocation information. A symbol can represent a function, variable, section or other named entity, and it can be local to an object file or externally visible to other objects. When one object file calls a function defined in another, the compiler or assembler may emit a relocation entry rather than knowing the function's final address.

The linker collects symbol definitions and references from all relevant input files, determines which definition satisfies each reference and computes the necessary address-dependent values. Errors such as undefined references or multiple conflicting definitions can arise during this process. The linker therefore acts as a central mechanism connecting separately produced compilation units.

Static linking incorporates selected library code into the resulting binary, producing an executable that contains the linked implementations required at build time. This can simplify deployment because fewer external shared-library dependencies may be required, although it can increase binary size and duplicate library code across multiple applications. Dynamic linking instead leaves references to shared libraries that are resolved when the program loads or, in some systems, later during execution.

Dynamic libraries allow multiple programs to share common code in memory and can permit libraries to be updated independently, subject to compatibility constraints. Dynamic linking introduces additional runtime dependencies and can lead to loader errors when required libraries or compatible symbols are unavailable. The distinction between static and dynamic linking is therefore also a deployment and compatibility consideration.

Relocation is necessary because the final addresses of code and data are often unknown when an individual object file is compiled. The linker assigns output addresses to sections and adjusts locations identified by relocation records. Some architectures use absolute addresses, while others provide position-independent addressing techniques. Position-independent code is important for shared libraries and address-space layout randomisation because code can execute correctly regardless of the address at which it is loaded.

Linkers may also generate procedure-linkage and global-offset mechanisms that cooperate with runtime loaders to resolve external functions or data. The precise relocation mechanisms are architecture- and binary-format-dependent, but the general problem is to adapt separately produced machine code to its final memory layout without recompiling every source module.

Linkers can perform more than simple concatenation. They may remove unused sections through dead-code elimination, perform link-time optimisation, merge compatible sections, reorder functions or data, construct import and export tables, and generate metadata required by the operating system. Link-time optimisation allows the compiler to retain intermediate information and perform certain optimisations across source-file boundaries that ordinary compilation cannot see. This can enable more effective inlining and whole-program analysis but can increase build time and memory consumption. Linkers can also create map files and diagnostic information that help developers understand symbol placement and binary composition. Their role has consequently expanded in sophisticated toolchains beyond the traditional view of simply joining object files together.

Executable formats and operating systems determine how linked binaries are represented and loaded. ELF is widely used on Unix-like systems, PE is associated with Windows executable and library formats, and Mach-O is used by Apple operating systems. These formats can represent sections, segments, symbols, relocations, dynamic dependencies, entry points and debugging information. The linker constructs a binary that a program loader can understand. The loader may map segments into virtual memory, resolve dynamic dependencies, apply remaining relocations, establish process state and transfer control to the program's entry point. Thus linking and loading are distinct stages, although their boundaries can become blurred by platform-specific mechanisms and runtime dynamic linking.

Linkers are closely connected to compilers, assemblers and runtime systems. A compiler usually generates object code or an intermediate representation that eventually becomes object code, while an assembler translates assembly source into object code. The linker combines those object files and libraries, and the runtime environment provides facilities used after execution begins. Runtime libraries can themselves be linked statically or dynamically. Startup code inserted or selected during linking can establish the process environment before application-level main or equivalent entry code runs. Foreign-function interfaces also depend heavily on linking conventions because code written in different languages must agree on symbol names, calling conventions, data representations and binary interfaces.

Common linker problems include undefined symbols, duplicate definitions, incompatible architectures, missing libraries, incompatible application binary interfaces and incorrect library search paths. Library ordering can matter for some static-linking mechanisms because archives are often processed according to specific extraction rules. Symbol visibility controls which definitions are available across module boundaries and can reduce unintended dependencies. Name mangling allows languages such as C++ to encode function signatures into symbol names, while external-linkage conventions can provide stable interfaces for interoperability. Linkers therefore form an important boundary between source-level program structure and binary-level composition. Their behaviour directly affects whether separately developed components can be assembled into a functioning executable or library.