Relink: Rust ELF Loader and Runtime/JIT Linker
Relink loads ELF images from files or memory and performs dynamic loading, dependency resolution, relocation, and symbol lookup. It fits plugin systems, JIT and hot-reload flows, isolated link contexts, scan-first layout optimization, kernels, and embedded runtimes when dlopen is too rigid.
Use Cases
- Load plugins, JIT artifacts, or hot-update modules at runtime with custom dependency resolution and symbol scopes.
- Scan dependencies and sections before mapping, then use
--emit-relocsto reorder layout, pack hot code, use huge pages, or run custom passes. - Keep ELF scanning, mapping, and relocation available in
no_std, kernel, embedded, or custom mmap environments. - Scan, rewrite, and load images with different ELF layouts, ABIs, or target architectures.
- Compose shared objects, executable images, and relocatable objects in one loading flow.
What It Loads
- Shared objects / dynamic libraries (
ET_DYN) - Executables and PIE-style images (
ET_EXEC, plus executable-styleET_DYN) - Relocatable object files (
ET_REL) when theobjectfeature is enabled - File-backed or in-memory inputs via
&str,String,&[u8],Vec<u8>,ElfFile, andElfBinary
Use Loader::load() when you want automatic ELF type detection. Use load_dylib(), load_exec(), or load_object() when you want strict type checks.
Core Capabilities
| Capability | What Relink provides |
|---|---|
| In-memory loading | Load ELF images from paths, memory buffers, or parsed inputs |
| Custom linking policy | Caller-controlled DT_NEEDED resolution, symbol lookup order, scopes, and relocation interception |
| Isolated link contexts | Multiple LinkContexts keep independent module stores, dependency graphs, and symbol scopes |
| Scan-first planning | Scan dependencies and sections first, then adjust layout, materialization, or section data before mapping |
| Dynamic link-time optimization | With --emit-relocs, reorder sections, pack hot code, and run custom passes |
| Replaceable mapping backend | Plug in platform-specific mmap, permission, page-size, or huge-page policies |
| Type-safe symbol access | Symbol handles are tied to the lifetime of their loaded image, reducing dangling-symbol risks |
| Hybrid linking | Compose .so, executable images, and feature-gated .o / ET_REL inputs |
Compared With dlopen
| Capability | Relink | dlopen-style loading |
|---|---|---|
| In-memory loading | ✅ Paths / memory buffers / parsed ELF | ❌ |
ET_REL loading |
✅ Feature-gated | ❌ |
| Pre-link planning | ✅ Dependencies / sections / mapping strategy | ❌ |
| Dynamic link-time optimization | ✅ Section reordering / hot-code packing / custom passes | ❌ |
| Mapping policy | ✅ Replaceable mmap backend, page size, and huge-page policy | ❌ |
| Dependency and symbol policy | ✅ Dependency graph / scope / lookup / interception control | ❌ |
| Context isolation | ✅ Multiple LinkContexts isolate dependency graphs and symbol scopes |
❌ |
| Heterogeneous loading | ✅ Different ELF layouts / ABIs / target architectures | ❌ |
Quick Start
The default feature set is suitable for loading dynamic libraries, executables, and handling TLS:
[]
= "0.15.0"
To enable the common advanced features in one bundle:
[]
= { = "0.15.1", = ["full"] }
Load a Dynamic Library and Call a Symbol
use ;
extern "C"
Loading Paths
| Path | Entry point | Best for |
|---|---|---|
| Direct loading | Loader::load_dylib() / load_exec() / load_object() |
You already know which image to load and only need scopes, TLS, lazy binding, or relocation hooks |
| Runtime dependency linking | Linker::load() |
Use KeyResolver and LinkContext to manage dependency graphs, scopes, and context isolation |
| Scan-first linking | Linker::load_scan_first() |
Discover DT_NEEDED dependencies first, then run layout passes, choose materialization policy, and relocate as one group |
| Relocatable objects | Loader::load_object() |
Compose .o and .so inputs at runtime; requires the object feature |
| Custom mapping environment | Loader::with_mmap() / with_page_size() |
Plug in custom mmap, permission, page-size, or huge-page policies |
Advanced Capability Index
| Topic | Entry point / example |
|---|---|
| Load from memory | ElfBinary::new(name, bytes), or direct load_dylib(&bytes) / load_exec(&bytes) |
| Host symbols and scopes | SyntheticModule, scope(), extend_scope() |
| Relocation interception | pre_handler(), post_handler(), see cargo run --example relocation_handler |
| Lazy binding | relocator().lazy(), requires the lazy-binding feature |
| Runtime dependency graphs | KeyResolver, LinkContext, Linker::load() |
| Pre-map layout optimization | Linker::load_scan_first(), map_pipeline(), see cargo run --example linker_scan_first |
| Relocatable objects | cargo run --example load_object --features object |
| Lifecycle callbacks | cargo run --example lifecycle |
Dynamic link-time layout optimization usually requires the target ELF to retain relocation information, for example by passing -Wl,--emit-relocs to the linker. Scan-first passes can inspect sections, modify data, adjust materialization, and place code, read-only data, writable data, or TLS into different arenas before mapping.
Benchmarks
The table below is a GitHub Actions snapshot, not a universal performance claim. It is meant as a reproducible reference point for the current benchmark suite; run cargo bench on your target environment for numbers that matter to your workload. Full environment details are in actions/runs/25632675040/job/75239090388, and the fixture is the repository's libc -> libb -> liba test chain, not the system C library.
Lower is better for loading. scan_first includes dependency scanning and section-region planning, so it is a planning-path cost rather than a direct dlopen replacement.
| Benchmark | Time | Relative time |
|---|---|---|
elf_loader/memory |
89.531 µs |
0.78x |
elf_loader/file |
101.01 µs |
0.88x |
linker/runtime |
111.32 µs |
0.97x |
libloading/lazy |
115.34 µs |
1.00x |
libloading/now |
115.77 µs |
1.00x |
linker/scan_first |
288.92 µs |
2.51x |
Symbol lookup was measured after both loaders had already loaded the fixture chain:
| Benchmark | Time | Relative time |
|---|---|---|
symbol/elf_loader/hit |
10.280 ns |
0.13x |
symbol/libloading/hit |
80.154 ns |
1.00x |
symbol/elf_loader/miss |
11.548 ns |
0.03x |
symbol/libloading/miss |
375.49 ns |
1.00x |
Feature Flags
| Feature | Default | Purpose |
|---|---|---|
libc |
Yes | Use the libc backend on Unix-like platforms |
tls |
Yes | Enable TLS relocation handling and the built-in TLS resolver |
lazy-binding |
No | Enable PLT/GOT lazy binding and lazy-fixup lookup configuration |
object |
No | Enable relocatable object (ET_REL) loading and Loader::load_object() |
version |
No | Enable version-aware symbol lookup such as get_version() |
log |
No | Enable log integration for loader and relocation diagnostics |
portable-atomic |
No | Support targets without native pointer-sized atomics |
use-syscall |
No | Use the Linux syscall backend instead of libc |
full |
No | Convenience bundle: tls, lazy-binding, object, libc |
Notes:
- The default features are
tls+libc. - Compiling with
tlsis not enough by itself for TLS-using modules. Start fromLoader::new().with_default_tls_resolver()or provide your own TLS resolver when loading ELF objects that require TLS relocations. load_object()is feature-gated.cargo run --example load_objectwill fail under the default feature set unless you add--features object.
Examples
The examples/ directory covers the main extension points:
| Example | What it demonstrates | Command |
|---|---|---|
load_dylib |
Load shared objects and resolve host symbols | cargo run --example load_dylib |
linker_load |
Resolve DT_NEEDED dependencies with Linker::load() |
cargo run --example linker_load |
from_memory |
Load ELF data from a byte buffer | cargo run --example from_memory |
load_exec |
Inspect executable entry and base addresses | cargo run --example load_exec |
load_hook |
Observe segment loading with with_hook() |
cargo run --example load_hook |
linker_scan_first |
Discover DT_NEEDED, run scan-first passes, and configure pre-map layout |
cargo run --example linker_scan_first |
lifecycle |
Custom .init / .fini handling |
cargo run --example lifecycle |
user_data |
Initialize dynamic-image metadata | cargo run --example user_data |
relocation_handler |
Intercept relocations with a custom handler | cargo run --example relocation_handler |
load_object |
Load relocatable object files | cargo run --example load_object --features object |
Platform Notes
| Architecture | Dynamic libraries / executables | Dynamic link-time optimization | .o / ET_REL |
|---|---|---|---|
x86_64 |
✅ Primary validation path | ✅ Layout passes / placement / hot code / huge-page arenas | ✅ object feature |
x86 / aarch64 / arm / riscv64 / riscv32 / loongarch64 |
✅ | 🟡 Basic dependency planning; complex reordering pending | ⏳ Pending |
Legend: ✅ supported, 🟡 basic support, ⏳ pending. Complex section-reorder repair and .o / ET_REL support are currently centered on x86_64 relocation handling; contributions for the other architectures are welcome.
Symbol lookup is name-based and does not perform Rust name mangling for you. Export C ABI symbols when you want stable runtime lookup names.
Contributing
Issues and pull requests are welcome, especially around platform support, examples, and diagnostics. Star the project if it is useful in your work.
License
This project is dual-licensed under either of the following: