kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

commit c5899fea4182a2cc597568712047d0b29cf5002c
parent f5e26d02b0f89dfa410e54faed53cf944961bae3
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Wed, 17 Jun 2026 07:47:18 -0700

doc: plan ELF shared library support

Diffstat:
Adoc/plan/DYNLIB.md | 343+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdoc/plan/README.md | 1+
2 files changed, 344 insertions(+), 0 deletions(-)

diff --git a/doc/plan/DYNLIB.md b/doc/plan/DYNLIB.md @@ -0,0 +1,343 @@ +# Dynamic/shared libraries (Linux first) + +Goal: make kit produce loadable shared libraries, with Linux/aarch64 as the +first supported target. The useful first result is: + +``` +kit ld -shared -soname libfoo.so.1 -o libfoo.so foo.pic.o +kit cc -shared -fPIC -Wl,-soname,libfoo.so.1 -o libfoo.so foo.c +``` + +where `readelf -h` reports `ET_DYN`, there is no required entry point, no +`PT_INTERP`, `DT_SONAME` is present when requested, exported symbols are in +`.dynsym`, unresolved imports become runtime relocations, and a normal Linux +loader can use the `.so` as a dependency of an executable. + +## Current state + +kit already has most of the dynamic-executable substrate: + +- The public link API has `KIT_LINK_OUTPUT_SHARED`, `soname`, rpath/runpath, + exports, and `allow_undefined` fields in `KitLinkSessionOptions`. +- `driver/cmd/{cc,ld,build}.c` parse `-shared` / `-soname` but reject shared + output with "creating dynamic/shared libraries is not yet supported". +- `src/api/link.c` maps `KIT_LINK_OUTPUT_SHARED` to `link_set_pie(l, 1)`, but + warns that `-soname`, rpaths, and explicit exports are ignored. +- `src/obj/elf/link_dyn.c` synthesizes `.dynsym`, `.dynstr`, `.gnu.hash`, + `.rela.dyn`, `.rela.plt`, `.plt`, `.got.plt`, and `.dynamic`, but it is + explicitly shaped as ET_DYN executable support. It always chooses an + interpreter path when dynamic state exists. +- `src/obj/elf/link.c` emits `ET_DYN` when `img->pie` is set, but requires + `img->entry_sym` and writes `e_entry` from it. It emits dynamic program + headers as `PT_PHDR`, `PT_INTERP`, `PT_DYNAMIC`, and `PT_GNU_STACK` whenever + `pie && img->dyn`. +- The linker can already consume DSO inputs: `LINK_INPUT_DSO_BYTES` contributes + exported dynsym symbols for resolution and creates `DT_NEEDED` records. +- AArch64 already has PLT emission in `src/arch/aa64/link.c` and GOT-using + relocation rows in `src/arch/aa64/reloc.c`. +- kit-generated aarch64 code uses `obj_symbol_extern_via_got()` for undefined + extern data when the target is ELF PIC/PIE, so some PIC codegen policy is + already centralized in `src/obj/obj_secnames.c`. + +The missing part is not "all dynamic linking"; it is a distinct DSO output +mode. PIE executables and DSOs are both `ET_DYN`, but they differ in entry +requirements, interpreter headers, exported-symbol policy, undefined-symbol +defaults, dynamic tags, relocation legality, and driver behavior. + +## Scope + +### Linux/aarch64 MVP + +- ELF64 only. +- Target triples like `aarch64-linux-*`. +- Work backward from a known-good loader fixture: verify the shared-library + test harness with all-clang first, then swap in `kit ld -shared` while + keeping clang PIC objects, then swap in `kit cc -fPIC` so the final lane is + all kit. +- Support regular function/data exports, DSO inputs, `DT_NEEDED`, + `DT_SONAME`, `DT_RPATH` / `DT_RUNPATH`, `.gnu.hash`, `.rela.dyn`, + `.rela.plt`, AArch64 PLT/GOT, `R_AARCH64_RELATIVE`, `GLOB_DAT`, and + `JUMP_SLOT`. +- Default shared-library undefined-symbol policy should match ELF linkers: + allow unresolved non-weak references by default in `-shared`, unless a + stricter flag is requested. + +### Explicitly later + +- x86-64 and riscv64 DSOs. +- TLS models beyond the currently practical local-exec / initial-exec subset. +- Copy relocations, symbol version definitions, version scripts, GNU + `--dynamic-list`, `--exclude-libs`, `-Bsymbolic*`, `--as-needed` parity, and + lazy binding. +- Mach-O dylib and COFF/PE DLL production. +- Runtime-library policy for building libkit_rt itself as a DSO. + +## Implementation checklist + +### 1. Split PIE-executable mode from DSO mode + +- Add an internal `emit_shared` / `output_shared` bit on `Linker` and mirror it + on `LinkImage`, instead of representing shared output only as `emit_pie`. +- Keep `emit_pie` for dynamic/static PIE executable behavior. Let shared output + imply image base 0 and dynamic-section synthesis without implying an + executable entry or interpreter. +- Add `link_set_shared(Linker*, int)` beside `link_set_pie`. +- In `kit_link_session_new`, for `KIT_LINK_OUTPUT_SHARED`, set shared mode, + enable dynamic layout, and do not call the static-exe setup. +- Update comments in `src/link/link.h`, `src/link/link_internal.h`, and + `doc/LINK.md` after the implementation so "PIE/DSO" is not conflated with + "PIE executable". + +Acceptance: + +- A shared link reaches `kit_link_session_resolve` without looking up `_start`. +- Existing PIE executable tests keep their current behavior. + +### 2. Entry and ELF header behavior + +- Change `link_resolve_entry` / ELF emit so `KIT_LINK_OUTPUT_SHARED` permits + `entry_sym == LINK_SYM_NONE`. +- In `link_emit_elf`, write `e_type = ET_DYN` for shared output and + `e_entry = 0` unless an explicit entry was intentionally accepted. +- Keep `ET_EXEC` and PIE executable behavior unchanged. +- Ensure link maps / symbol side-files tolerate no entry. + +Acceptance: + +- `kit ld -shared ...` emits an ELF header with `Type: DYN` and entry address + `0x0`. +- Missing `_start` is not a diagnostic for shared output. + +### 3. Program headers for DSOs + +- For shared output, do not synthesize `.interp` and do not emit `PT_INTERP`. + DSOs are loaded by another program's interpreter. +- Decide whether `PT_PHDR` is emitted for DSOs. It is valid and useful when the + phdr table is in a loadable segment, but it must not drag in executable-only + assumptions. +- Continue emitting `PT_DYNAMIC` and `PT_GNU_STACK`. +- Keep `PT_TLS` behavior if TLS sections are present, but gate unsupported TLS + reloc models before producing a bogus library. +- Preserve page/file congruence and the existing headers PT_LOAD behavior; do + not disturb scripted/freestanding output. + +Acceptance: + +- `readelf -l libfoo.so` shows `DYNAMIC` and no `INTERP`. +- Program headers' `p_vaddr % p_align == p_offset % p_align`. + +### 4. Dynamic section tags + +- Plumb `KitLinkSessionOptions.soname`, `rpaths`, `runpaths`, and `exports` + into `Linker` / `LinkDynState`. +- Add `DT_SONAME` when `-soname` is set. +- Add `DT_RPATH` or `DT_RUNPATH` entries according to the existing driver + `--enable-new-dtags` / `--disable-new-dtags` policy. +- Keep `DT_NEEDED` for DSO inputs already used by the link. +- Do not emit `DT_FLAGS_1 = DF_1_NOW` unconditionally for DSOs unless the link + explicitly requests now binding. For the MVP, either omit it or share the + current eager-binding behavior intentionally and document it. +- Rework `.dynstr` construction so soname/path strings are included during + layout, not appended at emit time. + +Acceptance: + +- `readelf -d` shows requested `SONAME`, `NEEDED`, and `RUNPATH`/`RPATH`. +- No warning from `src/api/link.c` for supported shared-library flags. + +### 5. Exported dynamic symbols + +- Define the default export set for shared output as all defined non-local, + non-hidden, non-section, non-file symbols. Preserve weak and protected + bindings correctly. +- Honor symbol visibility from the object model (`SV_DEFAULT`, `SV_HIDDEN`, + `SV_PROTECTED`) in `.dynsym.st_other`. +- Treat explicit `exports` as a later filtering/augmentation feature unless the + first implementation wires it completely; do not silently ignore it. +- Keep executable `--export-dynamic` separate from DSO exports. A shared library + is not an executable with `-E`. +- Ensure LTO preservation keeps exported DSO symbols alive. The existing + `KIT_LINK_OUTPUT_SHARED` preserve-all-nonlocal path in `src/api/link.c` is + the right starting point. + +Acceptance: + +- Hidden symbols are absent from `.dynsym` or marked non-preemptible as + appropriate; default-visible globals are exported. +- A consumer executable can resolve an exported function/data symbol from the + produced `.so`. + +### 6. Undefined symbols and imports + +- For shared output, unresolved global references should become dynsym imports + even when they were not satisfied by a link-time DSO input, unless the caller + requests `--no-undefined` / `-z defs`. +- Add a distinct "runtime import with no provider DSO" state or allow + `imported` symbols with `dso_input_id == LINK_INPUT_NONE`; make + `collect_needed` skip provider-less imports while still emitting dynsym and + relocations. +- Keep executable behavior strict by default: unresolved references in + executable links remain errors unless `allow_undefined` or an external + resolver applies. +- Support weak undefined symbols as zero/null without requiring dynamic + relocations where the psABI expects that behavior. + +Acceptance: + +- `kit ld -shared foo.o` can leave `bar` undefined and emits `bar` in + `.dynsym` as `UND`. +- A final executable link fails or succeeds according to its own unresolved + policy, independent of the library production step. + +### 7. Relocation legality and runtime relocations + +- Audit `apply_all_relocs` for shared-output semantics. For DSOs, text + relocations should be rejected unless an explicit textrel option exists. +- AArch64 MVP: + - calls/jumps to imported functions route through PLT and `R_AARCH64_JUMP_SLOT`; + - absolute data/function pointer slots against imports emit `GLOB_DAT`; + - absolute slots against internal non-ABS symbols emit `R_AARCH64_RELATIVE`; + - GOT entries for preemptible/default-visible definitions must be handled + correctly, not only undefined externals; + - direct `ADR_PREL_PG_HI21` / `ADD_ABS_LO12_NC` references to preemptible + default-visible DSO definitions must be rejected or emitted through a + DSO-safe form. +- Add a shared-output validation pass that catches reloc kinds the dynamic + loader cannot apply for DSOs. This should diagnose the input object and + symbol, not panic late in emit. +- Keep `R_*_COPY` out of DSO output. Copy relocs belong to executable links. + +Acceptance: + +- Non-PIC absolute/text reloc inputs fail with a clear diagnostic under + `-shared`. +- PIC object fixtures with function calls, data imports, function-pointer + initializers, and local address constants load and run through a consumer. + +### 8. AArch64 PIC codegen from kit + +- Validate that `-fPIC` sets `target.pic = KIT_PIC_PIC` for all compile paths + that can feed a shared link. +- Audit aarch64 backend symbol-address lowering: + - undefined extern data currently uses GOT via `obj_symbol_extern_via_got`; + - default-visible definitions in the same DSO may still be preemptible under + ELF interposition rules, so data accesses may also need GOT-indirect forms + unless hidden/protected or `-Bsymbolic` applies; + - direct calls can remain direct for local/hidden definitions but external + default-visible calls need PLT-compatible relocations when interposable. +- Make `KIT_CG_OUTPUT_SHARED` and `KIT_CG_INTERPOSITION_*` drive these choices + for both `-O0` and `-O1`. +- Keep local hidden/internal symbols direct so PIC does not pessimize + everything. + +Acceptance: + +- `kit cc -fPIC -c foo.c` produces relocations accepted by `kit ld -shared`. +- `kit cc -shared -fPIC foo.c` produces the same class of working `.so` as + linking equivalent external PIC objects. + +### 9. Driver behavior + +- Remove the `-shared` rejection in `driver/cmd/ld.c` after the linker support + is in place. +- Remove the `-shared` rejection in `driver/cmd/cc.c` only after kit-generated + PIC objects are safe; before that, `kit ld -shared` can be enabled while + `kit cc -shared` remains gated. +- For `kit cc -shared`, default compilation to `-fPIC` if the user did not + explicitly choose PIC/PIE/non-PIC. Reject `-shared -fno-pic`. +- Set default output names consistently (`a.out` is wrong for `-shared`; use + user `-o` or a library-shaped fallback only if the driver already has one). +- `-Wl,-soname` should require `-shared`, as it does now, but become useful. +- Decide and document `-nostdlib` / runtime archive behavior for shared + libraries. Avoid pulling executable startup objects into a DSO. +- Keep `build-lib -dynamic` gated until the lower-level `ld` and `cc` paths are + working; then route it through `KIT_LINK_OUTPUT_SHARED`. + +Acceptance: + +- The all-clang harness lane is green before either driver gate is removed. +- `kit ld -shared` is enabled after it passes with clang PIC objects. +- `kit cc -shared -fPIC` is enabled after kit-generated PIC objects pass the + same DSO fixture and no `_start` or crt executable startup is included. + +### 10. Test sequencing + +Prefer red-green targeted tests. Add small ELF/aarch64 cases before broad +matrix runs. The sequencing is part of the design: prove the test harness with +the system toolchain first, then replace one kit component at a time. + +- Build one reusable `test/link/elf-dso-aa64/` fixture or equivalent. It should + produce a small `libfoo.so` and a consumer executable that calls an exported + function, reads exported data, exercises an undefined import supplied by the + consumer or another DSO, and checks hidden/default visibility. The fixture has + three lanes: + - **Harness baseline, all clang:** clang compiles PIC objects, clang links + the shared library, and clang links the consumer. This verifies the source + fixture, sysroot, runtime loader, qemu/VM lane, `readelf` checks, and run + harness before kit is involved. + - **Linker swap, kit ld only:** clang still compiles all PIC objects, but + `kit ld -shared` links `libfoo.so`; clang or the system linker may still + link the consumer. This isolates ELF DSO production, dynamic tags, + exports/imports, PLT/GOT, and runtime relocations from kit codegen. + - **Compiler swap, all kit:** `kit cc -fPIC` compiles the library objects, + `kit ld -shared` links the DSO, and `kit cc`/`kit ld` links the consumer. + This is the first lane that validates kit-generated PIC and DSO + interposition choices. +- Each lane should run the same observable checks: + - header: ET_DYN, e_entry 0, no PT_INTERP, has PT_DYNAMIC; + - dynamic tags: SONAME, NEEDED, RUNPATH/RPATH; + - export set: default-visible exported, hidden not exported; + - undefined import allowed in DSO and appears in `.dynsym`; + - non-PIC relocation rejected with a precise diagnostic; + - DSO consumed by a small executable under the Linux/aarch64 lane. +- Keep object-level tests on the linker-swap lane until they are green; do not + debug kit codegen and ELF DSO layout in the same red test. +- Add kit-codegen tests for `kit cc -fPIC -c` relocation shape only after the + clang-object linker-swap lane is stable; then add `kit cc -shared`. +- Use targeted commands such as `make test-link TARGET=aarch64-linux` or the + closest existing selector. Redirect output to a file and inspect/tail that + file. +- Once aarch64 is stable, repeat the same fixtures for x86-64 and riscv64 only + after their PLT/GOT relocation coverage is audited. + +## Likely milestone order + +1. **All-clang harness baseline:** write the aarch64 shared-library fixture and + prove it with clang compile, clang `-shared`, clang/system consumer link, + `readelf` assertions, and runtime execution. No kit linker/codegen work is + considered validated until this lane is green. +2. **ELF linker skeleton:** internal shared mode, no entry, no interpreter, + `ET_DYN` DSO headers. +3. **Dynamic metadata:** SONAME/RPATH/RUNPATH plumbing and dynstr layout. +4. **Swap in `kit ld -shared`:** keep clang PIC library objects, but produce + the `.so` with kit. Make imports/exports, PLT/GOT, `DT_NEEDED`, and runtime + relocations work in this isolated lane. +5. **Diagnostics:** reject non-PIC/textrel inputs clearly under + `kit ld -shared`. +6. **Enable `kit ld -shared`:** remove the driver gate for object/archive/DSO + inputs once the clang-object linker-swap lane is green. +7. **Swap in kit codegen:** compile the same library sources with + `kit cc -fPIC`, audit/fix aarch64 interposition choices, and keep the DSO + linked by kit. This creates the all-kit library path. +8. **Enable `kit cc -shared`:** compile and link source to `.so` through the + driver, with no executable startup objects. +9. **All-kit consumer lane:** link the consumer with kit as well, so the full + compile/link/run path is kit-produced except for the host loader/sysroot. +10. **Broaden:** x86-64/riscv64, build-lib dynamic mode, and additional ELF + linker compatibility flags. + +## Open design decisions + +- Should shared links default to `--allow-shlib-undefined`, with `-z defs` / + `--no-undefined` as the strict mode? This matches common ELF behavior and + should be the default unless kit intentionally chooses stricter diagnostics. +- Should `DF_1_NOW` remain the default for DSOs? Current PIE executable output + uses eager binding. DSOs can work that way, but it is a policy choice rather + than a requirement. +- How much ELF interposition should codegen honor in v1? A conservative MVP can + require `-fvisibility=hidden` for kit-generated DSOs until default-visible + definition preemption is fully correct, but `kit ld -shared` for external PIC + inputs should not impose that restriction. +- Do we want a `-Bsymbolic` subset early? It can simplify same-DSO references, + but it should be an explicit semantic change, not a hidden implementation + shortcut. diff --git a/doc/plan/README.md b/doc/plan/README.md @@ -11,6 +11,7 @@ shrinks to whatever remains open (and is deleted once nothing remains open). | [RELEASE.md](RELEASE.md) | Cross-cutting initial-release punchlist: remaining release blockers and per-subsystem validation gaps (completed items are removed, not checked off). | — | | [OPTIMIZER.md](OPTIMIZER.md) | Completing the O2 SSA mid-end, live-range splitting/coalescing, the residual O1 generated-code-quality gaps that need SSA, -O0 quality, machine register-constraint improvements, and broader inlining. Carries the known non-deficiencies (what *not* to chase). | [../OPT.md](../OPT.md) | | [LINKER.md](LINKER.md) | Incremental linking (JIT append + the unbuilt file-based "m2" redesign) **and** system-linker compatibility (ordered DSO/`--as-needed`, ELF TLS planning + TLSDESC, shared/relocatable links, sysroot interop, toolchain validation). | [../LINK.md](../LINK.md), [../OBJ.md](../OBJ.md) | +| [DYNLIB.md](DYNLIB.md) | Producing ELF shared libraries, Linux/aarch64 first: splitting DSO output from PIE executables, dynamic tags, exports/imports, relocation legality, PIC codegen, driver enablement, and targeted tests. | [../LINK.md](../LINK.md), [../OBJ.md](../OBJ.md) | | [DEBUG.md](DEBUG.md) | The interactive JIT debugger + DWARF: x64/rv64 session parity, displaced-step instruction coverage, unit/smoke tests, REPL polish, Toy/C REPL frontends, and DWARF producer/consumer gaps (loclists, CFI register recovery, composite locations). | [../DBG.md](../DBG.md), [../DWARF.md](../DWARF.md) | | [PROF.md](PROF.md) | The not-yet-built host-native sampling profiler (`kit prof`): SIGPROF frame-pointer walk reusing the debugger's signal infrastructure, the `KitProfBuf`/`KitProfWriter` API, and folded/flat output. Complementary to (not rebased on) the emulator callgrind in INSTRUMENT.md. | [../DBG.md](../DBG.md) | | [INSTRUMENT.md](INSTRUMENT.md) | Debugging/profiling tools in the Valgrind/callgrind/asan/ubsan tradition: the emulator as a dynamic-binary-instrumentation substrate (the `EmuToolHooks` ABI, shadow planes, the memory seam, a guest debugger), plus the cross-frontend compiler-sanitizer story. | [../EMU.md](../EMU.md) |