Skip to content

weizhiao/Relink

Relink: Runtime ELF Linking and Optimization for Rust

Relink logo

Crates.io Crates.io downloads Docs.rs Minimum supported Rust version Build status MIT/Apache-2.0 license

English | 简体中文

Contributing | 贡献指南

Plan, optimize, map, relocate, and link ELF images at runtime.
From basic loading to caller-controlled dependency graphs and section-level layout rewrites.

ET_DYN · ET_EXEC · ET_REL · no_std · Typed symbols · Scan-first linking · Section layout passes

Relink is a high-performance, no_std-friendly ELF loader and runtime linker for Rust. It is built for plugin systems, JITs, runtimes, kernels, embedded loaders, hot-reload workflows, and other environments where dlopen-style loading is too rigid and hand-rolled relocation logic is too painful to maintain.

Beyond mapping a single ELF file, Relink can discover DT_NEEDED graphs before runtime materialization, run caller-provided layout passes, relocate modules with policy-driven scopes, and materialize reorderable sections into optimized runtime memory regions.

What It Loads

  • Shared objects / dynamic libraries (ET_DYN)
  • Executables and PIE-style images (ET_EXEC, plus executable-style ET_DYN)
  • Relocatable object files (ET_REL) when the object feature is enabled
  • File-backed or in-memory inputs via &str, String, &[u8], &Vec<u8], ElfFile, and ElfBinary

If you want automatic detection, use Loader::load(). If you want strict type checks, use load_dylib(), load_exec(), or load_object().

Why Relink

If you need... Relink gives you...
Runtime loading from files or memory Loader::load* accepts paths, ElfFile, ElfBinary, &[u8], and &Vec<u8]
Safer symbol handling Typed get::<T>() lookups tied to the loaded image lifetime
Runtime link-time optimization Linker::load_scan_first() discovers first, then runs LinkPipeline passes before mapping
Section-level layout control Reorderable modules can be materialized as section regions instead of whole DSO spans
Explicit dependency policy KeyResolver resolves roots and DT_NEEDED edges into canonical runtime keys
Host-controlled linking pre_find_fn(), post_find_fn(), lazy_pre_find_fn(), lazy_post_find_fn(), pre_handler(), and post_handler()
Hybrid linking at runtime Mix .so and .o inputs with scope() and add_scope()
Low-level deployment targets A no_std core plus custom Mmap backends

Compared With Typical Approaches

Capability Relink dlopen-style loading Hand-rolled ELF loader
Load directly from memory Yes Usually awkward or unavailable Yes, if you build it
Load relocatable objects (ET_REL) Yes, feature-gated No Yes, if you build it
Inspect and rewrite layout before mapping Yes, through scan-first link passes No Yes, if you build it
Caller-owned dependency graph Yes, through Linker, LinkContext, and KeyResolver Usually no Yes, if you build it
Typed symbol lifetime safety Yes No Depends on your design
Custom relocation interception Yes Usually no Yes, if you build it
no_std-friendly core Yes No Depends on your implementation

Safety by Construction

Typed symbols borrow the loaded image, so they cannot outlive the library that produced them.

let symbol = unsafe {
    lib.get::<fn()>("plugin_fn")
        .expect("symbol `plugin_fn` not found")
};
drop(lib);
// symbol(); // does not compile: the symbol cannot outlive the library

Quick Start

Add the crate with the default feature set:

[dependencies]
elf_loader = "0.14"

Or enable the common advanced feature bundle:

[dependencies]
elf_loader = { version = "0.14", features = ["full"] }

Load a Dynamic Library and Call a Symbol

use elf_loader::{Loader, Result};

extern "C" fn host_double(value: i32) -> i32 {
    value * 2
}

fn main() -> Result<()> {
    let lib = Loader::new()
        .load_dylib("path/to/plugin.so")?
        .relocator()
        .pre_find_fn(|name| {
            if name == "host_double" {
                Some(host_double as *const ())
            } else {
                None
            }
        })
        .relocate()?;

    let run = unsafe {
        lib.get::<extern "C" fn(i32) -> i32>("run")
            .expect("symbol `run` not found")
    };
    assert_eq!(run(21), 42);

    Ok(())
}

Mental Model

path / bytes / ElfFile / ElfBinary
                 |
            Loader or Linker
                 |
    +------------+----------------------+
    |                                   |
 direct load                         scan-first link
    |                                   |
 RawDylib / RawExec / RawObject*    LinkPlan
    |                              passes / layout / arenas
                 |
              Relocator
   pre_find / scope / lazy lookups / handlers / binding
                 |
    +------------+-------------+
    |            |             |
 LoadedDylib   LoadedExec   LoadedObject*
                 |
      get() / deps() / TLS / metadata

* requires the `object` feature

Common Workflows

Load from Memory

use elf_loader::{Loader, Result, input::ElfBinary};

fn main() -> Result<()> {
    let bytes = std::fs::read("path/to/plugin.so").unwrap();

    let lib = Loader::new()
        .load_dylib(ElfBinary::new("plugin.so", &bytes))?
        .relocator()
        .relocate()?;

    println!("loaded {} at 0x{:x}", lib.name(), lib.base());
    Ok(())
}

load_dylib(&bytes) and load_exec(&bytes) also work if a synthetic name such as "<memory>" is acceptable.

Mix .o and .so Inputs

This requires the object feature.

# use elf_loader::{Loader, Result};
# fn main() -> Result<()> {
let mut loader = Loader::new();

let base = loader
    .load_object("path/to/base.o")?
    .relocator()
    .pre_find_fn(|_| None)
    .relocate()?;

let plugin = loader
    .load_dylib("path/to/plugin.so")?
    .relocator()
    .scope([&base])
    .relocate()?;
# let _ = plugin;
# Ok(())
# }

Optimize a Runtime Dependency Graph Before Mapping

Use Linker when loading is more than "map this one file". The scan-first path resolves DT_NEEDED edges, builds a plan for the whole pending group, lets you mutate layout/materialization, and only then maps and relocates the modules.

# use elf_loader::{Result, input::ElfFile};
# use elf_loader::linker::{
#     DependencyRequest, KeyResolver, LinkContext, LinkPassPlan, Linker, Materialization,
#     ReorderPass, ResolvedKey,
# };
# struct Resolver;
# impl KeyResolver<'static, &'static str, ()> for Resolver {
#     fn load_root(&mut self, key: &&'static str) -> Result<ResolvedKey<'static, &'static str>> {
#         Ok(ResolvedKey::load(*key, ElfFile::from_path("path/to/plugin.so")?))
#     }
#     fn resolve_dependency(
#         &mut self,
#         _req: &DependencyRequest<'_, &'static str, ()>,
#     ) -> Result<Option<ResolvedKey<'static, &'static str>>> {
#         Ok(None)
#     }
# }
# fn main() -> Result<()> {
let mut context = LinkContext::<&'static str, ()>::new();
let resolver = Resolver;

let configure = |plan: &mut LinkPassPlan<'_, &'static str, (), ReorderPass>| -> Result<()> {
    plan.set_materialization(plan.root(), Materialization::SectionRegions);
    Ok(())
};

let plugin = Linker::new()
    .resolver(resolver)
    .map_pipeline(|mut pipeline| {
        pipeline.push(configure);
        pipeline
    })
    .load_scan_first(&mut context, "plugin")?;
# let _ = plugin;
# Ok(())
# }

See cargo run --example load_scan_first for a complete example that constructs real DT_NEEDED edges and loads a dependency chain through the scan-first linker.

Configure Lazy Binding Fixups

This requires the lazy-binding feature.

# use elf_loader::{Loader, Result};
# extern "C" fn host_double(value: i32) -> i32 { value * 2 }
# fn main() -> Result<()> {
let lib = Loader::new()
    .load_dylib("path/to/plugin.so")?
    .relocator()
    .pre_find_fn(|name| {
        if name == "host_double" {
            Some(host_double as *const ())
        } else {
            None
        }
    })
    .share_find_with_lazy()
    .lazy()
    .relocate()?;
# let _ = lib;
# Ok(())
# }

Use share_find_with_lazy() when PLT fixups should reuse the same host lookup policy as the initial relocation pass. If lazy fixups need different rules, configure lazy_pre_find_fn() / lazy_post_find_fn() directly.

Inspect an Executable or PIE

use elf_loader::{Loader, Result};

fn main() -> Result<()> {
    let mut loader = Loader::new();
    let exec = loader.load_exec("path/to/program")?;

    println!("name  = {}", exec.name());
    println!("entry = 0x{:x}", exec.entry());
    println!("base  = 0x{:x}", exec.base());

    Ok(())
}

Where It Fits Best

  • Plugin and extension systems that need host-provided symbols or custom symbol search order
  • JITs and runtimes that want to load ELF content from memory instead of only from disk
  • Kernels, embedded environments, and low-level runtimes that need more control than an OS-native loader exposes
  • Hot-reload or instrumentation workflows that benefit from relocation hooks and lifecycle control
  • ELF-focused tooling and research projects where visibility into relocation behavior matters

Where It May Be Too Much

  • Applications that only need plain OS-native dynamic loading with no custom symbol policy
  • Projects that want a module/plugin boundary but do not want to think about ELF details at all
  • Heavy ET_REL workflows on non-x86_64 targets that have not been validated in your environment yet

Feature Flags

Feature Default Purpose
tls Yes Enables TLS relocation handling and APIs such as Loader::with_default_tls_resolver()
lazy-binding No Enables PLT/GOT lazy binding plus Relocator::lazy(), share_find_with_lazy(), and lazy_pre_find*() / lazy_post_find*()
object No Enables relocatable object (ET_REL) loading via Loader::load_object()
version No Enables version-aware symbol lookup such as get_version()
log No Enables log integration for loader and relocation diagnostics
portable-atomic No Adds support for targets without native pointer-sized atomics
use-syscall No Uses the Linux syscall backend instead of libc where applicable
full No Convenience bundle for tls, lazy-binding, and object

Notes:

  • Compiling with tls is not enough by itself for TLS-using modules. Start from Loader::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_object will 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
from_memory Load ELF data from a byte buffer cargo run --example from_memory
load_exec Inspect executable metadata such as entry/base cargo run --example load_exec
load_hook Observe segment loading with with_hook() cargo run --example load_hook
load_scan_first Discover DT_NEEDED, run layout passes, and materialize section regions cargo run --example load_scan_first
lifecycle Custom .init / .fini handling cargo run --example lifecycle
user_data Attach per-image metadata with with_context_loader() 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

  • The crate currently targets x86_64, x86, aarch64, arm, riscv64, riscv32, and loongarch64.
  • Dynamic library and executable loading are the primary supported paths across those architectures.
  • Relocatable object (.o) support is currently centered on x86_64 relocation handling. Treat non-x86_64 object loading as experimental unless you have validated it for your own target.
  • 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 relocation coverage, platform support, and documentation.

  • Open an issue if you hit a loader or relocation edge case.
  • Send a PR if you want to improve architecture support, examples, or diagnostics.
  • Star the project if it is useful in your work.

License

This project is dual-licensed under either of the following:

Contributors

Project contributors

About

A high-performance, no_std-friendly ELF loader and runtime linker for Rust

Topics

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors