linker-lang 0.2.0

Static linking: symbol resolution, section merging, and relocation patching.
Documentation

Overview

A compiler's back end emits each translation unit on its own: a few named byte sections, some symbols that mark addresses inside them, and relocations that still need an address filled in. Linking is the step that joins those islands into something you can load.

linker-lang does exactly that, and nothing more. Hand it a list of Objects and it lays their sections out end to end, resolves every symbol to its final address in that layout, and patches each relocation with the address it was waiting for. The result is an Image whose bytes are ready to load and whose symbol table says where everything ended up.

The model is format-agnostic. Nothing in it names ELF or PE: an Object is what a reader for either format would populate, and an Image is what a writer would serialize. The crate carries the linking logic without committing to a container format, so a reader and a writer slot in on either side of it later.

Installation

[dependencies]
linker-lang = "0.2"

Or from the terminal:

cargo add linker-lang

Quick Start

use linker_lang::{link, Object, Width};

// The code: a symbol `start` at the top of a `.text` section.
let mut code = Object::new("code");
code.section(".text", [0x90, 0x90, 0x90, 0x90]);
code.define("start", ".text", 0);

// The data: an 8-byte slot that should hold the address of `start`.
let mut data = Object::new("data");
data.section(".data", [0u8; 8]);
data.relocate(".data", 0, "start", Width::U64, 0);

let image = link(&[code, data]).expect("symbols resolve");

// `.text` is laid out first at address 0, so `start` resolves there, and the slot in
// `.data` was patched with that address.
assert_eq!(image.symbol("start"), Some(0));
let slot = image.section(".data").unwrap().data();
assert_eq!(u64::from_le_bytes(slot.try_into().unwrap()), 0);

// The Display impl is a readable link map:
//   entry = none
//   .text @ 0x0000000000000000 (4 bytes)
//   .data @ 0x0000000000000004 (8 bytes)
//   symbols:
//       0x0000000000000000 start
println!("{image}");

Linking model

A link takes a list of objects and produces one image. Three passes do the work:

Pass What it does
Merge sections Sections that share a name across objects are concatenated, in object order, into one output section. The output sections are placed at addresses, end to end from the base address.
Resolve symbols Each symbol resolves to an absolute address — its section's address, plus where its object landed in that section, plus the offset. A name defined twice is a DuplicateSymbol.
Patch relocations Each relocation is a hole that must hold the address of a symbol, plus an addend. The linker resolves the target and writes the address into the section bytes, little-endian, in the requested Width.

Sections are addressed by name, so building an object reads the way the layout does:

use linker_lang::{Linker, Object, Width};

// Two compiled functions go into `.text`; a dispatch table in `.data` points at them.
let mut code = Object::new("code");
code.section(".text", vec![0u8; 16]);
code.define("on_start", ".text", 0);
code.define("on_tick", ".text", 8);

let mut table = Object::new("table");
table.section(".data", vec![0u8; 16]);
table.relocate(".data", 0, "on_start", Width::U64, 0);
table.relocate(".data", 8, "on_tick", Width::U64, 0);

let image = Linker::new().base_address(0x40_0000).entry("on_start").link(&[code, table]).unwrap();

assert_eq!(image.entry(), image.symbol("on_start"));
let slots = image.section(".data").unwrap().data();
assert_eq!(u64::from_le_bytes(slots[0..8].try_into().unwrap()), image.symbol("on_start").unwrap());
assert_eq!(u64::from_le_bytes(slots[8..16].try_into().unwrap()), image.symbol("on_tick").unwrap());

API Overview

For a complete reference with examples, see docs/API.md.

  • link — the one-call shortcut: link with the default configuration.
  • Linker — the configurable form: a base address and an optional entry point.
  • Object — a compilation unit: named byte sections, symbols, and relocations.
  • Width — the size of a relocated address (U32 / U64).
  • Image — the linked result: laid-out sections, the symbol table, the entry point, and a link-map Display.
  • OutputSection — one laid-out section: name, address, and final bytes.
  • LinkError — the reason a link could not be completed.

Error handling

Every way a link can fail is a distinct LinkError variant that names the object, symbol, or section involved — a symbol defined twice, a relocation or entry point with no definition, a slot that runs past its section or an address that does not fit it. The linker reports the reason rather than producing a partial or wrong image.

use linker_lang::{link, LinkError, Object, Width};

let mut obj = Object::new("uses.o");
obj.section(".data", [0u8; 8]);
obj.relocate(".data", 0, "external", Width::U64, 0); // never defined

match link(&[obj]) {
    Err(LinkError::UndefinedSymbol { name, object }) => {
        assert_eq!((name.as_str(), object.as_str()), ("external", "uses.o"));
    }
    other => panic!("expected UndefinedSymbol, got {other:?}"),
}

Performance

Performance is a hard constraint, not an afterthought (see REPS.md). A link is a few linear passes: sections are concatenated into preallocated buffers, symbol lookups go through an ordered map, and each relocation is one resolve and one little-endian write. The cost is linear in the total input size, with no copies beyond the section bytes themselves.

Run the benchmarks on your hardware for trends:

cargo bench --bench bench

The suite covers the two cost drivers — many small objects wired together by a relocation table (symbol resolution and patching dominate), and a few large sections (the byte merge dominates).

Configuration

Feature Flags

Feature Default Description
std on Links the standard library. Without it the crate is #![no_std] and needs only alloc.
serde off Derives Serialize / Deserialize for Image, OutputSection, Width, and LinkError, so a linked image or a failure can be cached, logged, or moved between tools.
# no_std build:
linker-lang = { version = "0.2", default-features = false }

# with serialization:
linker-lang = { version = "0.2", features = ["serde"] }

Examples

Two runnable examples live in examples/:

# Link hand-built objects with a cross-object relocation and print the link map.
cargo run --example link_map

# Compile functions with `codegen-lang`, then link them into one image.
cargo run --example from_codegen

Testing

# Unit, integration (workflow, invalid input, properties), and doc tests
cargo test --all-features

# Property tests only
cargo test --all-features --test properties

# Lints and formatting
cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings

# Benchmarks (Criterion)
cargo bench --bench bench

The integration suite in tests/workflow.rs compiles functions with codegen-lang, wraps each as a linkable object, and links them — exercising symbol resolution, section merging, and relocation patching on real backend output. tests/invalid.rs confirms every failure is reported as the precise error, and tests/properties.rs generates random links and checks symbol addresses and patched slots against an independent computation.

Cross-Platform Support

The crate is pure, dependency-light Rust with no platform-specific code, and is tested on Linux, macOS, and Windows (x86_64) through the CI matrix on stable and the 1.85 MSRV.

Contributing

Engineering standards for this crate are the Rust Efficiency & Performance Standards; the current scope and plan are in dev/ROADMAP.md. Before a PR: cargo fmt --all, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features must be clean.