Dynamic Linking Contract

Status: Phase 1 (experimental eager binding)

This page defines what dynamic linking means on P1Start and what is deliberately out of scope today.

See also

Dynamic linking ABI — code index (curated) — code-indexed DT_* and R_X86_64_* tables (newdocs/dynamic-linking-abi.md); pair this policy page with that map when debugging ld.elf.

Why This Is Conservative

P1Start prioritizes deterministic behavior, clear failure modes, and low runtime complexity.
Dynamic linking is introduced in narrow, testable slices so static applications remain stable while loader policy becomes explicit.

Current Support (Phase 1 Experimental)

  • Static linking remains the only production SDK/runtime contract for applications.

  • ld.elf parses PT_DYNAMIC and selected DT_* metadata for runtime binding.

  • ld.elf maps candidate dependencies and applies eager relocations for the x86_64 subset listed under Implemented Phase 1 Runtime Scope below (including RELATIVE, GLOB_DAT, JUMP_SLOT, absolute/PC-relative relocations, GOTPCREL*, and R_X86_64_32 / 32S).

  • p1dl exposes stable API entry points (dlopen / dlsym / dlclose) with deterministic argument validation and a loader-bridge dispatch path.

Hard Runtime Policy (Phase 1 Experimental)

  • If ld.elf detects DT_NEEDED, it must attempt deterministic eager binding.

  • Launch continues only when all required dependencies resolve and supported relocations bind successfully.

  • Unsupported metadata/relocations or unresolved symbols must fail loudly with DL_E_* diagnostics.

  • Silent continuation on partial failure is forbidden.

Implemented Phase 1 Runtime Scope (Experimental)

The current runtime dynamic-linking implementation supports only:

  • ELF64 / x86_64 userland objects.

  • PIE/shared-object position-independent code only.

  • Relocation types:

    • R_X86_64_RELATIVE

    • R_X86_64_GLOB_DAT

    • R_X86_64_JUMP_SLOT

    • R_X86_64_64

    • R_X86_64_PC32, R_X86_64_PLT32 (PLT32 is applied with the same PC-relative eager semantics as PC32)

    • R_X86_64_PC64

    • R_X86_64_GOTPCREL, R_X86_64_GOTPCRELX, R_X86_64_REX_GOTPCRELX (requires a prior GLOB_DAT/JUMP_SLOT recording the GOT slot address for the symbol index)

    • R_X86_64_32, R_X86_64_32S

  • Eager binding only (no lazy PLT binding).

  • Dependency init hooks: DT_INIT and DT_INIT_ARRAY.

  • Runtime-handle teardown hooks on close-to-zero: DT_FINI and DT_FINI_ARRAY.

  • Library discovery from explicit embedded path metadata plus /lib.

  • Bridge-backed p1dl calls for deterministic runtime loads from policy-approved paths.

This scope is implemented and boot-smoke validated, but remains experimental rather than a production SDK compatibility promise.

Explicitly Out Of Scope (Phase 1)

  • Symbol versioning (DT_VER*).

  • LD_LIBRARY_PATH-style environment overrides.

  • Lazy binding.

  • Text relocations.

  • Non-PIE shared objects.

  • R_X86_64_COPY and other copy-relocation semantics.

Library Search Path Policy Direction

  • Deterministic order is required.

  • No implicit current-working-directory lookup.

  • Phase-1 goal order (implemented for the main executable search list used by startup DL_NEEDED resolution and the p1dl bridge):

    1. DT_RUNPATH entries in declaration order, each segment with $ORIGIN / ${ORIGIN} expanded using the main program directory from argv[0] (see final bullet),

    2. then DT_RPATH entries in declaration order (same expansion),

    3. then fixed system path /lib (and a deterministic \lib VFS probe variant).

  • Nested DT_NEEDED: each DSO searches (1) its own expanded DT_RUNPATH, (2) its expanded DT_RPATH, (3) then the list inherited from the parent (for main’s direct deps, that is the main’s global list: runpath + rpath + /lib). Recursion passes each object’s merged list to its children.

  • $ORIGIN / ${ORIGIN}: expand to the directory of the ELF that owns the tag (from the resolved load path). The main executable uses the directory of argv[0]. $PLATFORM and $LIB are not implemented in Phase 1.

Versioning And Compatibility Direction

  • Initial dynamic-linking phase does not claim SONAME/version conflict resolution support.

  • Unsupported version metadata must produce explicit loader failure, not best-effort binding.

  • SONAME matching/version negotiation is deferred until after base relocation/symbol binding is stable.

  • Current loader policy fails with DL_E_VERSION_UNSUPPORTED when DT_VER* metadata is detected in the main executable or dependencies.

Symbol Visibility And Binding Direction

  • Initial runtime binding scope is process-local and conservative.

  • Default expectation is local-only behavior unless explicit global export policy is introduced in a later phase.

  • Current resolver policy:

    • ignore STB_LOCAL exports for inter-object resolution,

    • ignore non-default visibility exports,

    • prefer strong global over weak for same symbol name,

    • fail on duplicate strong globals with DL_E_DUPLICATE_SYMBOL,

    • keep first weak definition when only weak candidates exist.

  • Duplicate weak symbols are allowed (first-seen wins). Duplicate strong global symbols are errors (DL_E_DUPLICATE_SYMBOL). Local symbols are ignored for inter-object binding.

PLT/GOT Direction

  • Because R_X86_64_JUMP_SLOT is in scope, PLT/GOT relocation handling is required.

  • Phase-1 remains eager-only: PLT entries are resolved during load, not lazy-bound at first call.

p1dl API Contract (Current)

  • dlopen() / dlsym() / dlclose() are stable API entry points.

  • During current experimental bring-up they validate arguments/handles first (InvalidArgument / InvalidHandle), then call a loader-installed bridge dispatch when present, and otherwise return an explicit unsupported error (NotSupported).

  • Phase-1 bridge behavior supports deterministic runtime dlopen mapping for policy-approved ELF64 shared objects.

  • Phase-1 bridge contract details:

    • dlopen(path) first checks already-loaded handles, then attempts deterministic runtime mapping from configured loader search paths.

    • dlsym(handle, name) resolves only within that handle’s export table.

    • dlclose(handle) decrements the bridge refcount for runtime-loaded objects; when the refcount reaches zero, the loader runs DT_FINI_ARRAY (reverse order) then DT_FINI for that handle. Memory unmapping (full unload) and teardown of startup-pinned DT_NEEDED dependencies remain out of scope — see Init/fini policy bullets below.

  • Init/fini policy in this phase:

    • DT_INIT / DT_INIT_ARRAY are executed after dependency mapping/relocation.

    • Runtime-loaded handles execute DT_FINI_ARRAY (reverse order) then DT_FINI when dlclose drops refcount to zero.

    • Startup-pinned DT_NEEDED dependencies are not finalized by bridge dlclose.

  • A runtime smoke harness is available (scripts/run_p1dl_runtime_boot_smoke.py) to stage fixture artifacts, boot QEMU, and assert bridge + p1dl pass markers from serial output (kernel_serial.log at repo root). The script pre-flights qemu-system-x86_64 and run_qemu.bat, warns if OVMF.fd is missing, and accounts for long build_image.bat runs before the serial file appears; see SDK/loader-fixtures/p1dl-runtime-smoke/README.md and SDK/shared-library-development.md.

  • Current fixture status: p1dl_runtime_smoke.elf passes in-QEMU with successful hello.so probe/load, bridge install, and [p1dl-smoke] PASS.

  • As loader support arrives, unsupported objects or metadata must map to explicit error variants, never panic paths.

Loader Error Codes (Contract)

ld.elf diagnostics should include stable machine-friendly codes.

  • DL_E_NEEDED_UNSUPPORTED — executable has DT_NEEDED while runtime DSO loading is disabled.

  • DL_E_UNSUPPORTED_RELOC — relocation type is outside currently supported relocation subset.

  • DL_E_UNSUPPORTED_OBJECT — object metadata violates current phase policy (for example non-PIE in phase-1 scope).

  • DL_E_VERSION_UNSUPPORTED — symbol versioning metadata encountered before versioning support exists.

  • DL_E_POLICY_DENIED — loading denied by runtime security/search-path policy.

  • DL_E_SYMBOL_NOT_FOUND — required symbol for eager relocation was not resolved from loaded dependency exports.

  • DL_E_DUPLICATE_SYMBOL — duplicate strong global symbol definition detected during dependency export table merge.

Acceptance Criteria (Phase 1 Experimental)

  • Existing static SDK apps run unchanged.

  • ld.elf reports deterministic PT_DYNAMIC/DT_* metadata and deterministic search-path probing.

  • ld.elf binds supported relocation types eagerly and fails loudly for unsupported/unresolved cases.

  • p1dl remains a stable surface with deterministic unsupported behavior.

Developer workflow (building and staging .so files)

Step-by-step recipes (Clang + LLD, hello.so fixture, build/image.manifest E lines, runtime dlopen via p1dl, QEMU smoke): SDK/shared-library-development.md in the main repository.

Coverage-matrix summary (paths, relocation list, status): reference/coverage-matrix section Dynamic linking & shared objects.