dl

package
v0.0.0-...-ba0b20d Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 27, 2026 License: GPL-3.0 Imports: 9 Imported by: 0

README

pkg/dl — Pure-Go ELF Dynamic Loader

A runtime ELF dynamic linker implemented entirely in Go, with no cgo or external dependencies. Targets Linux amd64 (x86-64) and Linux arm64 (AArch64).

Security Warning

This package loads and executes native machine code from disk. Loading a shared object (.so) is equivalent to executing arbitrary code in the current process. No sandboxing is provided or claimed. Treat all .so files as untrusted unless you have independently verified them.

What This Is

A "mini ld.so" for userspace Go programs. It can:

  • Load ELF shared objects (ET_DYN) from the filesystem
  • Recursively resolve dependencies (DT_NEEDED)
  • Perform relocations (RELATIVE, GLOB_DAT, JUMP_SLOT, ABS64, TLS, TLSDESC, IRELATIVE)
  • Resolve symbols using GNU hash and SysV hash tables
  • Support symbol versioning (VERSYM/VERNEED/VERDEF)
  • Run constructors/destructors (DT_INIT/DT_INIT_ARRAY, DT_FINI/DT_FINI_ARRAY)
  • Apply GNU_RELRO protections
  • Handle TLS relocations (DTPMOD, DTPOFF, TPOFF, TLSDESC)
  • Call exported functions with integer/pointer arguments

Supported Platforms

Architecture Build Tag Relocations
x86-64 GOARCH=amd64 RELATIVE, GLOB_DAT, JUMP_SLOT, 64, DTPMOD64, DTPOFF64, TPOFF64, TLSDESC, IRELATIVE
AArch64 GOARCH=arm64 RELATIVE, GLOB_DAT, JUMP_SLOT, ABS64, TLS_DTPMOD64, TLS_DTPOFF64, TLS_TPREL64, TLSDESC, IRELATIVE

Limitations

  • No floating-point arguments: The Call* helpers use syscall.Syscall which only passes integer registers. Functions like cos(double) cannot be called correctly.
  • TLS access from Go: TLS relocations are applied for structural correctness, but Go manages its own thread-local storage (FS/TPIDR_EL0). Calling C functions that access TLS variables from Go goroutines may not work.
  • Lazy binding: PLT lazy binding is implemented as eager resolution because we cannot inject a resolver trampoline into the loaded code. The RTLD_LAZY flag is accepted but behaves identically to RTLD_NOW.
  • RTLD_DEEPBIND: Implemented for symbol lookup ordering but may not match glibc behavior in all edge cases.
  • Init/Fini: Constructors and destructors are called via syscall.Syscall, which works for simple init functions but may fail for complex constructors that depend on full C runtime state.

Building

# Native build (amd64)
GOOS=linux GOARCH=amd64 go build ./pkg/dl/...
GOOS=linux GOARCH=amd64 go build ./cmd/dl/...

# Cross-compile for arm64
GOOS=linux GOARCH=arm64 go build ./pkg/dl/...
GOOS=linux GOARCH=arm64 go build ./cmd/dl/...

Running Examples

All examples use the single cmd/dltest binary with subcommands:

Using system libraries (default root /)
go run ./cmd/dltest puts
go run ./cmd/dltest info libc.so.6
go run ./cmd/dltest selfcheck
Using an alternate sysroot (e.g. /linux)
go run ./cmd/dltest -root /linux puts
go run ./cmd/dltest -root /linux info libc.so.6
go run ./cmd/dltest -root /linux selfcheck

Debugging

Set the environment variable LOADER_DEBUG=1 or use dl.WithDebug():

LOADER_DEBUG=1 go run ./cmd/dltest puts

Or programmatically:

ld := dl.New(dl.WithDebug())

API Overview

// Create a loader with options.
ld := dl.New(
    dl.WithRoot("/linux"),
    dl.WithSearchPaths("/opt/lib"),
    dl.WithDebug(),
)

// Load a shared object.
h, err := ld.Open("libc.so.6", dl.RTLD_NOW|dl.RTLD_GLOBAL)
defer h.Close()

// Resolve a symbol.
addr, err := h.Sym("puts")

// Resolve a versioned symbol.
addr, err = h.SymVersion("puts", "GLIBC_2.2.5")

// Call the function.
buf, ptr := dl.CString("hello")
_ = buf // keep alive
dl.Call1(addr, ptr)

// Check stats.
stats := ld.Stats()

Architecture Overview

┌─────────────────────────────────────────────┐
│                  Loader                      │
│  - Object cache (by path + soname)           │
│  - Global scope (RTLD_GLOBAL objects)        │
│  - TLS module allocator                      │
│  - Search path configuration                 │
├─────────────────────────────────────────────┤
│                  Object                      │
│  - Mapped PT_LOAD segments                   │
│  - Parsed dynamic section (DT_*)             │
│  - Symbol table + string table               │
│  - GNU hash / SysV hash                      │
│  - Version tables (VERSYM/VERDEF/VERNEED)    │
│  - Relocation tables (RELA/REL/JMPREL)       │
│  - TLS metadata                              │
│  - Init/fini arrays                          │
│  - Refcount + state flags                    │
├─────────────────────────────────────────────┤
│                  Handle                      │
│  - Primary object pointer                    │
│  - Local scope (self + transitive deps)      │
│  - Sym() / SymVersion() / Close()            │
└─────────────────────────────────────────────┘
Loading flow
  1. Search: Resolve soname to filesystem path (RUNPATH → LD_LIBRARY_PATH → RPATH → defaults)
  2. Parse: Read ELF header + program headers, validate e_machine
  3. Map: Reserve address range, mmap PT_LOAD segments with correct protections
  4. Dynamic: Parse PT_DYNAMIC for tables, strings, symbols, hashes, versions
  5. Dependencies: BFS-load all DT_NEEDED libraries
  6. Relocate: Apply RELA/REL/JMPREL entries (deps first, then primary)
  7. RELRO: mprotect GNU_RELRO region as read-only
  8. Init: Call DT_INIT then DT_INIT_ARRAY (deps first)
  9. Return: Handle with local scope for symbol lookup
Symbol resolution order
  1. DEEPBIND → search self first
  2. Local scope (handle's objects in load order)
  3. Global scope (all RTLD_GLOBAL objects in load order)
  4. Weak symbols accepted as fallback

File Layout

File Purpose
dl.go Public API: Loader, Handle, Flags, Options, Stats
elf.go ELF64 constants and structures
errors.go Typed error types
object.go Object struct, ELF header/phdr parsing, symbol access
loader.go Open/Close, library search, dependency loading
dynamic.go PT_DYNAMIC parsing and rebasing
map.go mmap, protections, BSS, RELRO
sym.go GNU hash, SysV hash, versioning, scoped lookup
reloc.go Relocation engine (table iteration)
reloc_amd64.go x86-64 relocation dispatch
reloc_arm64.go AArch64 relocation dispatch
tls.go TLS module/offset allocation
tls_amd64.go x86-64 TLS variant II notes
tls_arm64.go AArch64 TLS variant I notes
initfini.go Constructor/destructor calling
call.go CString helpers
call_amd64.go x86-64 Call0..Call6, IFUNC resolver
call_arm64.go AArch64 Call0..Call6, IFUNC resolver
arch_amd64.go x86-64 machine validation + default paths
arch_arm64.go AArch64 machine validation + default paths

Documentation

Overview

Package dl implements a pure-Go ELF dynamic loader / runtime linker for Linux amd64 and arm64. It provides dlopen/dlsym semantics: loading shared objects (.so files) at runtime, resolving symbols, performing relocations, and calling exported functions — all without cgo.

SECURITY WARNING: Loading a shared object executes arbitrary native machine code in the current process. No sandboxing is provided or claimed. Treat all .so files as untrusted unless you have independently verified them.

Quick start

ld := dl.New(dl.WithRoot("/linux"))
h, err := ld.Open("libc.so.6", dl.RTLD_NOW|dl.RTLD_GLOBAL)
if err != nil { log.Fatal(err) }
defer h.Close()
addr, err := h.Sym("puts")
if err != nil { log.Fatal(err) }
dl.Call1(addr, dl.CString("hello world"))

Package dl provides a pure-Go ELF dynamic loader / runtime linker.

SECURITY WARNING: This package loads and executes native machine code from disk. Treat all .so files as untrusted unless independently verified. Loading a shared object is equivalent to executing arbitrary native code in the current process. No sandboxing is provided or claimed.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrNotFound       = fmt.Errorf("dl: library not found")
	ErrNotELF         = fmt.Errorf("dl: not an ELF file")
	ErrWrongArch      = fmt.Errorf("dl: wrong ELF machine type for this architecture")
	ErrNotShared      = fmt.Errorf("dl: ELF type is not ET_DYN (shared object / PIE)")
	ErrSymbolNotFound = fmt.Errorf("dl: symbol not found")
	ErrClosed         = fmt.Errorf("dl: handle is closed")
	ErrUnsupported    = fmt.Errorf("dl: unsupported")
)

Common sentinel errors.

Functions

func CString

func CString(s string) ([]byte, uintptr)

CString allocates a NUL-terminated byte slice suitable for passing to C functions expecting a const char*. The returned uintptr points to the first byte. The caller must keep the returned []byte alive for the duration of the call (to prevent GC from collecting it).

Example:

s, p := dl.CString("hello")
_ = s // keep alive
dl.Call1(putsAddr, p)

func CStringPtr

func CStringPtr(s string) uintptr

CStringPtr is a convenience that returns just the pointer. The caller must ensure the string stays referenced until after the native call returns. In practice, the Go compiler will keep the string argument alive.

func Call0

func Call0(addr uintptr) uintptr

Call0 calls a native void(*)(void) function at addr.

func Call1

func Call1(addr uintptr, a1 uintptr) uintptr

Call1 calls a native function with 1 integer/pointer argument.

func Call2

func Call2(addr uintptr, a1, a2 uintptr) uintptr

Call2 calls a native function with 2 integer/pointer arguments.

func Call3

func Call3(addr uintptr, a1, a2, a3 uintptr) uintptr

Call3 calls a native function with 3 integer/pointer arguments.

func Call6

func Call6(addr uintptr, a1, a2, a3, a4, a5, a6 uintptr) uintptr

Call6 calls a native function with up to 6 integer/pointer arguments.

Types

type Flags

type Flags uint32

Flags control how a shared object is loaded and bound.

const (
	// RTLD_LAZY defers function binding until first call (PLT lazy binding).
	RTLD_LAZY Flags = 0x00001

	// RTLD_NOW resolves all symbols before Open returns.
	RTLD_NOW Flags = 0x00002

	// RTLD_GLOBAL makes the object's symbols available for subsequent loads.
	RTLD_GLOBAL Flags = 0x00100

	// RTLD_LOCAL restricts symbol scope to the handle (default).
	RTLD_LOCAL Flags = 0x00000

	// RTLD_DEEPBIND causes the loaded object to prefer its own symbols over
	// those in the global scope when resolving references.
	RTLD_DEEPBIND Flags = 0x00008

	// RTLD_NODELETE prevents unloading even when refcount reaches zero.
	RTLD_NODELETE Flags = 0x01000

	// RTLD_NOLOAD does not load the object; returns a handle only if already
	// loaded (useful for promoting to RTLD_GLOBAL or getting stats).
	RTLD_NOLOAD Flags = 0x00004
)

type FormatError

type FormatError struct {
	Path   string
	Detail string
}

FormatError is returned when an ELF file is malformed.

func (*FormatError) Error

func (e *FormatError) Error() string

type Handle

type Handle struct {
	// contains filtered or unexported fields
}

Handle represents a loaded shared object (or group of objects for transitive dependencies). Use Sym to resolve exported symbols and Close to decrement the reference count.

func (*Handle) Close

func (h *Handle) Close() error

Close decrements the reference count of all objects in this handle's scope. When an object's count reaches zero, its destructors (DT_FINI_ARRAY, DT_FINI) are called and its mappings are released — in reverse dependency order.

func (*Handle) Sym

func (h *Handle) Sym(name string) (uintptr, error)

Sym resolves a symbol by name and returns its absolute virtual address. The returned uintptr can be passed to Call0..Call6 helpers.

func (*Handle) SymVersion

func (h *Handle) SymVersion(name, version string) (uintptr, error)

SymVersion resolves a versioned symbol. Version should match the version string from the library's version definition (e.g. "GLIBC_2.17").

type LoadError

type LoadError struct {
	Path string
	Err  error
}

LoadError is returned when an ELF object cannot be loaded.

func (*LoadError) Error

func (e *LoadError) Error() string

func (*LoadError) Unwrap

func (e *LoadError) Unwrap() error

type Loader

type Loader struct {
	// contains filtered or unexported fields
}

Loader holds global state for the dynamic linker: loaded objects, search paths, the global symbol scope, and caching. It is safe for concurrent use.

func New

func New(opts ...Option) *Loader

New creates a Loader with the given options.

func (*Loader) AddSearchPath

func (ld *Loader) AddSearchPath(path string)

AddSearchPath appends a directory to the library search list.

func (*Loader) Objects

func (ld *Loader) Objects() []*Object

Objects returns a deduplicated snapshot of all loaded objects for debugging. The map may contain both path and soname keys pointing to the same object; this method deduplicates by pointer identity.

func (*Loader) Open

func (ld *Loader) Open(name string, flags Flags) (*Handle, error)

Open loads a shared object and its dependencies, applies relocations, and runs constructors. The returned Handle provides Sym/Close.

Flags control binding and scope behavior:

  • RTLD_NOW: resolve all symbols immediately (default if RTLD_LAZY not set)
  • RTLD_LAZY: defer PLT binding (currently resolved eagerly for correctness)
  • RTLD_GLOBAL: add symbols to the global scope
  • RTLD_LOCAL: keep symbols in handle-local scope (default)
  • RTLD_DEEPBIND: prefer object's own symbols
  • RTLD_NOLOAD: return handle only if already loaded
  • RTLD_NODELETE: prevent unloading

func (*Loader) SetRoot

func (ld *Loader) SetRoot(root string)

SetRoot changes the filesystem root for library search.

func (*Loader) Stats

func (ld *Loader) Stats() Stats

Stats returns a snapshot of loader statistics.

type Object

type Object struct {
	// Identity
	Path   string // resolved filesystem path
	Soname string // DT_SONAME if present

	// Memory layout
	Base     uintptr // load bias (difference between mapped and vaddr)
	MinVaddr uint64  // lowest vaddr of PT_LOAD segments
	MaxVaddr uint64  // highest vaddr+memsz (page-aligned)
	MapSize  uintptr // total mapped region size

	// Program headers (in-memory copy)
	Phdrs []elf64Phdr

	// Dependencies (sonames from DT_NEEDED)
	Needed []string

	// RPATH / RUNPATH for dependency search
	Rpath   string
	Runpath string
	// contains filtered or unexported fields
}

Object represents a single loaded ELF shared object. It contains the parsed headers, memory mappings, dynamic tables, symbol/string tables, relocation info, TLS metadata, and dependency list.

type Option

type Option func(*Loader)

Option configures a Loader.

func WithDebug

func WithDebug() Option

WithDebug enables verbose debug logging to stderr.

func WithLDLibraryPath

func WithLDLibraryPath() Option

WithLDLibraryPath enables reading LD_LIBRARY_PATH from the environment and prepending those directories to the search list.

func WithLogger

func WithLogger(l *log.Logger) Option

WithLogger sets a custom logger for debug output.

func WithRoot

func WithRoot(root string) Option

WithRoot sets an alternate filesystem root. Library search paths like /lib, /usr/lib become <root>/lib, <root>/usr/lib. Default is "/".

func WithSearchPaths

func WithSearchPaths(paths ...string) Option

WithSearchPaths adds extra directories to the default library search list.

type RelocError

type RelocError struct {
	Type   uint32 // R_* relocation type number
	Symbol string // symbol name, if any
	Object string // object containing the relocation
	Err    error
}

RelocError is returned when a relocation cannot be applied.

func (*RelocError) Error

func (e *RelocError) Error() string

func (*RelocError) Unwrap

func (e *RelocError) Unwrap() error

type Stats

type Stats struct {
	LoadedObjects int    // number of currently loaded ELF objects
	TotalRelocs   uint64 // total relocations applied across all objects
	SymbolLookups uint64 // total symbol lookup attempts
	CacheHits     uint64 // symbol cache hits
}

Stats contains diagnostic counters for a Loader.

type SymbolError

type SymbolError struct {
	Name    string
	Version string // empty if unversioned
	Object  string // object that references the symbol
	Err     error
}

SymbolError is returned when a symbol cannot be resolved.

func (*SymbolError) Error

func (e *SymbolError) Error() string

func (*SymbolError) Unwrap

func (e *SymbolError) Unwrap() error

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL