diff --git a/CLAUDE.md b/CLAUDE.md index 7542bc46f34..676cd75cfe3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -59,7 +59,6 @@ There are no spec documents. A rule a reader can check lives in an agent's promp Vendor firmware a device or CPU verifies by its maker's signature may be shipped: pinned by version and hash, redistributable unmodified, recorded in `NOTICE`. A device's is loaded only by its own driver through its IOMMU domain and never executes on the CPU; CPU microcode is loaded by the kernel. `NOTICE` names every committed third-party file with its hash, upstream and licence. -- **toyos-ld** — frozen: everything links with rust-lld, and toyos-ld stays only as the linker inside ToyOS until lld runs there, then goes. - **rust/** — Rust compiler/std fork with ToyOS platform support (submodule). Auto-bootstraps; kept current with upstream. Its rules: `.claude/agents/implementer.md`, "A fork". ## Build & test diff --git a/Cargo.lock b/Cargo.lock index 4fa74723474..89524a21729 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1787,9 +1787,6 @@ version = "0.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271638cd5fa9cca89c4c304675ca658efc4e64a66c716b7cfe1afb4b9611dbbc" dependencies = [ - "crc32fast", - "hashbrown 0.16.1", - "indexmap", "memchr", ] @@ -2513,17 +2510,6 @@ dependencies = [ name = "toyos-keymap" version = "0.1.0" -[[package]] -name = "toyos-ld" -version = "0.3.0" -dependencies = [ - "object", - "sha2", - "thiserror 2.0.18", - "toyos-tmpdir", - "zerocopy", -] - [[package]] name = "toyos-libc-copies" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 67651bf7ab0..7a5128a7935 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,7 +42,6 @@ members = [ "toyos-i219", "toyos-inspect", "toyos-keymap", - "toyos-ld", "toyos-logstream", "toyos-manifest", "toyos-microcode", @@ -234,40 +233,6 @@ opt-level = 2 [profile.dev] opt-level = 2 -[profile.dev.package.toyos-ld] -opt-level = 3 - -# The one profile every guest binary is built with. Optimised, because an -# unoptimised guest mismeasures everything under TCG; -# debug-assertions and overflow-checks on, because fail-fast beats speed here. -# `--release` would turn the last two off silently. -# -# It is declared here rather than in `toyos-ld/Cargo.toml` because cargo -# ignores `[profile]` in a workspace member and that is one now. It is the only -# member the profile reaches: it is a `[programs]` entry in `system.toml` as -# well as a host crate, so `src/build.rs` builds it a second time for -# `x86_64-unknown-toyos`. `kernel/`, `bootloader/` and `userland/` are excluded -# from this workspace and each declares its own copy. -# -# `incremental` stays at the dev default (on), which switches rustc's MIR -# inliner off for every guest crate — LLVM inlines regardless, and a -# non-incremental kernel edit loop measures 4-10x the wall clock. A codegen -# backend with no inliner of its own (cranelift) reopens that trade. -[profile.toyos] -inherits = "dev" -opt-level = 2 -debug = true -# DWARF stays out of the image: symbols name a frame, and `debug = true` is kept -# for the codegen it decides. -strip = "debuginfo" -debug-assertions = true -overflow-checks = true - -# Cargo does not carry the profile's `strip` onto a package with an override of -# its own, and this one has one in `dev`. -[profile.toyos.package.toyos-ld] -strip = "debuginfo" - [[test]] name = "toyos-build" path = "tests/toyos.rs" diff --git a/README.md b/README.md index 76b3e91d0f7..7fd2274effe 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # ToyOS An operating system written from scratch in Rust — bootloader, kernel, drivers, -userland, and the compiler and linker that build them. +userland, and the compiler that builds them. ``` git clone https://github.com/Japabu/toyos @@ -167,8 +167,7 @@ TinyCC's expectations. **LLVM's linker.** `rust-lld`, the one the Rust toolchain carries, links everything that runs on ToyOS, plus everything that runs before it: the UEFI bootloader as PE32+, the kernel and every userland program as -position-independent ELF. `toyos-ld`, the linker this project wrote, is frozen: -it is the one linker that runs inside ToyOS, until LLD does. +position-independent ELF. **A real Rust target.** `x86_64-unknown-toyos` lives in ToyOS's fork of the compiler, with a prebuilt `std` in the sysroot. One `rustc` invocation turns an diff --git a/issues/anonymous-mmap-is-not-demand-paged.md b/issues/anonymous-mmap-is-not-demand-paged.md index 91029c7e12f..399e9071eb6 100644 --- a/issues/anonymous-mmap-is-not-demand-paged.md +++ b/issues/anonymous-mmap-is-not-demand-paged.md @@ -4,11 +4,11 @@ kind: defect opened: 2026-08-08 --- -# Anonymous `mmap` is not demand-paged, and `.bss` is written into the file +# Anonymous `mmap` is not demand-paged -Two claims this tree makes about itself that its own code does not keep. Found +A claim this tree makes about itself that its own code does not keep. Found by `wt/toyos-fpu` while building a page-fault workload for `fpu_isolation`, and -recorded rather than fixed: both are memory-subsystem changes with their own +recorded rather than fixed: it is a memory-subsystem change with its own blast radius. **`sys_mmap` allocates and maps the whole region up front.** `PageAlloc::new` is @@ -23,15 +23,6 @@ of nothing else. Whether it should be lazy is a design question with a real answer either way — the eager path is simpler and cannot fail late — but the description and the code should agree. -**`toyos-ld` materializes `.bss` into the file.** A `static mut [u8; 16 MiB]` -that is all zeroes produced an 18 MB binary, and a 128 MiB one produced a -135 MB binary and a 465 MB initrd; both are in the same run's `initrd: adding` -lines. A `PT_LOAD`'s `filesz` therefore equals its `memsz`, which also means the -loader's `Anonymous` tail (`loader/mod.rs`, `file_backed_end < seg_end`) is -always empty — that branch exists and nothing in the tree can reach it. Zeroed -pages a linker never has to write are the whole point of `.bss`, and this is -paid in image size, in initrd size, and in every copy of both. - The one demand-paged thing a userland program can still reach is a *writable file-backed* page — `demand_paging_sse` and `fpu_isolation` both use one — at 2 MiB of test image per fault. diff --git a/issues/cargo-package-fails-in-a-linked-worktree.md b/issues/cargo-package-fails-in-a-linked-worktree.md index df886e46a30..0e5aacaf917 100644 --- a/issues/cargo-package-fails-in-a-linked-worktree.md +++ b/issues/cargo-package-fails-in-a-linked-worktree.md @@ -6,7 +6,7 @@ opened: 2026-09-26 # `cargo package`/`cargo publish --dry-run` cannot run inside a linked worktree -`cargo package -p ` (tried `toyos-abi` and `toyos-ld`) +`cargo package -p ` (tried `toyos-abi`) reds with a bare `error: No such file or directory (os error 2)` in a linked worktree, right after cargo's own trace logs `found a git repo` and `found (git) Cargo.toml`, inside diff --git a/issues/clippy-stage-two-is-lints-one-at-a-time.md b/issues/clippy-stage-two-is-lints-one-at-a-time.md index c2d86736172..2469ae0068f 100644 --- a/issues/clippy-stage-two-is-lints-one-at-a-time.md +++ b/issues/clippy-stage-two-is-lints-one-at-a-time.md @@ -36,7 +36,7 @@ Measuring the full `pedantic` + `nursery` groups (`-W` only, not gated) across the host workspace, kernel and bootloader found **~9,000 findings** across ~90 distinct lints (up from the 1,684-on-the-kernel-alone figure this entry carried before — that number was kernel only; the host workspace, including -`toyos-ld` and `toyos-cc`, carries most of the rest). Six lints earned a +`toyos-cc`, carries most of the rest). Six lints earned a place in the gate. Everything else stayed off, each for a reason below. ## Adopted @@ -108,7 +108,7 @@ before adopting: `toyos-gpt/src/lib.rs`'s three-clause partition-bounds check (`partition.first_lba > partition.last_lba || partition.first_lba < header.first_usable_lba || partition.last_lba > header.last_usable_lba`) - and `toyos-ld/src/emit_macho.rs`'s Mach-O header-size sum both look like + and a since-deleted linker's Mach-O header-size sum both look like copy-paste bugs to the lint's cross-clause heuristic and are correct as written — verified against what each is actually checking, not assumed. - **`large_stack_arrays` / `large_stack_frames` (4 findings)**: flagged a diff --git a/issues/debug-true-produces-no-debug-info.md b/issues/debug-true-produces-no-debug-info.md index 18a6582acbb..04d6712f217 100644 --- a/issues/debug-true-produces-no-debug-info.md +++ b/issues/debug-true-produces-no-debug-info.md @@ -6,19 +6,17 @@ opened: 2026-08-07 # `debug = true` produces no debug info, because the linker drops it -`toyos-ld` matches `SectionKind::Debug | DebugString | Linker | Note | Metadata` -and `continue`s (`collect.rs:410-416`), so **no binary this project produces has -a `.debug_*` section**. Verified with `readelf -S` on the kernel, the compositor -and toybox: the sections are `.text .strtab .symtab .rela.dyn .data -.eh_frame_hdr .dynamic .shstrtab` and nothing else. +Every `[profile.toyos]` but the loader's sets `strip = "debuginfo"`, and +`readelf -SW` on the x86-64 kernel, compositor and toybox finds **no DWARF +section** in any of the three: no `.debug_*` section but rustc's +`.debug_gdb_scripts`, which is no DWARF. `[profile.toyos]` states `debug = true` in every crate root, so rustc emits DWARF into every object file and the linker throws all of it away. The cost is compile time and has not been measured. The consequence for diagnostics is that a backtrace can carry a **name** and never a line number or an inlined frame, on any path — `.symtab`/`.strtab` is the whole of what survives, and it is 32.2% of -the 92,138,384 bytes of ELF this tree ships. Keeping `.debug_line` in `toyos-ld` -is what would change that, and it is not planned. +the 92,138,384 bytes of ELF this tree ships. **2026-08-29: the cost measured, and the cheap exit measured shut.** Cold guest build (`cargo run -- --build-only`, kernel+bootloader+userland+tests @@ -30,5 +28,5 @@ differently, and a kernel-only A/B shows `.data` +0x50, `.rela.dyn` +0x48 (three relocations) and a moved `.text` tail — debuginfo changes rustc's codegen, not just the metadata. So turning it off ships different bytes on every binary, and the choice between paying the 27%, shipping the -debuginfo-free codegen, and teaching `toyos-ld` to keep `.debug_line` is one +debuginfo-free codegen, and keeping `.debug_line` is one decision, not a cleanup. diff --git a/issues/root-in-memory-costs-its-whole-size.md b/issues/root-in-memory-costs-its-whole-size.md index 73b511261a1..6f1f8cbdcb4 100644 --- a/issues/root-in-memory-costs-its-whole-size.md +++ b/issues/root-in-memory-costs-its-whole-size.md @@ -19,10 +19,7 @@ audio run carries its one binary, and the shared list runs on boots of one lane in turn, each carrying at most `SHARED_BOOT_BYTES` (64 MiB) of test binaries; `qemu::carrying` adds what those binaries spawn or load. A boot that runs none carries `tests/testcases`'s own programs, 13 MiB (3328 blocks), where every test -boot carried the whole 619 MiB catalogue before. The one part past the bound is -`demand_window_race`, 131 MiB, nearly all of it the `.bss` zeros `toyos-ld` -writes into the file (`issues/anonymous-mmap-is-not-demand-paged.md`), -which a guest that mounts ROOT off a disk never reads and a guest here holds. +boot carried the whole 619 MiB catalogue before. **Every resident `/system` page is in RAM twice.** `ReadOnlyBacking::read_page` (`kernel/src/file_backing.rs`) copies each page a program touches out of the @@ -49,7 +46,6 @@ complete K ms`; a test boot on QEMU reads 1390, 108 (14) and 344. Not measured: the T14. What that run owes is that line off the stick and off NVMe, against the base's `Boot: complete`. -Exit: either measured and accepted by the owner as the price, or `.bss` left -out of the file by `toyos-ld` and the file cache serving the image's own page -rather than a copy, before the self-update's signature check makes the whole +Exit: either measured and accepted by the owner as the price, or the file +cache serving the image's own page rather than a copy, before the self-update's signature check makes the whole read a fixed cost. diff --git a/issues/the-hosted-rustc-names-a-linker-toyos-does-not-have.md b/issues/the-hosted-rustc-names-a-linker-toyos-does-not-have.md index 192cc12fed5..df4c842d09e 100644 --- a/issues/the-hosted-rustc-names-a-linker-toyos-does-not-have.md +++ b/issues/the-hosted-rustc-names-a-linker-toyos-does-not-have.md @@ -8,9 +8,8 @@ opened: 2026-09-26 `x86_64-unknown-toyos` names `rust-lld`, and the rustc `src/toolchain.rs` builds for a ToyOS host carries that target spec into the guest, where no -`rust-lld` exists: LLD runs on ToyOS only once clang and libc++ do. The linker -that does run there is the frozen `/system/bin/toyos-ld`, which that rustc -reaches only when told `-C linker=toyos-ld`. No image ships the hosted rustc +`rust-lld` exists: LLD runs on ToyOS only once clang and libc++ do, and no +other linker runs there. No image ships the hosted rustc today — `src/build.rs` refuses `hosted-rustc = true` until its licences are read — so nothing links through it yet. diff --git a/issues/the-split-window-tlb-cost-is-unpriced.md b/issues/the-split-window-tlb-cost-is-unpriced.md index 1a9b22576f8..e214452a904 100644 --- a/issues/the-split-window-tlb-cost-is-unpriced.md +++ b/issues/the-split-window-tlb-cost-is-unpriced.md @@ -26,7 +26,7 @@ The instrument is the T14 with `--metal-sim`'s real shape and a workload that loops across the boundary, comparing against a kernel built before this change. Nothing depends on the answer today: a bad one does not send the design back to 2 MiB granularity, since 2 MiB granularity does not protect anything. It would -send it to 2 MiB-aligning `toyos-ld`'s segments, which was measured at +4 MiB of +send it to 2 MiB-aligning the linker's segments, which was measured at +4 MiB of physical memory per process. **2026-08-25, promoted to `defect`.** Checked at the site: `WindowProt`'s doc diff --git a/issues/the-sysroots-core-and-alloc-carry-the-compiler-profiles-debug-assertions.md b/issues/the-sysroots-core-and-alloc-carry-the-compiler-profiles-debug-assertions.md index ddd545139be..2034a36b2eb 100644 --- a/issues/the-sysroots-core-and-alloc-carry-the-compiler-profiles-debug-assertions.md +++ b/issues/the-sysroots-core-and-alloc-carry-the-compiler-profiles-debug-assertions.md @@ -19,8 +19,7 @@ passes it to the library crates' cargo profile are built without. The libraries' assertions were never chosen: `std_config`'s doc comment takes the profile so the libraries are built as the compiler was. The guest profile keeps debug assertions on in ToyOS's own code "because -fail-fast beats speed here" (`kernel/Cargo.toml`, `userland/Cargo.toml`, the -root `Cargo.toml`); whether the libraries should too is the decision nobody +fail-fast beats speed here" (`kernel/Cargo.toml`, `userland/Cargo.toml`); whether the libraries should too is the decision nobody made. So every kernel and program ships `alloc`'s and `core`'s `debug_assert!`s, diff --git a/issues/the-tooling-is-a-review-prompt-and-three-workflows.md b/issues/the-tooling-is-a-review-prompt-and-three-workflows.md index e79d36b56f6..f64680b6482 100644 --- a/issues/the-tooling-is-a-review-prompt-and-three-workflows.md +++ b/issues/the-tooling-is-a-review-prompt-and-three-workflows.md @@ -30,8 +30,8 @@ and the gates that held them go. - `kernel/`, `bootloader/` and `userland/` inherit a redirected `build.target-dir` unless their `.cargo/config.toml` sets `target-dir = "target"`. - - `stage_artifact` in `src/build.rs` builds its path outside - `hostws::target_dir`. + - `stage_artifact` in `src/build.rs` writes `root/target/…` whatever + `build.target-dir` redirects. - `cargo clean` follows a shared target dir. - Artifact size, bootstrap delta and CI cache keys are unmeasured. - The nine workflows become three — `pr`, `nightly`, `publish`; then diff --git a/issues/toyos-builds-itself.md b/issues/toyos-builds-itself.md index 7c603e26e0c..1172af237d2 100644 --- a/issues/toyos-builds-itself.md +++ b/issues/toyos-builds-itself.md @@ -13,12 +13,10 @@ with lld, one build of one fork, `ToyOSOrg/llvm-project`. The C library stays `userland/libc`, ours. Each stage lands on x86-64 first and on AArch64 one step behind, on `issues/toyos-runs-on-arm64.md`'s track. -- **M2 — clang and lld as a package inside ToyOS; toyos-ld gone.** clang, lld +- **M2 — clang and lld as a package inside ToyOS.** clang, lld and their runtime built *for* ToyOS on the host and installed by `/system/bin/pkg`; `clang hello.c && ./a.out` works in the guest. *Exit*: - the in-guest compile-and-run test passes, and toyos-ld — today the only - linker a ToyOS process can run — is deleted with its crate, its - `[programs]` row and its tests. + the in-guest compile-and-run test passes. - **M3 — an LLVM-backed rustc inside ToyOS.** The hosted rustc carries LLVM instead of Cranelift. *Exit*: a Rust program compiled and run inside ToyOS by the hosted rustc. @@ -114,13 +112,12 @@ enough for an LLVM build tree memory beyond what 2 MiB process pages allow (`issues/process-memory-is-2-mib-pages-and-that-caps-the-process-count.md`). -**What M2 must do to delete toyos-ld.** It is frozen and links nothing the host -builds; what keeps it is that it is the one linker a ToyOS process can run, -shipped as `/system/bin/toyos-ld` by `system.toml`'s `[programs]` row and named -by the ToyOS-hosted rustc -(`issues/the-hosted-rustc-names-a-linker-toyos-does-not-have.md`). It -goes when lld runs in the guest: the row, the crate and its host tests go together, the hosted rustc names `rust-lld`, and -the published crates.io crate is yanked. +**No linker runs inside ToyOS** (owner, 2026-10-04: "Just get rid of toyos +ld"). `/system/bin/toyos-ld` went with its crate, its `[programs]` row and its +host tests, and nothing replaced it: a ToyOS process cannot link an object, and +the ToyOS-hosted rustc names no linker the guest has +(`issues/the-hosted-rustc-names-a-linker-toyos-does-not-have.md`). *Exit*: the +commit that lands M2's compile-and-run test, which links inside the guest. **What stops M2: LLVM, clang and lld built for a ToyOS host**, in the order each blocks the next, as @@ -136,9 +133,7 @@ measures it. - Test: `issues/a-worktree-cannot-build-a-hosted-rustc-of-its-own.md`. M3's exit then waits on a linker in the guest -(`issues/the-hosted-rustc-names-a-linker-toyos-does-not-have.md`), which -toyos-ld is not: it refuses every executable with thread-local storage, so -every std program. +(`issues/the-hosted-rustc-names-a-linker-toyos-does-not-have.md`). **Also owed, by milestone.** - M2: the host triple a ToyOS-hosted LLVM records diff --git a/issues/toyos-has-no-c-compiler-inside-it.md b/issues/toyos-has-no-c-compiler-inside-it.md index 37ee0646707..46943c89db1 100644 --- a/issues/toyos-has-no-c-compiler-inside-it.md +++ b/issues/toyos-has-no-c-compiler-inside-it.md @@ -7,13 +7,12 @@ opened: 2026-09-27 # ToyOS has no C compiler inside it Until 2026-09-27 every image carried `/system/bin/toyos-cc`, a C compiler that -ran on ToyOS, beside `/system/bin/toyos-ld`. clang replaced toyos-cc on the host +ran on ToyOS. clang replaced toyos-cc on the host and toyos-cc was deleted with its `system.toml` row, so a C program can be compiled *for* ToyOS and not *on* it. Nothing in the suite ran the in-guest compiler, so no test went with it. -`toyos-ld` is still in the image, frozen, and links objects a ToyOS process -already has; nothing in the image makes one from C. +Nothing in the image makes an object from C, nor links one. **Exit**: the self-hosting track's M2 (`issues/toyos-builds-itself.md`) — clang and lld installed as a package, and a guest test that compiles `hello.c` diff --git a/issues/toyos-runs-on-arm64.md b/issues/toyos-runs-on-arm64.md index 15795dda100..3e1b388235b 100644 --- a/issues/toyos-runs-on-arm64.md +++ b/issues/toyos-runs-on-arm64.md @@ -138,11 +138,7 @@ across `src/` and `tests/`. The harness hardwires q35 (`tests/common/qemu.rs:4246-4266`). `kvm_usable()` is `cfg!(target_arch = "x86_64") && …` (`src/lib.rs:65`). The rust fork has only `x86_64_unknown_toyos.rs`; its base `base/toyos.rs` is arch-neutral. -`toyos-ld` already parses and applies AArch64 relocations for its Mach-O host -output (`collect.rs` 80 `Aarch64` mentions, `reloc.rs` 69) but hardwires -`EM_X86_64` (`emit_elf.rs:1021`), `R_X86_64_RELATIVE` -(`emit_elf.rs:1228,1313,1399`) and `IMAGE_FILE_MACHINE_AMD64` -(`emit_pe.rs:143`) on output, and has no AArch64 TLS relocations. `toyos-elf` +`toyos-elf` refuses anything but `EM_X86_64` (`toyos-elf/src/header.rs:24,75`). **Already abstracted.** The syscall stub already has both arms @@ -168,7 +164,7 @@ most of `arch/tlb.rs`'s 303-line IPI shootdown machinery unnecessary — a contract change, not a port); the memory model (the 711 `Relaxed` orderings and every doorbell-after-descriptor site — undiscovered TSO reliance is the one cost nobody can estimate from a grep); userland TLS and the toolchain -(variant I and TLSDESC across `toyos-ld`, the kernel loader and std); the test harness (33,907 lines in +(variant I and TLSDESC across the kernel loader and std); the test harness (33,907 lines in `tests/common/`, written against q35, i8042, OVMF, KVM, `intel-iommu`). ## Sharing, not forking diff --git a/kernel/Cargo.toml b/kernel/Cargo.toml index 95ad2450333..5af70ccd801 100644 --- a/kernel/Cargo.toml +++ b/kernel/Cargo.toml @@ -470,6 +470,11 @@ heap-lockspin = ["pass-spin"] # Optimised, because an unoptimised guest mismeasures everything under TCG; # debug-assertions and overflow-checks on, because fail-fast beats speed here. # `--release` would turn the last two off silently. +# +# `incremental` stays at the dev default (on), which switches rustc's MIR +# inliner off for every guest crate — LLVM inlines regardless, and a +# non-incremental kernel edit loop measures 4-10x the wall clock. A codegen +# backend with no inliner of its own (cranelift) reopens that trade. [profile.toyos] inherits = "dev" opt-level = 2 diff --git a/src/build.rs b/src/build.rs index 3a6c2ad514a..832cfe5b4ab 100644 --- a/src/build.rs +++ b/src/build.rs @@ -142,8 +142,6 @@ struct AppsConfig { #[derive(Deserialize, Default)] #[serde(default, rename_all = "kebab-case", deny_unknown_fields)] struct ProgramConfig { - path: Option, - no_default_features: bool, /// Argv this program is started with, after argv[0]. args: Vec, /// Names the supervisor creates **one machine-wide port** for and endows this program @@ -177,26 +175,9 @@ struct ProgramConfig { login: bool, } -impl ProgramConfig { - /// Resolve the crate directory for this program. - /// Defaults to `userland/` if no explicit path is set. - fn crate_dir(&self, root: &Path, name: &str) -> PathBuf { - match &self.path { - Some(p) => root.join(p), - None => root.join("userland").join(name), - } - } - - /// Whether this program is a member of the **userland** workspace, the one - /// `-p` selects a package from and whose `target/` holds the result. - /// Programs with explicit paths or special flags are built from their own - /// directory instead — which is not the same as being built into it: - /// `toyos-ld` has an explicit path and is a member of the *host* - /// workspace, so cargo writes it to the repository root's `target/`. - /// `hostws::target_dir` is what answers that, never this. - fn is_workspace_member(&self) -> bool { - self.path.is_none() && !self.no_default_features - } +/// The crate directory of the program `name`. +fn program_dir(root: &Path, name: &str) -> PathBuf { + root.join("userland").join(name) } fn parse_config(path: &Path) -> SystemConfig { @@ -208,37 +189,21 @@ fn parse_config(path: &Path) -> SystemConfig { // --- Freshness checking --- -/// How much of a crate's target directory goes when the compiler moves. -#[derive(Clone, Copy)] -enum Clean { - All, - /// A crate with an explicit path (toyos-ld) is a host-workspace member, - /// and the workspace's `target/` holds the build system's own host build - /// beside it, which must survive; so what this empties is what the guest - /// build wrote there: `target/` for every architecture, and - /// `target/`, its host half — proc-macros and their rlibs, which the - /// sysroot's compiler built and cargo, keying them on a `rustc -vV` every - /// ToyOS compiler prints alike, would keep for the next one. - ToyosOnly, -} - -fn stale(root: &Path, crate_dir: &Path, identity: &Identity) -> Option { - let stamp = hostws::target_dir(root, crate_dir).join(".deps-stamp"); +fn stale(crate_dir: &Path, identity: &Identity) -> Option { + let stamp = crate_dir.join("target/.deps-stamp"); identity.stale(fs::read_to_string(&stamp).ok().as_deref()) } -fn clean(root: &Path, crate_dir: &Path, kind: Clean, stale: &Stale, identity: &Identity) { - // Where cargo actually wrote it. `toyos-ld` is a member of the host - // workspace, so its guest build lands in the root's `target/`. - let target = hostws::target_dir(root, crate_dir); +fn clean(root: &Path, crate_dir: &Path, stale: &Stale, identity: &Identity) { + let target = crate_dir.join("target"); let remove = |dirs: &mut dyn Iterator| { for dir in dirs.filter(|dir| dir.exists()) { eprintln!("external deps changed: cleaning {}", dir.display()); crate::keystore::remove(&dir); } }; - match (stale, kind) { - (Stale::All, Clean::All) => { + match stale { + Stale::All => { // `cargo clean` in a member's directory cleans the whole workspace, // this build system's own target directory included. Nothing asks // for that today; refusing it by name is cheaper than finding out. @@ -256,11 +221,7 @@ fn clean(root: &Path, crate_dir: &Path, kind: Clean, stale: &Stale, identity: &I .unwrap_or_else(|e| panic!("run cargo clean in {}: {e}", crate_dir.display())); assert!(status.success(), "cargo clean in {} exited {status}", crate_dir.display()); } - (Stale::All, Clean::ToyosOnly) => { - let guest = Arch::ALL.iter().map(|arch| target.join(arch.userland())); - remove(&mut guest.chain([target.join(PROFILE)])); - } - (Stale::Targets(moved), _) => remove(&mut moved.iter().map(|t| target.join(t))), + Stale::Targets(moved) => remove(&mut moved.iter().map(|t| target.join(t))), } fs::create_dir_all(&target).unwrap_or_else(|e| panic!("create {}: {e}", target.display())); @@ -280,37 +241,33 @@ fn invalidate_stale( root: &Path, lock: &mut buildlock::Held, identity: &Identity, - targets: &[(PathBuf, Clean)], + targets: &[PathBuf], ) { lock.act_if( buildlock::Scope::Worktree, "clean crate targets against changed external deps", || { - let work: Vec<(PathBuf, Clean, Stale)> = targets - .iter() - .filter_map(|(dir, kind)| stale(root, dir, identity).map(|s| (dir.clone(), *kind, s))) - .collect(); + let work: Vec<(PathBuf, Stale)> = + targets.iter().filter_map(|dir| stale(dir, identity).map(|s| (dir.clone(), s))).collect(); (!work.is_empty()).then_some(work) }, |work| { - for (dir, kind, stale) in work { - clean(root, &dir, kind, &stale, identity); + for (dir, stale) in work { + clean(root, &dir, &stale, identity); } }, ); } -/// Every target directory a config builds into, and how much of each goes when -/// stale. -fn config_targets(root: &Path, config: &SystemConfig) -> Vec<(PathBuf, Clean)> { - let mut targets: Vec<(PathBuf, Clean)> = Vec::new(); +/// Every crate directory whose target directory a config builds into. +fn config_targets(root: &Path, config: &SystemConfig) -> Vec { + let mut targets: Vec = Vec::new(); for c in config_crates(root, config) { let target = match c.built { - Built::Kernel | Built::Bootloader => (c.dir, Clean::All), - Built::Member => (root.join("userland"), Clean::All), - Built::Standalone => (c.dir, Clean::ToyosOnly), + Built::Kernel | Built::Bootloader => c.dir, + Built::Member => root.join("userland"), }; - if !targets.iter().any(|(dir, _)| *dir == target.0) { + if !targets.contains(&target) { targets.push(target); } } @@ -322,8 +279,6 @@ fn config_targets(root: &Path, config: &SystemConfig) -> Vec<(PathBuf, Clean)> { pub enum Features { /// Its manifest's defaults. Default, - /// None of its defaults: a `[programs]` row's `no-default-features`. - NoDefault, /// Its defaults and these, comma-separated. With(&'static str), /// Any it declares, since the command line picks: the kernel's, which @@ -336,7 +291,6 @@ impl Features { pub fn args(self) -> Vec<&'static str> { match self { Features::Default => vec![], - Features::NoDefault => vec!["--no-default-features"], Features::With(names) => vec!["--features", names], Features::AnyDeclared => vec!["--all-features"], } @@ -350,8 +304,6 @@ enum Built { Bootloader, /// A package of the userland workspace, built there with `-p`. Member, - /// A program built in its own directory. - Standalone, } /// One crate a config's image is built from. @@ -379,17 +331,17 @@ fn config_crates(root: &Path, config: &SystemConfig) -> Vec { features: Features::Default, }, ]; - for (name, cfg) in &config.programs { + for name in config.programs.keys() { crates.push(ConfigCrate { name: name.clone(), - dir: cfg.crate_dir(root, name), - built: if cfg.is_workspace_member() { Built::Member } else { Built::Standalone }, - features: if cfg.no_default_features { Features::NoDefault } else { Features::Default }, + dir: program_dir(root, name), + built: Built::Member, + features: Features::Default, }); } crates.push(ConfigCrate { name: SUPERVISOR_PROGRAM.to_string(), - dir: ProgramConfig::default().crate_dir(root, SUPERVISOR_PROGRAM), + dir: program_dir(root, SUPERVISOR_PROGRAM), built: Built::Member, features: Features::Default, }); @@ -891,7 +843,7 @@ fn build_programs( let programs: Vec = config_crates(root, config) .into_iter() - .filter(|c| matches!(c.built, Built::Member | Built::Standalone)) + .filter(|c| c.built == Built::Member) .filter(|c| match not_built_for(arch, &c.name) { Some(why) => { eprintln!("{}: not built for {}, and not on this ROOT: {why}", c.name, arch.name()); @@ -908,8 +860,7 @@ fn build_programs( c.dir.display() ); } - let workspace_packages: Vec<&str> = - programs.iter().filter(|c| c.built == Built::Member).map(|c| c.name.as_str()).collect(); + let workspace_packages: Vec<&str> = programs.iter().map(|c| c.name.as_str()).collect(); let ws_target = userland_dir.join(format!("target/{target}/{PROFILE}")); @@ -935,20 +886,10 @@ fn build_programs( cargo_build(&userland_dir, target, &extra, env, &cc_env, quiet); } - for c in programs.iter().filter(|c| c.built == Built::Standalone) { - cargo_build(&c.dir, target, &c.features.args(), env, &cc_env, quiet); - } - for c in &programs { let name = &c.name; - let binary = match c.built { - Built::Member => ws_target.join(name), - Built::Standalone => { - hostws::target_dir(root, &c.dir).join(format!("{target}/{PROFILE}/{name}")) - } - Built::Kernel | Built::Bootloader => unreachable!("filtered out above"), - }; - let data = fs::read(&binary).unwrap_or_else(|_| panic!("Failed to read binary for {name}")); + let data = + fs::read(ws_target.join(name)).unwrap_or_else(|_| panic!("Failed to read binary for {name}")); root_files.push((format!("bin/{name}"), data)); } } @@ -1183,8 +1124,9 @@ impl Boot { /// is a test image and is not here. pub struct Shipped { pub crates: BTreeSet<(PathBuf, Features)>, - /// `crates` but the kernel and the loader: every program an image runs. - pub programs: BTreeSet<(PathBuf, Features)>, + /// The directories of `crates` but the kernel and the loader: every + /// program an image runs. + pub programs: BTreeSet, pub assets: BTreeSet, } @@ -1205,8 +1147,8 @@ pub fn shipped(root: &Path) -> Result { )); } for c in config_crates(root, &config) { - if matches!(c.built, Built::Member | Built::Standalone) { - programs.insert((c.dir.clone(), c.features)); + if c.built == Built::Member { + programs.insert(c.dir.clone()); } crates.insert((c.dir, c.features)); } @@ -2166,11 +2108,11 @@ fn host_judge(root: &Path, (dir, bin): Judge) -> PathBuf { /// and over the name of whatever gets it next. pub fn build_toyos_bins(root: &Path, arch: Arch, crate_path: &Path, quiet: bool) -> Vec<(String, Vec)> { let _building = Building::start(format!("{} binaries of {}", arch.name(), hostws::rel(root, crate_path))); - let mut targets = vec![(crate_path.to_path_buf(), Clean::All)]; + let mut targets = vec![crate_path.to_path_buf()]; for entry in fs::read_dir(crate_path).into_iter().flatten().flatten() { let sub_path = entry.path(); if sub_path.is_dir() && sub_path.join("Cargo.toml").exists() { - targets.push((sub_path, Clean::All)); + targets.push(sub_path); } } let build = TestBuild::begin(root, arch, "test binaries", &targets); @@ -2269,7 +2211,7 @@ struct TestBuild { } impl TestBuild { - fn begin(root: &Path, arch: Arch, what: &str, stale_targets: &[(PathBuf, Clean)]) -> Self { + fn begin(root: &Path, arch: Arch, what: &str, stale_targets: &[PathBuf]) -> Self { let mut lock = buildlock::shared(root, what); let env = GuestEnv::new(toolchain::ensure(root, &mut lock, false)); invalidate_stale(root, &mut lock, &env.sysroot.identity, stale_targets); @@ -2282,7 +2224,7 @@ impl TestBuild { /// architecture the crate's other binaries do not all build for. pub fn build_toyos_bin(root: &Path, arch: Arch, crate_path: &Path, name: &str, quiet: bool) -> Vec { let _building = Building::start(format!("{} {name} of {}", arch.name(), hostws::rel(root, crate_path))); - let build = TestBuild::begin(root, arch, "a test binary", &[(crate_path.to_path_buf(), Clean::All)]); + let build = TestBuild::begin(root, arch, "a test binary", &[crate_path.to_path_buf()]); let (target, env) = (build.target, &build.env); cargo_build(crate_path, target, &["--bin", name], env, &[], quiet); let binary = crate_path.join(format!("target/{target}/{PROFILE}/{name}")); @@ -2427,35 +2369,8 @@ mod tests { ); } let (kernel, loader) = (root.join("kernel"), root.join("bootloader")); - let programs = shipped.crates.iter().filter(|(dir, _)| *dir != kernel && *dir != loader); - assert_eq!(shipped.programs, programs.cloned().collect()); - } - - /// **A standalone crate's clean takes all its guest build wrote — the - /// host half too, whose proc-macros and rlibs the sysroot's compiler built - /// — and nothing the host workspace built.** - #[test] - fn a_standalone_clean_takes_the_guest_builds_host_half_and_leaves_the_hosts() { - let root = toyos_tmpdir::TempDir::new("toyos-only-clean"); - let file = |rel: &str| { - let path = root.join(rel); - fs::create_dir_all(path.parent().unwrap()).unwrap(); - fs::write(&path, "built").unwrap(); - path - }; - fs::write(root.join("Cargo.toml"), "[workspace]\nmembers = [\"ld\"]\n").unwrap(); - let mut guest: Vec = - Arch::ALL.iter().map(|arch| file(&format!("target/{}/{PROFILE}/ld", arch.userland()))).collect(); - guest.push(file(&format!("target/{PROFILE}/deps/libsyn-1.rlib"))); - let host = file("target/debug/toyos-build"); - let identity = Identity::of_parts("compiler", "freestanding", "toyos"); - - clean(&root, &root.join("ld"), Clean::ToyosOnly, &Stale::All, &identity); - for gone in &guest { - assert!(!gone.exists(), "{} survived a clean of what the guest build wrote", gone.display()); - } - assert!(host.is_file(), "the host workspace's own build went"); - assert_eq!(fs::read_to_string(root.join("target/.deps-stamp")).unwrap(), identity.to_string()); + let programs = shipped.crates.iter().map(|(dir, _)| dir.clone()); + assert_eq!(shipped.programs, programs.filter(|dir| *dir != kernel && *dir != loader).collect()); } /// **Moved libraries take what was built for their targets, and nothing @@ -2484,14 +2399,14 @@ mod tests { let before = Identity::of_parts("compiler", "freestanding", "toyos"); for dir in crates { fs::write(dir.join("target/.deps-stamp"), before.to_string()).unwrap(); - assert_eq!(stale(&root, dir, &before), None, "{} was stale against its own stamp", dir.display()); + assert_eq!(stale(dir, &before), None, "{} was stale against its own stamp", dir.display()); } let moved = |identity: &Identity, want: Vec<&'static str>| { for dir in crates { - let found = stale(&root, dir, identity); + let found = stale(dir, identity); assert_eq!(found, Some(Stale::Targets(want.clone())), "{}", dir.display()); - clean(&root, dir, Clean::All, &found.unwrap(), identity); - assert_eq!(stale(&root, dir, identity), None, "{} was not stamped", dir.display()); + clean(&root, dir, &found.unwrap(), identity); + assert_eq!(stale(dir, identity), None, "{} was not stamped", dir.display()); } }; @@ -2517,9 +2432,9 @@ mod tests { } let compiler = Identity::of_parts("another compiler", "freestanding, edited", "toyos on the edited fork"); - assert_eq!(stale(&root, &kernel, &compiler), Some(Stale::All), "a moved compiler kept the host half"); + assert_eq!(stale(&kernel, &compiler), Some(Stale::All), "a moved compiler kept the host half"); fs::write(userland.join("target/.deps-stamp"), "sysroot:/a/stamp/naming/no/compiler").unwrap(); - assert_eq!(stale(&root, &userland, &fork_edit), Some(Stale::All), "a stamp naming no compiler was trusted"); + assert_eq!(stale(&userland, &fork_edit), Some(Stale::All), "a stamp naming no compiler was trusted"); } /// **A stamp that cannot be written stops the build**: one left behind would @@ -2534,7 +2449,7 @@ mod tests { fs::set_permissions(&target, fs::Permissions::from_mode(0o555)).unwrap(); let identity = Identity::of_parts("compiler", "freestanding", "toyos"); let failed = std::panic::catch_unwind(|| { - clean(&root, &root.join("kernel"), Clean::All, &Stale::Targets(vec![]), &identity) + clean(&root, &root.join("kernel"), &Stale::Targets(vec![]), &identity) }); fs::set_permissions(&target, fs::Permissions::from_mode(0o755)).unwrap(); let refusal = failed.expect_err("a stamp that was not written was taken for written"); @@ -2554,7 +2469,7 @@ mod tests { fs::write(kernel.join("Cargo.toml"), "[package\n").unwrap(); let identity = Identity::of_parts("compiler", "freestanding", "toyos"); - let failed = std::panic::catch_unwind(|| clean(&root, &kernel, Clean::All, &Stale::All, &identity)); + let failed = std::panic::catch_unwind(|| clean(&root, &kernel, &Stale::All, &identity)); let refusal = failed.expect_err("a cargo clean that failed was taken for one that ran"); let refusal = refusal.downcast_ref::().expect("a formatted panic"); assert!(refusal.starts_with(&format!("cargo clean in {} exited", kernel.display())), "{refusal}"); diff --git a/src/ci.rs b/src/ci.rs index 857c11475cc..2b69481bd85 100644 --- a/src/ci.rs +++ b/src/ci.rs @@ -684,8 +684,7 @@ fn apps_for( let mut red = Vec::new(); for program in &attempted { let manifest = format!("{}/Cargo.toml", program.dir); - let mut args = vec![verb, "--manifest-path", manifest.as_str(), "--target", triple]; - args.extend(program.features.args()); + let args = [verb, "--manifest-path", manifest.as_str(), "--target", triple]; if let Err(exit) = cargo(root, &args) { red.push(format!( "{} fails for {} and declares neither `fails` there nor `exempt`: {exit}", @@ -1092,11 +1091,7 @@ mod tests { /// program is judged for none. #[test] fn an_app_is_judged_for_every_host_its_fails_does_not_name() { - let program = |dir: &str, host| Program { - dir: dir.into(), - features: crate::build::Features::Default, - host, - }; + let program = |dir: &str, host| Program { dir: dir.into(), host }; let programs = [ program("calc", Host::App(Vec::new())), program("doom", Host::App(vec![Os::Windows])), diff --git a/src/hostws.rs b/src/hostws.rs index 0771a513dc4..8aeef23efff 100644 --- a/src/hostws.rs +++ b/src/hostws.rs @@ -11,15 +11,15 @@ //! by the commit that added this file). //! //! A third copy of the list would restore the defect, so there is exactly one: -//! the `[workspace]` table. This module is its only reader. [`target_dir`] -//! answers `src/build.rs`'s question about where cargo writes a crate's -//! artifacts — a member's land in the workspace root's `target/`, not its own — -//! and the gates below walk the tree and red on a `Cargo.toml` that joined -//! neither `members` nor `exclude`. **A new host crate that forgets to join is +//! the `[workspace]` table. This module is its only reader, and the gates below +//! walk the tree and red on a `Cargo.toml` that joined neither `members` nor +//! `exclude`. **A new host crate that forgets to join is //! a red, not a silent gap.** use std::collections::BTreeSet; -use std::path::{Path, PathBuf}; +use std::path::Path; +#[cfg(test)] +use std::path::PathBuf; /// The `[workspace]` table of `root/Cargo.toml`. fn workspace_table(root: &Path) -> toml::value::Table { @@ -94,44 +94,6 @@ pub fn is_member(root: &Path, crate_dir: &Path) -> bool { members(root).contains(&relative) } -/// The `target/` cargo writes what it builds in `crate_dir` into. -/// -/// **A workspace member has no `target/` of its own**: cargo writes every -/// member's artifacts to the workspace root's. `toyos-ld` is the one this -/// matters for — a host crate *and* a `[programs]` entry in `system.toml`, so -/// `src/build.rs` builds it for `x86_64-unknown-toyos` and then reads the -/// result back off disk. Reading the old per-crate path after it joined the -/// workspace would have been a `Failed to read binary for toyos-ld` on every -/// image build. -/// -/// Everything else this is asked about — `kernel/`, `bootloader/`, `userland/`, -/// the guest crates under `tests/` — is excluded from the workspace and keeps -/// its own. -/// -/// **One target directory per checkout, and never one across them.** Cargo's -/// freshness for a path package is mtime rather than content, and `-C metadata` -/// carries no checkout path, so two worktrees aimed at one directory contend for -/// one artifact under one name: the tree whose sources are merely *older* is -/// declared fresh, compiles nothing, and links the other branch's code, with no -/// diagnostic anywhere. Measured. -/// -/// **`-Z checksum-freshness` fixes that and still cannot be relied on here**, so -/// the rule above is about enablement and not about the feature: the flag is -/// honoured by a nightly-capable cargo and *silently ignored* by any other, and -/// nothing a `.cargo/config.toml` can say travels with the shared `target-dir` -/// it would also carry. This host's rustup default is stable, and -/// `src/toolchain.rs`'s `host_cargo` lends the `toyos` toolchain whatever cargo -/// the machine has — stable's, on every CI runner. Sharing on with -/// freshness off is the mis-link above, so this function joins one path and -/// stays out of it. -pub fn target_dir(root: &Path, crate_dir: &Path) -> PathBuf { - if is_member(root, crate_dir) { - root.join("target") - } else { - crate_dir.join("target") - } -} - /// Every directory under `root` holding a `Cargo.toml`, as paths relative to /// `root`, but the excluded ones. /// @@ -237,8 +199,7 @@ fn dead_member_target_paths(members: &BTreeSet, text: &str) -> Vec = - [".", "toyos-ld", "toyos-elf"].iter().map(|s| s.to_string()).collect(); + [".", "toyos-abi", "toyos-elf"].iter().map(|s| s.to_string()).collect(); assert_eq!( - dead_member_target_paths(&members, "let ld = root.join(\"../../toyos-ld/target/x\");"), - ["toyos-ld/target"], + dead_member_target_paths(&members, "let abi = root.join(\"../../toyos-abi/target/x\");"), + ["toyos-abi/target"], ); // `kernel/` and `userland/` are workspace *roots*, so naming one of // their target directories is correct and must not be reported. @@ -512,39 +469,19 @@ mod tests { #[test] fn the_dead_path_scan_reads_code_and_not_the_comment_beside_it() { assert_eq!( - code_only("let p = \"toyos-elf/target/x\"; // was toyos-ld/target\n", "//").trim(), + code_only("let p = \"toyos-elf/target/x\"; // was toyos-abi/target\n", "//").trim(), "let p = \"toyos-elf/target/x\";", ); assert_eq!(code_only(" target\n", "#").trim(), "target"); assert_eq!( - code_only(" # toyos-ld/target used to be here\n", "#").trim(), + code_only(" # toyos-abi/target used to be here\n", "#").trim(), "", ); // A `//` inside a string is not a comment, so what follows it on the // line is still code and still read. assert_eq!( - code_only("let u = \"https://x/\"; let p = \"toyos-ld/target\";\n", "//").trim(), - "let u = \"https://x/\"; let p = \"toyos-ld/target\";", - ); - } - - /// [`target_dir`] is what `src/build.rs` reads a guest binary back from, so - /// it has to answer differently for the two kinds of crate — and be shown - /// doing it, because reading the wrong one is a build failure with a - /// message that names a file rather than a cause. - #[test] - fn a_member_builds_into_the_workspace_target_and_an_excluded_crate_into_its_own() { - let root = repo_root(); - assert_eq!(target_dir(&root, &root.join("toyos-ld")), root.join("target")); - assert_eq!(target_dir(&root, &root), root.join("target")); - assert_eq!( - target_dir(&root, &root.join("kernel")), - root.join("kernel/target"), - "kernel/ is excluded and keeps its own target directory", - ); - assert_eq!( - target_dir(&root, &root.join("userland/snake")), - root.join("userland/snake/target"), + code_only("let u = \"https://x/\"; let p = \"toyos-abi/target\";\n", "//").trim(), + "let u = \"https://x/\"; let p = \"toyos-abi/target\";", ); } } diff --git a/src/licence.rs b/src/licence.rs index 3274750f49e..5dae94d57a7 100644 --- a/src/licence.rs +++ b/src/licence.rs @@ -458,12 +458,6 @@ pub const COMMITTED_FILES: &[(&str, &str, &str, Terms)] = &[ "ours: the first 4096 bytes of a rust-lld output (toyos-elf/tests/real.rs)", Terms::Spdx("MIT OR Apache-2.0"), ), - ( - "toyos-elf/tests/fixtures/toyos-ld-headers.bin", - "6243d543a15941133514c1a8a24c79d118060caeae7e985870a67d9fc3021354", - "ours: the first 4096 bytes of a toyos-ld output (toyos-elf/tests/real.rs)", - Terms::Spdx("MIT OR Apache-2.0"), - ), ( "toyos-symbols/tests/fixtures/input-test.bin", "6a08f75ee01bdbd1e77c9b3affd6185e981d86995da432c90ed107676f08eb83", @@ -1371,13 +1365,11 @@ mod tests { } /// The kernel's `--kernel-feature` picks any feature it declares, so its - /// graph is resolved with all of them; a `[programs]` row's - /// `no-default-features` and libc's features reach metadata as the build - /// passes them. + /// graph is resolved with all of them; libc's features reach metadata as + /// the build passes them. #[test] fn metadata_is_given_the_features_the_build_gives() { assert_eq!(Features::AnyDeclared.args(), ["--all-features"]); - assert_eq!(Features::NoDefault.args(), ["--no-default-features"]); assert_eq!( Features::With(crate::libc::FEATURES).args(), ["--features", "std-runtime"] diff --git a/src/userlandhost.rs b/src/userlandhost.rs index 113e4064501..71a2483a863 100644 --- a/src/userlandhost.rs +++ b/src/userlandhost.rs @@ -54,8 +54,6 @@ use std::collections::BTreeSet; use std::path::{Path, PathBuf}; -use crate::build::Features; - /// What [`survey`] found under one `userland/` directory. #[derive(Debug, PartialEq, Eq)] pub struct Survey { @@ -183,8 +181,6 @@ pub enum Host { pub struct Program { /// Its directory under the repository. pub dir: String, - /// What the image builds it with. - pub features: Features, pub host: Host, } @@ -192,14 +188,14 @@ pub struct Program { /// declares about the hosts. pub fn programs(root: &Path) -> Result, String> { let mut found = Vec::new(); - for (dir, features) in crate::build::shipped(root)?.programs { + for dir in crate::build::shipped(root)?.programs { let at = rel(root, &dir); let text = std::fs::read_to_string(dir.join("Cargo.toml")) .map_err(|e| format!("{at}/Cargo.toml: {e}"))?; let manifest: toml::Value = text.parse().map_err(|e| format!("{at}/Cargo.toml: not TOML: {e}"))?; let host = declared(&manifest, root).map_err(|why| format!("{at}/Cargo.toml: {why}"))?; - found.push(Program { dir: at, features, host }); + found.push(Program { dir: at, host }); } Ok(found) } @@ -524,7 +520,7 @@ mod tests { let programs = programs(&root).expect("every declaration reads"); let shipped = crate::build::shipped(&root).expect("the modes' configs").programs; let dirs: BTreeSet = programs.iter().map(|p| root.join(&p.dir)).collect(); - assert_eq!(dirs, shipped.into_iter().map(|(dir, _)| dir).collect()); + assert_eq!(dirs, shipped); assert!(programs.iter().any(|p| p.host == Host::App(Vec::new())), "no app: {programs:?}"); assert!(programs.iter().any(|p| p.host == Host::Exempt), "no exemption: {programs:?}"); } diff --git a/system.toml b/system.toml index 266087d1fe4..d93d01f23fa 100644 --- a/system.toml +++ b/system.toml @@ -40,8 +40,6 @@ proctest = {} # The installer. It holds nothing: it writes under `/apps` because `/apps` is # writable to it, and a row that gave it more would make that untrue. pkg = {} -toyos-ld = { path = "toyos-ld" } -# cargo = { path = "userland/cargo", no-default-features = true } # **Every image that carries a `TOYOS-LOG` partition runs this**, and every # image does. The kernel keeps the record ring and the console and writes no @@ -84,7 +82,7 @@ devices = ["pci:1af4:1041"] [programs.sshserver] service = true receives = ["netstack"] -starts = ["toybox", "shell", "terminal", "doom", "snake", "calc", "editor", "paint", "files", "host", "inspect", "pkg", "proctest", "toyos-ld", "sshserver", "swap", "update", "/apps"] +starts = ["toybox", "shell", "terminal", "doom", "snake", "calc", "editor", "paint", "files", "host", "inspect", "pkg", "proctest", "sshserver", "swap", "update", "/apps"] login = true # `swap` is the authority to replace a running service's binary; the build @@ -135,7 +133,7 @@ starts = ["shell"] [programs.shell] receives = ["surface"] -starts = ["toybox", "shell", "terminal", "doom", "snake", "calc", "editor", "paint", "files", "host", "inspect", "pkg", "proctest", "toyos-ld", "sshserver", "swap", "update", "/apps"] +starts = ["toybox", "shell", "terminal", "doom", "snake", "calc", "editor", "paint", "files", "host", "inspect", "pkg", "proctest", "sshserver", "swap", "update", "/apps"] [programs.doom] receives = ["compositor", "soundserver"] @@ -170,7 +168,7 @@ receives = ["compositor", "filepicker"] [programs.toybox] receives = ["compositor", "soundserver", "surface", "power"] syscap = ["roster"] -starts = ["toybox", "shell", "terminal", "doom", "snake", "calc", "editor", "paint", "files", "host", "inspect", "pkg", "proctest", "toyos-ld", "sshserver", "swap", "update", "/apps"] +starts = ["toybox", "shell", "terminal", "doom", "snake", "calc", "editor", "paint", "files", "host", "inspect", "pkg", "proctest", "sshserver", "swap", "update", "/apps"] # Symlinks created on ROOT at build time. # Keys are ROOT-relative; targets are the guest paths they resolve to. diff --git a/toyos-elf/Cargo.toml b/toyos-elf/Cargo.toml index a73b991f87c..86833e266d8 100644 --- a/toyos-elf/Cargo.toml +++ b/toyos-elf/Cargo.toml @@ -12,4 +12,5 @@ edition = "2021" license = "MIT OR Apache-2.0" [dev-dependencies] -object = { version = "0.38", default-features = false, features = ["read_core", "elf", "std"] } +# unaligned: an include_bytes! fixture is aligned to 1, and object reads a header in place. +object = { version = "0.38", default-features = false, features = ["read_core", "elf", "std", "unaligned"] } diff --git a/toyos-elf/src/note.rs b/toyos-elf/src/note.rs index 9ca1ba587a9..09fd15f2284 100644 --- a/toyos-elf/src/note.rs +++ b/toyos-elf/src/note.rs @@ -13,7 +13,7 @@ use crate::read; /// `n_type` of a GNU build-id note. pub const NT_GNU_BUILD_ID: u32 = 3; -/// The longest build-id this crate answers. lld's and `toyos-ld`'s are 20 +/// The longest build-id this crate answers. lld's is 20 /// bytes, a `uuid` or `md5` one 16; past this a descriptor is no build-id any /// linker writes. pub const MAX_BUILD_ID: usize = 32; diff --git a/toyos-elf/tests/fixtures/toyos-ld-headers.bin b/toyos-elf/tests/fixtures/toyos-ld-headers.bin deleted file mode 100644 index 999552ee3d1..00000000000 Binary files a/toyos-elf/tests/fixtures/toyos-ld-headers.bin and /dev/null differ diff --git a/toyos-elf/tests/real.rs b/toyos-elf/tests/real.rs index 51740d70238..8cf5da76f0f 100644 --- a/toyos-elf/tests/real.rs +++ b/toyos-elf/tests/real.rs @@ -2,69 +2,67 @@ //! //! Every other test in this crate hands the parser bytes no linker would emit, //! and a parser that refused everything would pass all of them. The fixture is -//! the first 4 KiB of `/system/bin/shell` as `toyos-ld` linked it — which is all -//! [`Layout::parse`] ever reads — so this is the shape the loader actually -//! meets at every boot. +//! the first 4 KiB of `/system/bin/shell` as rust-lld linked it with +//! `--build-id` — which is all [`Layout::parse`] ever reads — so this is the +//! shape the loader actually meets at every boot. //! -//! Refresh it with `dd if=userland/target/x86_64-unknown-toyos/debug/shell -//! of=toyos-elf/tests/fixtures/toyos-ld-headers.bin bs=1 count=4096`, and -//! expect the entry point below to move. +//! Refresh it with `dd if=userland/target/x86_64-unknown-toyos/toyos/shell +//! of=toyos-elf/tests/fixtures/lld-headers.bin bs=4096 count=1`, and expect +//! every number below to move: each is `readelf -hlnW` of the fixture. use toyos_elf::{Layout, Machine}; -const HEADERS: &[u8] = include_bytes!("fixtures/toyos-ld-headers.bin"); +const LLD_HEADERS: &[u8] = include_bytes!("fixtures/lld-headers.bin"); #[test] -fn a_toyos_ld_binary_parses_to_what_readelf_says() { - let layout = Layout::parse(HEADERS, Machine::X86_64).expect("toyos-ld's own output"); +fn a_linked_binary_parses_to_what_readelf_says() { + let layout = Layout::parse(LLD_HEADERS, Machine::X86_64).expect("rust-lld's own output"); - assert_eq!(layout.entry().get(), 0x3261c); + assert_eq!(layout.entry().get(), 0x4a07c); assert_eq!(layout.extent().min(), 0); - assert_eq!(layout.extent().max(), 0x167000); + assert_eq!(layout.extent().max(), 0xc53d4); - // text (R-X), data (RW-), rodata carrying .rela.dyn and .dynamic (R--). + // R--, R-X, the RELRO data carrying TLS and `.dynamic` (RW-), and the + // rest of the data with `.bss` (RW-). let segs = layout.segments(); - assert_eq!(segs.len(), 3); - assert!(segs[0].flags().executable() && !segs[0].writable()); - assert!(segs[1].writable() && !segs[1].flags().executable()); - assert!(!segs[2].writable() && !segs[2].flags().executable()); - assert_eq!(layout.writable_window(), Some((0x145000, 0x155000))); + assert_eq!(segs.len(), 4); + assert!(!segs[0].writable() && !segs[0].flags().executable()); + assert!(segs[1].flags().executable() && !segs[1].writable()); + assert!(segs[2].writable() && !segs[2].flags().executable()); + assert!(segs[3].writable() && !segs[3].flags().executable()); + assert_eq!(layout.writable_window(), Some((0xb9270, 0xc53d4))); let dynamic = layout.dynamic().map(|d| (d.file_offset(), d.image().start().get(), d.image().len())); - assert_eq!(dynamic, Some((0x166490, 0x166490, 0x60))); + assert_eq!(dynamic, Some((0xbe620, 0xc0620, 0x100))); let eh = layout.eh_frame_hdr().map(|r| (r.start().get(), r.len())); - assert_eq!(eh, Some((0x13e118, 0x688c))); - assert_eq!(layout.tls().unwrap().memsz(), 0x90); - assert_eq!(layout.tls().unwrap().align(), 0x40); + assert_eq!(eh, Some((0x2538c, 0x251c))); + assert_eq!(layout.tls().unwrap().memsz(), 0x68); + assert_eq!(layout.tls().unwrap().align(), 0x8); - // `e_phoff` 0x40, six headers of 56 bytes, inside the text segment's file - // bytes at offset 0. + // `e_phoff` 0x40, eleven headers of 56 bytes, inside the first segment's + // file bytes at offset 0. let table = layout.program_headers().map(|t| (t.image().start().get(), t.image().len(), t.count())); - assert_eq!(table, Some((0x40, 6 * 56, 6))); + assert_eq!(table, Some((0x40, 11 * 56, 11))); let sections = layout.section_headers().expect("a section header table"); - assert_eq!((sections.count, sections.entry_size), (9, 64)); + assert_eq!((sections.count, sections.entry_size), (26, 64)); - // Every `DT_*` vaddr in this file resolves, and no two segments contend for - // a page — the two derived answers `spawn` refuses a binary over. + // No two segments contend for a page, and a vaddr resolves to its file + // offset — the two derived answers `spawn` refuses a binary over. assert_eq!(layout.overlapping_load_pages(4096), None); - assert_eq!(layout.vaddr_to_file_offset(0x166490), Some(0x166490)); - assert_eq!(layout.vaddr_to_file_offset(0x155000), Some(0x155000)); + assert_eq!(layout.vaddr_to_file_offset(0xc0620), Some(0xbe620)); + assert_eq!(layout.vaddr_to_file_offset(0xc1800), Some(0xbe800)); + assert_eq!(layout.vaddr_to_file_offset(0x2a8), Some(0x2a8)); } -/// The first 4 KiB of `/system/bin/shell` as rust-lld linked it with -/// `--build-id`: `dd if=userland/target/x86_64-unknown-toyos/toyos/shell -/// of=toyos-elf/tests/fixtures/lld-headers.bin bs=4096 count=1`. -const LLD_HEADERS: &[u8] = include_bytes!("fixtures/lld-headers.bin"); - /// The build-id `object` reads out of `file`'s program headers. fn objects_build_id(file: &[u8]) -> Option> { use object::read::elf::{FileHeader, ProgramHeader}; - let header = object::elf::FileHeader64::::parse(file).ok()?; - let endian = header.endian().ok()?; - for phdr in header.program_headers(endian, file).ok()? { - let Ok(Some(mut notes)) = phdr.notes(endian, file) else { continue }; - while let Ok(Some(note)) = notes.next() { + let header = object::elf::FileHeader64::::parse(file).expect("object reads the header"); + let endian = header.endian().expect("object reads the endianness"); + for phdr in header.program_headers(endian, file).expect("object reads the program headers") { + let Some(mut notes) = phdr.notes(endian, file).expect("object reads a note segment") else { continue }; + while let Some(note) = notes.next().expect("object reads a note") { if note.name() == b"GNU" && note.n_type(endian) == object::elf::NT_GNU_BUILD_ID { return Some(note.desc().to_vec()); } @@ -96,9 +94,3 @@ fn an_lld_binarys_build_id_is_what_object_and_readelf_read() { ]; assert_eq!(ours.as_deref(), Some(&readelf[..])); } - -#[test] -fn a_toyos_ld_binary_without_the_flag_carries_no_build_id() { - assert_eq!(our_build_id(HEADERS), None); - assert_eq!(objects_build_id(HEADERS), None); -} diff --git a/toyos-elf/tests/tables.rs b/toyos-elf/tests/tables.rs index c7ee70c07d7..b0c7967f073 100644 --- a/toyos-elf/tests/tables.rs +++ b/toyos-elf/tests/tables.rs @@ -143,12 +143,6 @@ fn validation_refuses_a_write_outside_the_window_by_name() { /// A table the loader reads while it writes must not lie inside the range it /// writes, and the refusal names which one. -/// -/// The window is `/system/bin/shell`'s own, `(0x145000, 0x155000)` — the number -/// `real.rs` reads off the committed `toyos-ld` header fixture — against the -/// range the loader used to permit, which is that window's start rounded down -/// to the 2 MiB page: `[0, 0x200000)`. That file's `.rela.dyn` sits at -/// `0x166490`, outside the first and inside the second. #[test] fn a_read_table_inside_the_write_window_is_refused_by_name() { let exact = (0x145000u64, 0x155000u64); diff --git a/toyos-ld/Cargo.toml b/toyos-ld/Cargo.toml deleted file mode 100644 index 6d6564bfbbd..00000000000 --- a/toyos-ld/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "toyos-ld" -version = "0.3.0" -edition = "2021" -license = "MIT OR Apache-2.0" -description = "The one linker that runs inside ToyOS. Frozen: everything the host builds links through rust-lld, and this goes when LLD runs on ToyOS." -repository = "https://github.com/ToyOSOrg/ToyOS" -# Off crates.io: no Rust target names it any more. -publish = false - -[dependencies] -object = { version = "0.38", default-features = false, features = ["read", "write", "elf", "coff", "pe", "macho", "std"] } -sha2 = "0.10" -thiserror = "2" -zerocopy = { version = "0.8", features = ["derive"] } - -[dev-dependencies] -# Test scratch that is gone on every way out; `--ci host` reds on any left behind. -# A path alone, so `cargo publish` drops it. -toyos-tmpdir = { path = "../toyos-tmpdir" } - -[lints.rust] -warnings = "deny" - -# `[profile.toyos]` used to be declared here. Cargo ignores `[profile]` in a -# workspace member and this is one — it now lives in the root `Cargo.toml`, -# verbatim, and reaches this crate from there. `src/hostws.rs` reds on a member -# that declares one. diff --git a/toyos-ld/README.md b/toyos-ld/README.md deleted file mode 100644 index 77f06a506c6..00000000000 --- a/toyos-ld/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# toyos-ld - -Linker that resolves libraries, combines object files, and produces executables. - -The one linker that runs inside ToyOS, until LLD does. Frozen: everything the host builds links through `rust-lld`. diff --git a/toyos-ld/src/collect.rs b/toyos-ld/src/collect.rs deleted file mode 100644 index d4f61cd9d21..00000000000 --- a/toyos-ld/src/collect.rs +++ /dev/null @@ -1,1437 +0,0 @@ -use object::elf; -use object::pe; -use object::macho; -use object::read::elf::ElfFile64; -use object::read::{self, Object, ObjectSection, ObjectSymbol}; -use object::RelocationFlags; -use crate::LinkError; -use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; -use std::fs; -use std::path::PathBuf; - -/// Newtype for indices into `LinkState::sections`. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub(crate) struct SectionIdx(pub usize); - -impl std::ops::Index for Vec { - type Output = InputSection; - fn index(&self, idx: SectionIdx) -> &InputSection { &self[idx.0] } -} - -impl std::ops::IndexMut for Vec { - fn index_mut(&mut self, idx: SectionIdx) -> &mut InputSection { &mut self[idx.0] } -} - -/// Newtype for indices into the input objects slice. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub(crate) struct ObjIdx(pub usize); - -/// Type-safe symbol reference that distinguishes global from local symbols. -/// Local symbols carry their originating object index, ensuring they are never -/// confused with same-named locals from other objects (e.g. `.str.63`). -/// -/// `Ord` is over the input position and the name, both of which a byte-identical -/// set of inputs fixes, so it is a key an ordered container may hold. -#[derive(Debug, Clone, Hash, Eq, PartialEq, PartialOrd, Ord)] -pub(crate) enum SymbolRef { - Global(String), - Local(ObjIdx, String), -} - -impl SymbolRef { - pub(crate) fn name(&self) -> &str { - match self { - SymbolRef::Global(n) | SymbolRef::Local(_, n) => n, - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum SectionKind { - Code, - ReadOnly, - Data, - Bss, - Tls, - TlsBss, - /// Mach-O `__thread_vars`: TLV descriptors (thunk + key + offset). - TlsVariables, - InitArray, - FiniArray, -} - -/// Target architecture for the link output, detected from input object metadata. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum Arch { - Aarch64, - X86_64, -} - -impl SectionKind { - pub fn is_writable(self) -> bool { - matches!(self, Self::Data | Self::Bss | Self::InitArray | Self::FiniArray) - } - pub fn is_nobits(self) -> bool { - matches!(self, Self::Bss | Self::TlsBss) - } - pub fn is_tls(self) -> bool { - matches!(self, Self::Tls | Self::TlsBss | Self::TlsVariables) - } -} - -#[derive(Clone)] -pub(crate) struct InputSection { - pub(crate) name: String, - pub(crate) data: Vec, - pub(crate) align: u64, - pub(crate) size: u64, - pub(crate) vaddr: Option, - pub(crate) kind: SectionKind, - /// Section has SHF_MERGE flag (eligible for deduplication) - pub(crate) merge: bool, - /// Section has SHF_STRINGS flag (null-terminated string entries) - pub(crate) strings: bool, - /// Entry size for merge sections (e.g., 1 for .rodata.str1.1) - pub(crate) entsize: u64, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum RelocType { - // x86_64 - X86_64, - X86Pc32, - X86Plt32, - X86_32, - X86_32S, - X86Gotpcrel, - X86Gotpcrelx, - X86RexGotpcrelx, - X86Tpoff32, - X86Gottpoff, - X86Tlsgd, - X86Tlsld, - X86Dtpoff32, - X86Tlv, - // AArch64 - Aarch64Abs64, - Aarch64Abs32, - Aarch64Prel32, - Aarch64Call26, - Aarch64Jump26, - Aarch64AdrPrelPgHi21, - Aarch64AddAbsLo12Nc, - Aarch64Ldst8AbsLo12Nc, - Aarch64Ldst16AbsLo12Nc, - Aarch64Ldst32AbsLo12Nc, - Aarch64Ldst64AbsLo12Nc, - Aarch64Ldst128AbsLo12Nc, - Aarch64MovwUabsG0Nc, - Aarch64MovwUabsG1Nc, - Aarch64MovwUabsG2Nc, - Aarch64MovwUabsG3, - Aarch64AdrGotPage, - Aarch64Ld64GotLo12Nc, - Aarch64GotPcrel32, - Aarch64TlvpLoadPage21, - Aarch64TlvpLoadPageoff12, -} - -impl std::fmt::Display for RelocType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - RelocType::X86_64 => write!(f, "R_X86_64_64"), - RelocType::X86Pc32 => write!(f, "R_X86_64_PC32"), - RelocType::X86Plt32 => write!(f, "R_X86_64_PLT32"), - RelocType::X86_32 => write!(f, "R_X86_64_32"), - RelocType::X86_32S => write!(f, "R_X86_64_32S"), - RelocType::X86Gotpcrel => write!(f, "R_X86_64_GOTPCREL"), - RelocType::X86Gotpcrelx => write!(f, "R_X86_64_GOTPCRELX"), - RelocType::X86RexGotpcrelx => write!(f, "R_X86_64_REX_GOTPCRELX"), - RelocType::X86Tpoff32 => write!(f, "R_X86_64_TPOFF32"), - RelocType::X86Gottpoff => write!(f, "R_X86_64_GOTTPOFF"), - RelocType::X86Tlsgd => write!(f, "R_X86_64_TLSGD"), - RelocType::X86Tlsld => write!(f, "R_X86_64_TLSLD"), - RelocType::X86Dtpoff32 => write!(f, "R_X86_64_DTPOFF32"), - RelocType::X86Tlv => write!(f, "X86_64_RELOC_TLV"), - RelocType::Aarch64Abs64 => write!(f, "R_AARCH64_ABS64"), - RelocType::Aarch64Abs32 => write!(f, "R_AARCH64_ABS32"), - RelocType::Aarch64Prel32 => write!(f, "R_AARCH64_PREL32"), - RelocType::Aarch64Call26 => write!(f, "R_AARCH64_CALL26"), - RelocType::Aarch64Jump26 => write!(f, "R_AARCH64_JUMP26"), - RelocType::Aarch64AdrPrelPgHi21 => write!(f, "R_AARCH64_ADR_PREL_PG_HI21"), - RelocType::Aarch64AddAbsLo12Nc => write!(f, "R_AARCH64_ADD_ABS_LO12_NC"), - RelocType::Aarch64Ldst8AbsLo12Nc => write!(f, "R_AARCH64_LDST8_ABS_LO12_NC"), - RelocType::Aarch64Ldst16AbsLo12Nc => write!(f, "R_AARCH64_LDST16_ABS_LO12_NC"), - RelocType::Aarch64Ldst32AbsLo12Nc => write!(f, "R_AARCH64_LDST32_ABS_LO12_NC"), - RelocType::Aarch64Ldst64AbsLo12Nc => write!(f, "R_AARCH64_LDST64_ABS_LO12_NC"), - RelocType::Aarch64Ldst128AbsLo12Nc => write!(f, "R_AARCH64_LDST128_ABS_LO12_NC"), - RelocType::Aarch64MovwUabsG0Nc => write!(f, "R_AARCH64_MOVW_UABS_G0_NC"), - RelocType::Aarch64MovwUabsG1Nc => write!(f, "R_AARCH64_MOVW_UABS_G1_NC"), - RelocType::Aarch64MovwUabsG2Nc => write!(f, "R_AARCH64_MOVW_UABS_G2_NC"), - RelocType::Aarch64MovwUabsG3 => write!(f, "R_AARCH64_MOVW_UABS_G3"), - RelocType::Aarch64AdrGotPage => write!(f, "R_AARCH64_ADR_GOT_PAGE"), - RelocType::Aarch64Ld64GotLo12Nc => write!(f, "R_AARCH64_LD64_GOT_LO12_NC"), - RelocType::Aarch64GotPcrel32 => write!(f, "ARM64_RELOC_POINTER_TO_GOT"), - RelocType::Aarch64TlvpLoadPage21 => write!(f, "ARM64_RELOC_TLVP_LOAD_PAGE21"), - RelocType::Aarch64TlvpLoadPageoff12 => write!(f, "ARM64_RELOC_TLVP_LOAD_PAGEOFF12"), - } - } -} - -#[derive(Clone)] -pub(crate) struct InputReloc { - pub(crate) section: SectionIdx, - pub(crate) offset: u64, - pub(crate) r_type: RelocType, - pub(crate) target: SymbolRef, - pub(crate) addend: i64, - /// Mach-O SUBTRACTOR pairs: `target - subtrahend + addend`. - pub(crate) subtrahend: Option, -} - -#[derive(Debug, Clone, Copy)] -pub(crate) enum SymbolDef { - Defined { section: SectionIdx, value: u64, size: u64 }, - /// Dynamic (shared library) import. `is_func` distinguishes function vs data symbols - /// so the linker can create stubs only for functions (not data like __stdoutp). - /// `is_tls` marks thread-local symbols that need TPOFF64 relocs, not GLOB_DAT. - Dynamic { is_func: bool, is_tls: bool }, -} - -pub(crate) struct LinkState { - pub(crate) sections: Vec, - pub(crate) relocs: Vec, - /// Ordered because the symbol table and the string table naming it *are* - /// this iteration. The rule the crate follows: a container iterated into the - /// output carries its own total order, and one that is only asked whether it - /// contains a name — `dynamic_imports`, `merge_set`, `relaxed_calls` — stays - /// a hash container. - pub(crate) globals: BTreeMap, - pub(crate) locals: BTreeMap<(ObjIdx, String), SymbolDef>, - pub(crate) tls_sections: Vec, - /// Non-loadable metadata sections (e.g. .rustc) preserved in shared library output. - pub(crate) metadata: Vec<(String, Vec)>, - /// Symbol names provided by shared library (.so) inputs. - pub(crate) dynamic_imports: HashSet, - /// Bare filenames of .so inputs (for DT_NEEDED entries). - pub(crate) dynamic_libs: Vec, - /// Target architecture, detected from input object file metadata. - pub(crate) arch: Arch, -} - -/// Index within a single parsed object's local section list. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -struct LocalSectionIdx(usize); - -/// Intermediate result from parsing a single object file. Uses local section -/// indices that get remapped to global `SectionIdx` during merge. -enum ParsedInput { - /// A regular .o object file with sections, symbols, and relocations. - Object(ParsedObject), - /// A shared library (.so) providing dynamic symbols. - SharedLib { - globals: Vec<(String, SymbolDef)>, - dynamic_imports: Vec, - filename: String, - }, -} - -struct ParsedObject { - arch: Arch, - sections: Vec, - tls_local_indices: Vec, - metadata: Vec<(String, Vec)>, - globals: Vec<(String, SymbolDef, LocalSectionIdx)>, - locals: Vec, - relocs: Vec, -} - -struct LocalSymbol { - name: String, - section: LocalSectionIdx, - value: u64, - size: u64, -} - -/// Relocation with local section indices (not yet remapped to global). -struct LocalReloc { - section: LocalSectionIdx, - offset: u64, - r_type: RelocType, - target: LocalSymbolRef, - addend: i64, - subtrahend: Option, -} - -/// Symbol reference using local section indices for section-synthetic symbols. -enum LocalSymbolRef { - Global(String), - Local(String), - /// Synthetic section symbol referencing a local section index. - SectionSym(LocalSectionIdx), -} - -pub(crate) fn collect(objects: &[(String, Vec)]) -> Result { - // Flatten archives into individual object files, borrowing data from inputs. - let mut flat: Vec<(String, &[u8])> = Vec::new(); - for (name, data) in objects { - if is_archive(data) { - let data: &[u8] = data; - let archive = object::read::archive::ArchiveFile::parse(data) - .map_err(|e| LinkError::Parse { file: name.clone(), message: e.to_string() })?; - for member in archive.members() { - let member = member - .map_err(|e| LinkError::Parse { file: name.clone(), message: e.to_string() })?; - let member_name = String::from_utf8_lossy(member.name()).to_string(); - if !member_name.ends_with(".o") { continue; } - let member_data = member.data(data) - .map_err(|e| LinkError::Parse { file: format!("{name}({member_name})"), message: e.to_string() })?; - flat.push((format!("{name}({member_name})"), member_data)); - } - } else { - flat.push((name.clone(), data)); - } - } - - // Phase 1: parse all objects - let parsed: Vec = flat.iter() - .map(|(name, data)| parse_single_input(name, data)) - .collect::>()?; - - // Phase 2: merge results sequentially. - let mut state = LinkState { - sections: Vec::new(), - relocs: Vec::new(), - globals: BTreeMap::new(), - locals: BTreeMap::new(), - tls_sections: Vec::new(), - metadata: Vec::new(), - dynamic_imports: HashSet::new(), - dynamic_libs: Vec::new(), - arch: Arch::Aarch64, - }; - - for (obj_idx, input) in parsed.into_iter().enumerate() { - match input { - ParsedInput::SharedLib { globals, dynamic_imports, filename } => { - for (name, def) in globals { - state.globals.entry(name).or_insert(def); - } - state.dynamic_imports.extend(dynamic_imports); - if !state.dynamic_libs.contains(&filename) { - state.dynamic_libs.push(filename); - } - } - ParsedInput::Object(parsed) => { - merge_parsed_object(&mut state, parsed, ObjIdx(obj_idx)); - } - } - } - - Ok(state) -} - -/// Parse a single input file (object or shared library) without shared state. -fn parse_single_input(name: &str, data: &[u8]) -> Result { - // ELF shared library: extract dynamic symbols only. - if let Ok(elf) = ElfFile64::parse(data) { - if elf.elf_header().e_type.get(object::Endianness::Little) == elf::ET_DYN { - let mut globals = Vec::new(); - let mut dynamic_imports = Vec::new(); - for sym in elf.dynamic_symbols() { - let sym_name = match sym.name() { - Ok(n) if !n.is_empty() => n, - _ => continue, - }; - if sym.is_undefined() { continue; } - let sym_name = sym_name.to_string(); - let is_func = sym.kind() == read::SymbolKind::Text; - let is_tls = sym.kind() == read::SymbolKind::Tls; - dynamic_imports.push(sym_name.clone()); - globals.push((sym_name, SymbolDef::Dynamic { is_func, is_tls })); - } - let filename = std::path::Path::new(name) - .file_name() - .unwrap_or_default() - .to_string_lossy() - .to_string(); - return Ok(ParsedInput::SharedLib { globals, dynamic_imports, filename }); - } - } - - // Generic parse: ELF .o or COFF .o - let obj = object::File::parse(data) - .map_err(|e| LinkError::Parse { file: name.to_string(), message: e.to_string() })?; - - parse_object(&obj, name).map(ParsedInput::Object) -} - -/// Parse a single object file into a `ParsedObject` with local section indices. -fn parse_object(obj: &object::File, _name: &str) -> Result { - let arch = if matches!(obj.architecture(), object::Architecture::X86_64) { - Arch::X86_64 - } else { - Arch::Aarch64 - }; - - let mut sections = Vec::new(); - let mut tls_local_indices = Vec::new(); - let mut metadata = Vec::new(); - let mut globals = Vec::new(); - let mut locals = Vec::new(); - let mut relocs = Vec::new(); - // Map from object's section index to local index in our sections vec - let mut sec_map: HashMap = HashMap::new(); - - for section in obj.sections() { - let sec_name = section.name().unwrap_or(""); - - if sec_name.starts_with(".rustc") { - let data = section.data().unwrap_or(&[]).to_vec(); - if !data.is_empty() { - metadata.push((sec_name.to_string(), data)); - } - continue; - } - - let kind = match section.kind() { - read::SectionKind::Text => SectionKind::Code, - read::SectionKind::Data | read::SectionKind::Common => SectionKind::Data, - read::SectionKind::ReadOnlyData - | read::SectionKind::ReadOnlyDataWithRel - | read::SectionKind::ReadOnlyString => SectionKind::ReadOnly, - read::SectionKind::UninitializedData => SectionKind::Bss, - read::SectionKind::Tls => SectionKind::Tls, - read::SectionKind::UninitializedTls => SectionKind::TlsBss, - read::SectionKind::TlsVariables => SectionKind::TlsVariables, - read::SectionKind::Elf(elf::SHT_INIT_ARRAY) => SectionKind::InitArray, - read::SectionKind::Elf(elf::SHT_FINI_ARRAY) => SectionKind::FiniArray, - read::SectionKind::Elf(elf::SHT_X86_64_UNWIND) => SectionKind::ReadOnly, - read::SectionKind::OtherString - | read::SectionKind::Other - | read::SectionKind::Debug - | read::SectionKind::DebugString - | read::SectionKind::Linker - | read::SectionKind::Note - | read::SectionKind::Metadata => continue, - read::SectionKind::Elf(t) => panic!( - "unhandled ELF section type {:#x} in section {}", - t, - section.name().unwrap_or(""), - ), - read::SectionKind::Unknown => continue, - _ => panic!( - "unhandled section kind {:?} in section {}", - section.kind(), - section.name().unwrap_or(""), - ), - }; - - let sec_data = section.data().unwrap_or(&[]).to_vec(); - let local_idx = LocalSectionIdx(sections.len()); - sec_map.insert(section.index(), local_idx); - - let (merge, strings, entsize) = match section.flags() { - read::SectionFlags::Elf { sh_flags } => { - let m = (sh_flags & elf::SHF_MERGE as u64) != 0; - let s = (sh_flags & elf::SHF_STRINGS as u64) != 0; - (m, s, if m { section.file_range().map(|_| { - if s { 1u64 } else { 0 } - }).unwrap_or(0) } else { 0 }) - } - _ => (false, false, 0), - }; - - sections.push(InputSection { - name: sec_name.to_string(), - data: sec_data, - align: section.align().max(1), - size: section.size(), - vaddr: None, - kind, - merge, - strings, - entsize, - }); - - if kind.is_tls() { - tls_local_indices.push(local_idx); - } - } - - let is_macho = matches!(obj.format(), object::BinaryFormat::MachO); - - // Collect symbols - for symbol in obj.symbols() { - let sym_name = match symbol.name() { - Ok(n) if !n.is_empty() => demangle_macho(n, is_macho), - _ => continue, - }; - if symbol.is_undefined() { continue; } - if symbol.kind() == read::SymbolKind::Section { continue; } - let sec_idx = match symbol.section() { - read::SymbolSection::Section(idx) => idx, - _ => continue, - }; - let local_sec = match sec_map.get(&sec_idx) { - Some(&g) => g, - None => continue, - }; - let sec_addr = obj.section_by_index(sec_idx).map(|s| s.address()).unwrap_or(0); - let value = symbol.address() - sec_addr; - let size = symbol.size(); - - if symbol.is_global() { - // COFF weak externals - if let Some(rest) = sym_name.strip_prefix(".weak.") { - let alias = rest.strip_suffix(".default").unwrap_or(rest).to_string(); - globals.push((alias, SymbolDef::Defined { - section: SectionIdx(0), // placeholder, remapped during merge - value, size, - }, local_sec)); - } - globals.push((sym_name, SymbolDef::Defined { - section: SectionIdx(0), // placeholder - value, size, - }, local_sec)); - } else { - locals.push(LocalSymbol { name: sym_name, section: local_sec, value, size }); - } - } - - // Collect relocations - for section in obj.sections() { - let local_sec = match sec_map.get(§ion.index()) { - Some(&g) => g, - None => continue, - }; - if sections[local_sec.0].kind.is_nobits() { continue; } - - let mut pending_subtractor: Option<(u64, LocalSymbolRef)> = None; - - for (offset, reloc) in section.relocations() { - let target = match resolve_reloc_target_local(obj, &reloc, &sec_map, §ions, arch, is_macho) { - Some(t) => t, - None => continue, - }; - - if let RelocationFlags::MachO { r_type, .. } = reloc.flags() { - if is_macho_subtractor(r_type, arch) { - assert!(pending_subtractor.is_none(), - "consecutive SUBTRACTOR without paired UNSIGNED"); - pending_subtractor = Some((offset, target)); - continue; - } - } - - let subtrahend = pending_subtractor.take().map(|(sub_offset, sub_sym)| { - assert_eq!(sub_offset, offset, - "SUBTRACTOR at offset {sub_offset:#x} not paired with \ - relocation at same offset (got {offset:#x})"); - sub_sym - }); - - let (r_type, is_macho_instruction) = match reloc.flags() { - RelocationFlags::Elf { r_type } => match elf_to_reloc_type(r_type) { - Some(r) => (r, false), - None if r_type == 0 => continue, - None => return Err(LinkError::UnsupportedRawRelocation { - raw_type: format!("ELF {r_type}"), - symbol: local_sym_name(&target), - }), - }, - RelocationFlags::Coff { typ } => match coff_to_reloc_type(typ) { - Some(r) => (r, false), - None => return Err(LinkError::UnsupportedRawRelocation { - raw_type: format!("COFF {typ}"), - symbol: local_sym_name(&target), - }), - }, - RelocationFlags::MachO { r_type, r_length, .. } => { - let mapped = match arch { - Arch::X86_64 => macho_x86_64_to_reloc_type(r_type, r_length), - Arch::Aarch64 => { - let data = §ions[local_sec.0].data; - let off = offset as usize; - let insn = u32::from_le_bytes(data[off..off + 4].try_into().unwrap()); - macho_arm64_to_reloc_type(r_type, r_length, insn) - } - }; - match mapped { - Some(r) => (r, matches!(arch, Arch::Aarch64) && macho_arm64_is_instruction_reloc(r_type)), - None => return Err(LinkError::UnsupportedRawRelocation { - raw_type: format!("Mach-O type={r_type} length={r_length}"), - symbol: local_sym_name(&target), - }), - } - } - flags => return Err(LinkError::UnsupportedRawRelocation { - raw_type: format!("{flags:?}"), - symbol: local_sym_name(&target), - }), - }; - - // A subtractor pair carries its displacement in the subtrahend, and - // a Mach-O instruction relocation carries none at all: both leave - // the addend at zero, for different reasons and by the same route. - let addend = if subtrahend.is_some() || is_macho_instruction { - 0 - } else if reloc.has_implicit_addend() { - let data = §ions[local_sec.0].data; - let off = offset as usize; - let implicit = match reloc.size() { - 64 => i64::from_le_bytes(data[off..off + 8].try_into().unwrap()), - 32 => i32::from_le_bytes(data[off..off + 4].try_into().unwrap()) as i64, - 16 => i16::from_le_bytes(data[off..off + 2].try_into().unwrap()) as i64, - sz => panic!("unexpected implicit addend size {sz} bits for {:?}", local_sym_name(&target)), - }; - if let read::RelocationTarget::Section(target_si) = reloc.target() { - let target_sec_addr = obj.section_by_index(target_si) - .map(|s| s.address()).unwrap_or(0) as i64; - if reloc.kind() == read::RelocationKind::Relative { - implicit + section.address() as i64 + offset as i64 - target_sec_addr - } else { - implicit - target_sec_addr - } - } else { - reloc.addend() + implicit - } - } else { - reloc.addend() - }; - - relocs.push(LocalReloc { - section: local_sec, - offset, - r_type, - target, - addend, - subtrahend, - }); - } - } - - Ok(ParsedObject { - arch, - sections, - tls_local_indices, - metadata, - globals, - locals, - relocs, - }) -} - -/// Resolve a relocation target to a `LocalSymbolRef` (no shared state needed). -fn resolve_reloc_target_local( - obj: &object::File, - reloc: &read::Relocation, - sec_map: &HashMap, - _sections: &[InputSection], - _arch: Arch, - is_macho: bool, -) -> Option { - match reloc.target() { - read::RelocationTarget::Symbol(sym_idx) => { - let sym = obj.symbol_by_index(sym_idx).ok()?; - let name = sym.name().unwrap_or(""); - let name = &demangle_macho(name, is_macho); - - let is_section_sym = name.is_empty() || sym.kind() == read::SymbolKind::Section; - if is_section_sym { - let si = match sym.section() { - read::SymbolSection::Section(si) => si, - _ => return None, - }; - let &local_idx = sec_map.get(&si)?; - Some(LocalSymbolRef::SectionSym(local_idx)) - } else if sym.is_global() || sym.is_undefined() { - Some(LocalSymbolRef::Global(name.to_string())) - } else { - Some(LocalSymbolRef::Local(name.to_string())) - } - } - read::RelocationTarget::Section(si) => { - let &local_idx = sec_map.get(&si)?; - Some(LocalSymbolRef::SectionSym(local_idx)) - } - _ => None, - } -} - -fn local_sym_name(sym: &LocalSymbolRef) -> String { - match sym { - LocalSymbolRef::Global(n) | LocalSymbolRef::Local(n) => n.clone(), - LocalSymbolRef::SectionSym(idx) => format!("__section_sym_{}", idx.0), - } -} - -/// Merge a parsed object into the global LinkState, remapping local section indices. -fn merge_parsed_object(state: &mut LinkState, parsed: ParsedObject, obj_idx: ObjIdx) { - if matches!(parsed.arch, Arch::X86_64) { - state.arch = Arch::X86_64; - } - - let base = state.sections.len(); - let remap = |local: LocalSectionIdx| -> SectionIdx { SectionIdx(base + local.0) }; - - // Merge sections - state.sections.extend(parsed.sections); - - // Merge TLS section indices - for local_idx in parsed.tls_local_indices { - state.tls_sections.push(remap(local_idx)); - } - - // Merge metadata - state.metadata.extend(parsed.metadata); - - // Merge globals - for (name, mut def, local_sec) in parsed.globals { - if let SymbolDef::Defined { ref mut section, .. } = def { - *section = remap(local_sec); - } - match state.globals.get(&name) { - Some(SymbolDef::Defined { .. }) => {} - _ => { - // When a local definition supersedes a shared library import, - // remove it from dynamic_imports so it won't get a GOT+GLOB_DAT entry. - if matches!(def, SymbolDef::Defined { .. }) { - state.dynamic_imports.remove(&name); - } - state.globals.insert(name.clone(), def); - } - } - } - - // Merge locals - for LocalSymbol { name, section: local_sec, value, size } in parsed.locals { - let global_sec = remap(local_sec); - if let Some(SymbolDef::Defined { section: existing_sec, .. }) = state.locals.get(&(obj_idx, name.clone())) { - assert_eq!( - *existing_sec, global_sec, - "local symbol {name:?} in obj {} defined in two \ - different sections ({} vs {})", - obj_idx.0, existing_sec.0, global_sec.0 - ); - } - state.locals.insert((obj_idx, name), SymbolDef::Defined { - section: global_sec, - value, size, - }); - } - - // Merge relocations, remapping local indices to global - for local_reloc in parsed.relocs { - let section = remap(local_reloc.section); - let target = remap_sym_ref_and_register(local_reloc.target, obj_idx, &remap, &mut state.locals); - let subtrahend = local_reloc.subtrahend.map(|s| remap_sym_ref_and_register(s, obj_idx, &remap, &mut state.locals)); - - state.relocs.push(InputReloc { - section, - offset: local_reloc.offset, - r_type: local_reloc.r_type, - target, - addend: local_reloc.addend, - subtrahend, - }); - } -} - -/// Convert a `LocalSymbolRef` to a global `SymbolRef`, registering synthetic -/// section symbols in `locals` as needed. -fn remap_sym_ref_and_register( - sym: LocalSymbolRef, - obj_idx: ObjIdx, - remap: &dyn Fn(LocalSectionIdx) -> SectionIdx, - locals: &mut BTreeMap<(ObjIdx, String), SymbolDef>, -) -> SymbolRef { - match sym { - LocalSymbolRef::Global(name) => SymbolRef::Global(name), - LocalSymbolRef::Local(name) => SymbolRef::Local(obj_idx, name), - LocalSymbolRef::SectionSym(local_idx) => { - let global_idx = remap(local_idx); - let syn = format!("__section_sym_{}_{}", obj_idx.0, global_idx.0); - locals.entry((obj_idx, syn.clone())).or_insert(SymbolDef::Defined { - section: global_idx, - value: 0, size: 0, - }); - SymbolRef::Local(obj_idx, syn) - } - } -} - -/// Extract exported dynamic symbols from an ET_DYN ELF (.so) and add them -/// to `globals` with a sentinel section index. These symbols satisfy undefined -/// references without contributing any code/data to the output. -/// Strip the Mach-O leading `_` prefix from C symbol names so all internal -/// names use ELF convention. The prefix is re-added at Mach-O emit time. -fn demangle_macho(name: &str, is_macho: bool) -> String { - if is_macho { - name.strip_prefix('_').unwrap_or(name).to_string() - } else { - name.to_string() - } -} - -fn elf_to_reloc_type(r_type: u32) -> Option { - // R_X86_64_NONE (0) is a no-op relocation that can appear in object files. - // Other unknown types are rejected at the call site. - Some(match r_type { - elf::R_X86_64_64 => RelocType::X86_64, - elf::R_X86_64_PC32 => RelocType::X86Pc32, - elf::R_X86_64_PLT32 => RelocType::X86Plt32, - elf::R_X86_64_32 => RelocType::X86_32, - elf::R_X86_64_32S => RelocType::X86_32S, - elf::R_X86_64_GOTPCREL => RelocType::X86Gotpcrel, - elf::R_X86_64_GOTPCRELX => RelocType::X86Gotpcrelx, - elf::R_X86_64_REX_GOTPCRELX => RelocType::X86RexGotpcrelx, - elf::R_X86_64_TPOFF32 => RelocType::X86Tpoff32, - elf::R_X86_64_GOTTPOFF => RelocType::X86Gottpoff, - elf::R_X86_64_TLSGD => RelocType::X86Tlsgd, - elf::R_X86_64_TLSLD => RelocType::X86Tlsld, - elf::R_X86_64_DTPOFF32 => RelocType::X86Dtpoff32, - elf::R_AARCH64_ABS64 => RelocType::Aarch64Abs64, - elf::R_AARCH64_ABS32 => RelocType::Aarch64Abs32, - elf::R_AARCH64_PREL32 => RelocType::Aarch64Prel32, - elf::R_AARCH64_CALL26 => RelocType::Aarch64Call26, - elf::R_AARCH64_JUMP26 => RelocType::Aarch64Jump26, - elf::R_AARCH64_ADR_PREL_PG_HI21 => RelocType::Aarch64AdrPrelPgHi21, - elf::R_AARCH64_ADD_ABS_LO12_NC => RelocType::Aarch64AddAbsLo12Nc, - elf::R_AARCH64_LDST8_ABS_LO12_NC => RelocType::Aarch64Ldst8AbsLo12Nc, - elf::R_AARCH64_LDST16_ABS_LO12_NC => RelocType::Aarch64Ldst16AbsLo12Nc, - elf::R_AARCH64_LDST32_ABS_LO12_NC => RelocType::Aarch64Ldst32AbsLo12Nc, - elf::R_AARCH64_LDST64_ABS_LO12_NC => RelocType::Aarch64Ldst64AbsLo12Nc, - elf::R_AARCH64_LDST128_ABS_LO12_NC => RelocType::Aarch64Ldst128AbsLo12Nc, - elf::R_AARCH64_MOVW_UABS_G0_NC => RelocType::Aarch64MovwUabsG0Nc, - elf::R_AARCH64_MOVW_UABS_G1_NC => RelocType::Aarch64MovwUabsG1Nc, - elf::R_AARCH64_MOVW_UABS_G2_NC => RelocType::Aarch64MovwUabsG2Nc, - elf::R_AARCH64_MOVW_UABS_G3 => RelocType::Aarch64MovwUabsG3, - elf::R_AARCH64_ADR_GOT_PAGE => RelocType::Aarch64AdrGotPage, - elf::R_AARCH64_LD64_GOT_LO12_NC => RelocType::Aarch64Ld64GotLo12Nc, - _ => return None, - }) -} - -/// Map COFF x86_64 relocation types to RelocType. -fn coff_to_reloc_type(typ: u16) -> Option { - Some(match typ { - pe::IMAGE_REL_AMD64_ADDR64 => RelocType::X86_64, - pe::IMAGE_REL_AMD64_ADDR32 => RelocType::X86_32, - pe::IMAGE_REL_AMD64_ADDR32NB => RelocType::X86_32S, - pe::IMAGE_REL_AMD64_REL32 - | pe::IMAGE_REL_AMD64_REL32_1 - | pe::IMAGE_REL_AMD64_REL32_2 - | pe::IMAGE_REL_AMD64_REL32_3 - | pe::IMAGE_REL_AMD64_REL32_4 - | pe::IMAGE_REL_AMD64_REL32_5 => RelocType::X86Plt32, - pe::IMAGE_REL_AMD64_SECREL => RelocType::X86_32, - _ => return None, - }) -} - -/// Check if a Mach-O relocation is a SUBTRACTOR (first half of a difference pair). -fn is_macho_subtractor(r_type: u8, arch: Arch) -> bool { - match arch { - Arch::X86_64 => r_type == macho::X86_64_RELOC_SUBTRACTOR, - Arch::Aarch64 => r_type == macho::ARM64_RELOC_SUBTRACTOR, - } -} - -/// Map Mach-O arm64 relocation types to RelocType. -/// -/// `ARM64_RELOC_PAGEOFF12` is used for both ADD and LDR/STR instructions. -/// We inspect the instruction opcode to determine the correct ELF-style -/// relocation type (ADD vs LDST with appropriate scale). -fn macho_arm64_to_reloc_type(r_type: u8, r_length: u8, insn: u32) -> Option { - Some(match r_type { - macho::ARM64_RELOC_UNSIGNED if r_length == 3 => RelocType::Aarch64Abs64, - macho::ARM64_RELOC_BRANCH26 => RelocType::Aarch64Call26, - macho::ARM64_RELOC_PAGE21 => RelocType::Aarch64AdrPrelPgHi21, - macho::ARM64_RELOC_PAGEOFF12 => classify_pageoff12(insn), - macho::ARM64_RELOC_GOT_LOAD_PAGE21 => RelocType::Aarch64AdrGotPage, - macho::ARM64_RELOC_GOT_LOAD_PAGEOFF12 => RelocType::Aarch64Ld64GotLo12Nc, - macho::ARM64_RELOC_POINTER_TO_GOT if r_length == 2 => RelocType::Aarch64GotPcrel32, - macho::ARM64_RELOC_TLVP_LOAD_PAGE21 => RelocType::Aarch64TlvpLoadPage21, - macho::ARM64_RELOC_TLVP_LOAD_PAGEOFF12 => RelocType::Aarch64TlvpLoadPageoff12, - _ => return None, - }) -} - -/// Classify a Mach-O `ARM64_RELOC_PAGEOFF12` by inspecting the instruction. -/// ADD immediate: bits [28:24] = 10001 -/// LDR/STR unsigned immediate: bits [27:24] = 1110 or 1111, bits [29:28] = size -fn classify_pageoff12(insn: u32) -> RelocType { - if (insn >> 24) & 0x1F == 0b10001 { - // ADD immediate - RelocType::Aarch64AddAbsLo12Nc - } else { - // LDR/STR: size field in bits [31:30] gives the scale - let size = insn >> 30; - let is_v = (insn >> 26) & 1; // SIMD/FP bit - if is_v == 1 && size == 0 { - // 128-bit SIMD load/store: opc[1]=1 with size=0 and V=1 - RelocType::Aarch64Ldst128AbsLo12Nc - } else { - match size { - 0 => RelocType::Aarch64Ldst8AbsLo12Nc, - 1 => RelocType::Aarch64Ldst16AbsLo12Nc, - 2 => RelocType::Aarch64Ldst32AbsLo12Nc, - 3 => RelocType::Aarch64Ldst64AbsLo12Nc, - _ => unreachable!(), - } - } - } -} - -/// Returns true if this Mach-O arm64 relocation type encodes its value inside -/// an instruction (as opposed to a raw data pointer). -fn macho_arm64_is_instruction_reloc(r_type: u8) -> bool { - !matches!(r_type, macho::ARM64_RELOC_UNSIGNED | macho::ARM64_RELOC_SUBTRACTOR) -} - -/// Map Mach-O x86_64 relocation types to RelocType. -fn macho_x86_64_to_reloc_type(r_type: u8, r_length: u8) -> Option { - Some(match r_type { - macho::X86_64_RELOC_UNSIGNED if r_length == 3 => RelocType::X86_64, - macho::X86_64_RELOC_UNSIGNED if r_length == 2 => RelocType::X86_32, - macho::X86_64_RELOC_SIGNED - | macho::X86_64_RELOC_SIGNED_1 - | macho::X86_64_RELOC_SIGNED_2 - | macho::X86_64_RELOC_SIGNED_4 => RelocType::X86Pc32, - macho::X86_64_RELOC_BRANCH => RelocType::X86Plt32, - macho::X86_64_RELOC_GOT_LOAD => RelocType::X86Gotpcrelx, - macho::X86_64_RELOC_GOT => RelocType::X86Gotpcrel, - macho::X86_64_RELOC_TLV => RelocType::X86Tlv, - _ => return None, - }) -} - -pub(crate) fn is_archive(data: &[u8]) -> bool { - data.starts_with(b"!\n") || data.starts_with(b"!\n") -} - -/// Check if data is a shared library (ELF ET_DYN). -pub(crate) fn is_shared_lib(data: &[u8]) -> bool { - // ELF magic + enough bytes for e_type at offset 16-17 - data.len() >= 18 - && data[..4] == [0x7f, b'E', b'L', b'F'] - && u16::from_le_bytes([data[16], data[17]]) == elf::ET_DYN -} - -pub(crate) fn extract_archive(name: &str, data: &[u8], out: &mut Vec<(String, Vec)>) -> Result<(), LinkError> { - let archive = object::read::archive::ArchiveFile::parse(data) - .map_err(|e| LinkError::Parse { file: name.to_string(), message: e.to_string() })?; - for member in archive.members() { - let member = member - .map_err(|e| LinkError::Parse { file: name.to_string(), message: e.to_string() })?; - let member_name = String::from_utf8_lossy(member.name()).to_string(); - if !member_name.ends_with(".o") { - continue; - } - let member_data = member.data(data) - .map_err(|e| LinkError::Parse { file: format!("{name}({member_name})"), message: e.to_string() })?; - out.push((format!("{name}({member_name})"), member_data.to_vec())); - } - Ok(()) -} - -pub(crate) fn find_lib(name: &str, paths: &[PathBuf]) -> Option<(String, Vec)> { - let exact = [format!("lib{name}.so"), format!("lib{name}.a"), format!("lib{name}.rlib")]; - for dir in paths { - for candidate in &exact { - let path = dir.join(candidate); - if let Ok(data) = fs::read(&path) { - return Some((path.display().to_string(), data)); - } - } - } - // Hash-suffixed Rust rlibs (e.g. libstd-abc123.rlib) - let prefix = format!("lib{name}-"); - for dir in paths { - if let Ok(entries) = fs::read_dir(dir) { - for entry in entries.flatten() { - let fname = entry.file_name(); - let fname = fname.to_string_lossy(); - if fname.starts_with(&prefix) - && (fname.ends_with(".rlib") || fname.ends_with(".a")) - { - if let Ok(data) = fs::read(entry.path()) { - return Some((entry.path().display().to_string(), data)); - } - } - } - } - } - None -} - -/// Quickly scan an object file for its defined and referenced (undefined) symbols. -/// Used for selective archive member extraction. -pub(crate) fn scan_symbols(data: &[u8]) -> (BTreeSet, BTreeSet) { - let mut defined = BTreeSet::new(); - let mut referenced = BTreeSet::new(); - - let obj = match read::File::parse(data) { - Ok(o) => o, - Err(e) => panic!("scan_symbols: failed to parse object: {e}"), - }; - - for sym in obj.symbols() { - let name = match sym.name() { - Ok(n) if !n.is_empty() => n.to_string(), - _ => continue, - }; - if sym.is_undefined() { - referenced.insert(name); - } else if sym.is_global() { - defined.insert(name); - } - } - - (defined, referenced) -} - -/// Remove unreachable sections (dead code elimination). -/// Roots: entry symbol's section + .init_array/.fini_array sections. -pub(crate) fn gc_sections(state: &mut LinkState, entry: &str) { - let num_sections = state.sections.len(); - if num_sections == 0 { return; } - - // Resolve symbol name → section index - let sym_to_section = |name: &str| -> Option { - if let Some(SymbolDef::Defined { section, .. }) = state.globals.get(name) { - return Some(*section); - } - None - }; - - // Build adjacency list: source_section → target sections - // Uses Vec instead of HashSet — BFS already skips visited nodes via `reachable[]`. - let mut edges: Vec> = vec![Vec::new(); num_sections]; - for reloc in &state.relocs { - for sym in std::iter::once(&reloc.target).chain(reloc.subtrahend.iter()) { - if let Some(target_sec) = sym_to_section(sym.name()) { - edges[reloc.section.0].push(target_sec.0); - } - if let SymbolRef::Local(obj_idx, name) = sym { - if let Some(SymbolDef::Defined { section: target_sec, .. }) = state.locals.get(&(*obj_idx, name.clone())) { - edges[reloc.section.0].push(target_sec.0); - } - } - } - } - - // Find roots - let mut reachable = vec![false; num_sections]; - let mut queue = std::collections::VecDeque::new(); - - // Entry symbol's section - if let Some(sec_idx) = sym_to_section(entry) { - reachable[sec_idx.0] = true; - queue.push_back(sec_idx.0); - } - - // `main` is always a root — it's called by `_start` (which may be in a - // shared library and thus invisible to GC). - if entry != "main" { - if let Some(sec_idx) = sym_to_section("main") { - if !reachable[sec_idx.0] { - reachable[sec_idx.0] = true; - queue.push_back(sec_idx.0); - } - } - } - - // .init_array / .fini_array sections are always roots - // TLS sections are always roots — they're accessed indirectly via __tls_get_addr - // and the GC can't trace through GOT/DTPMOD relocations to reach them. - // .eh_frame sections are always roots — they're accessed by the runtime unwinder - // via PT_GNU_EH_FRAME, not through symbol references. - for (idx, sec) in state.sections.iter().enumerate() { - if (sec.kind == SectionKind::InitArray - || sec.kind == SectionKind::FiniArray - || sec.kind.is_tls() - || sec.name == ".eh_frame") - && !reachable[idx] - { - reachable[idx] = true; - queue.push_back(idx); - } - } - - // BFS - while let Some(idx) = queue.pop_front() { - for &target in &edges[idx] { - if !reachable[target] { - reachable[target] = true; - queue.push_back(target); - } - } - } - - // Build old→new index mapping - let mut remap = vec![SectionIdx(0); num_sections]; - let mut new_idx = 0; - for old_idx in 0..num_sections { - if reachable[old_idx] { - remap[old_idx] = SectionIdx(new_idx); - new_idx += 1; - } - } - - // Remove dead sections - let mut new_sections = Vec::new(); - for (idx, sec) in state.sections.drain(..).enumerate() { - if reachable[idx] { - new_sections.push(sec); - } - } - state.sections = new_sections; - - // Remap relocs and remove dead ones - state.relocs.retain(|r| reachable[r.section.0]); - for reloc in &mut state.relocs { - reloc.section = remap[reloc.section.0]; - } - - // Remap globals - state.globals.retain(|_, def| match def { - SymbolDef::Dynamic { .. } => true, - SymbolDef::Defined { section, .. } => reachable[section.0], - }); - for def in state.globals.values_mut() { - if let SymbolDef::Defined { section, .. } = def { - *section = remap[section.0]; - } - } - - // Remap locals - state.locals.retain(|_, def| match def { - SymbolDef::Dynamic { .. } => true, - SymbolDef::Defined { section, .. } => reachable[section.0], - }); - for def in state.locals.values_mut() { - if let SymbolDef::Defined { section, .. } = def { - *section = remap[section.0]; - } - } - - // Remap tls_sections - state.tls_sections.retain(|idx| reachable[idx.0]); - for idx in &mut state.tls_sections { - *idx = remap[idx.0]; - } -} - -// ── Allocator shim synthesis ───────────────────────────────────────────── -// rustc normally generates these during final linking. We synthesize them -// as real code sections: each __rust_X is a `jmp __rdl_X` trampoline, -// and __rust_no_alloc_shim_is_unstable_v2 is a single `ret`. - -/// An item of the compiler-internal `___rustc` crate, split out of its v0 -/// mangling: `_RNvCs_7___rustc_`. -/// -/// The disambiguator is a rustc build hash that changes every time `rust/` is -/// rebuilt, so it is matched wild — a hash frozen into a string literal has no -/// way to announce that it has gone stale, and the symptom when it does is an -/// undefined symbol far from the cause. **The name never is**: the same -/// sysroot carries `__rust_start_panic`, `__rust_drop_panic`, -/// `__rust_foreign_exception`, `__rust_panic_cleanup`, `__rust_abort` and -/// `__rust_probestack`, and none of them is an allocator shim. -struct RustcItem<'a> { - disambiguator: &'a str, - name: &'a str, -} - -fn parse_rustc_item(symbol: &str) -> Option> { - let rest = symbol.strip_prefix("_RNvCs")?; - let (disambiguator, rest) = rest.split_once('_')?; - let rest = rest.strip_prefix("7___rustc")?; - let digits = rest.len() - rest.trim_start_matches(|c: char| c.is_ascii_digit()).len(); - let len: usize = rest.get(..digits)?.parse().ok()?; - // v0 separates the length from an identifier that itself starts with `_` - // by another `_`, which every name below does. - let name = rest.get(digits..)?.strip_prefix('_')?; - (name.len() == len).then_some(RustcItem { disambiguator, name }) -} - -fn mangle_rustc_item(disambiguator: &str, name: &str) -> String { - format!("_RNvCs{disambiguator}_7___rustc{}_{name}", name.len()) -} - -/// Each shim rustc synthesizes at final link time, with the -/// `#[global_allocator]` symbol it forwards to. A closed list. -const ALLOC_SHIMS: &[(&str, &str)] = &[ - ("__rust_alloc", "__rdl_alloc"), - ("__rust_dealloc", "__rdl_dealloc"), - ("__rust_realloc", "__rdl_realloc"), - ("__rust_alloc_zeroed", "__rdl_alloc_zeroed"), - ("__rust_alloc_error_handler", "__rdl_alloc_error_handler"), -]; - -const SHIM_NO_ALLOC_UNSTABLE: &str = "__rust_no_alloc_shim_is_unstable_v2"; - -/// A shim body, padded to 16 with `int3`. -fn define_shim(state: &mut LinkState, symbol: &str, mut code: Vec) -> SectionIdx { - code.resize(16, 0xCC); - let sec_idx = SectionIdx(state.sections.len()); - state.sections.push(InputSection { - name: format!(".text.{symbol}"), - data: code, - align: 16, - size: 16, - vaddr: None, - kind: SectionKind::Code, - merge: false, - strings: false, - entsize: 0, - }); - state - .globals - .insert(symbol.to_string(), SymbolDef::Defined { section: sec_idx, value: 0, size: 0 }); - sec_idx -} - -pub(crate) fn synthesize_alloc_shims(state: &mut LinkState) { - // Only symbols something references and nothing defines. Ordered, not - // hashed: iterating this is the order the sections reach the output. - let undefined: BTreeSet = state - .relocs - .iter() - .map(|r| r.target.name().to_string()) - .filter(|name| !state.globals.contains_key(name.as_str())) - .collect(); - - for symbol in &undefined { - let Some(item) = parse_rustc_item(symbol) else { continue }; - if item.name == SHIM_NO_ALLOC_UNSTABLE { - define_shim(state, symbol, vec![0xC3]); - continue; - } - let Some(&(_, target)) = ALLOC_SHIMS.iter().find(|(shim, _)| *shim == item.name) else { - continue; - }; - // `jmp rel32` - let sec_idx = define_shim(state, symbol, vec![0xE9, 0, 0, 0, 0]); - state.relocs.push(InputReloc { - section: sec_idx, - offset: 1, - r_type: RelocType::X86Plt32, - target: SymbolRef::Global(mangle_rustc_item(item.disambiguator, target)), - addend: -4, - subtrahend: None, - }); - } -} - -/// Merge SHF_MERGE|SHF_STRINGS sections with entsize=1 (null-terminated strings). -/// Deduplicates identical strings across input sections with the same name. -/// Updates symbol values and relocation addends to reflect merged offsets. -pub(crate) fn merge_string_sections(state: &mut LinkState) { - // Find sections eligible for string merging - let merge_indices: Vec = (0..state.sections.len()) - .filter(|&i| state.sections[i].merge && state.sections[i].strings && state.sections[i].entsize == 1) - .map(SectionIdx) - .collect(); - - if merge_indices.is_empty() { return; } - - // Group by section name - let mut groups: BTreeMap> = BTreeMap::new(); - for &idx in &merge_indices { - groups.entry(state.sections[idx].name.clone()).or_default().push(idx); - } - - // For each group with more than one section, merge them - // offset_remap: (old_section_idx, old_offset) → new_offset in merged section - let mut offset_remap: HashMap<(SectionIdx, u64), u64> = HashMap::new(); - let mut replaced_sections: HashSet = HashSet::new(); - - for group in groups.values() { - if group.len() < 2 { continue; } - - // Parse strings from all sections and intern them. - // Keys borrow from section data (not modified until after the loop). - let mut string_map: HashMap<&[u8], u64> = HashMap::new(); - let mut merged_data = Vec::new(); - - for &sec_idx in group { - let data = &state.sections[sec_idx].data; - let mut pos = 0; - while pos < data.len() { - // Find null terminator - let end = data[pos..].iter().position(|&b| b == 0) - .map(|p| pos + p + 1) - .unwrap_or(data.len()); - let string = &data[pos..end]; - - let new_offset = if let Some(&existing_off) = string_map.get(string) { - existing_off - } else { - let off = merged_data.len() as u64; - string_map.insert(string, off); - merged_data.extend_from_slice(string); - off - }; - - offset_remap.insert((sec_idx, pos as u64), new_offset); - pos = end; - } - } - drop(string_map); - - // Replace the first section with merged data; mark the rest for removal - let keep_idx = group[0]; - state.sections[keep_idx].data = merged_data; - state.sections[keep_idx].size = state.sections[keep_idx].data.len() as u64; - - for &sec_idx in &group[1..] { - replaced_sections.insert(sec_idx); - } - } - - if replaced_sections.is_empty() && offset_remap.is_empty() { return; } - - let merge_set: HashSet = merge_indices.iter().copied().collect(); - - // Save old (section, value) for symbols in merge sections before remapping. - // Only these are needed for relocation addend updates below. - let mut old_global_defs: HashMap = HashMap::new(); - for (name, def) in &state.globals { - if let SymbolDef::Defined { section, value, .. } = def { - if merge_set.contains(section) { - old_global_defs.insert(name.clone(), (*section, *value)); - } - } - } - let mut old_local_defs: HashMap<(ObjIdx, String), (SectionIdx, u64)> = HashMap::new(); - for ((oi, name), def) in &state.locals { - if let SymbolDef::Defined { section, value, .. } = def { - if merge_set.contains(section) { - old_local_defs.insert((*oi, name.clone()), (*section, *value)); - } - } - } - - // Update symbol definitions pointing into merged sections - for def in state.globals.values_mut() { - if let SymbolDef::Defined { section, value, .. } = def { - if let Some(&new_off) = offset_remap.get(&(*section, *value)) { - let old_name = &state.sections[*section].name; - if let Some(group) = groups.get(old_name) { - *section = group[0]; - } - *value = new_off; - } - } - } - for def in state.locals.values_mut() { - if let SymbolDef::Defined { section, value, .. } = def { - if let Some(&new_off) = offset_remap.get(&(*section, *value)) { - let old_name = &state.sections[*section].name; - if let Some(group) = groups.get(old_name) { - *section = group[0]; - } - *value = new_off; - } - } - } - - // Update relocation addends: when a relocation targets a symbol in a merged - // section, the addend may encode an offset into that section that needs remapping. - // PC-relative relocations (PC32, PLT32) include a -4 adjustment in the addend - // (for the 4-byte displacement size), so we must compensate when computing the - // logical offset into the section. - for reloc in &mut state.relocs { - let old_sv = match &reloc.target { - SymbolRef::Global(name) => old_global_defs.get(name), - SymbolRef::Local(oi, name) => old_global_defs.get(name) - .or_else(|| old_local_defs.get(&(*oi, name.clone()))), - }; - let Some(&(old_sec, old_val)) = old_sv else { continue; }; - let pc_adjust: i64 = match reloc.r_type { - RelocType::X86Pc32 | RelocType::X86Plt32 => 4, - _ => 0, - }; - let logical_offset = (old_val as i64 + reloc.addend + pc_adjust) as u64; - if let Some(&new_offset) = offset_remap.get(&(old_sec, logical_offset)) { - let new_def = match &reloc.target { - SymbolRef::Global(name) => state.globals.get(name), - SymbolRef::Local(oi, name) => state.globals.get(name) - .or_else(|| state.locals.get(&(*oi, name.clone()))), - }; - if let Some(SymbolDef::Defined { value: new_val, .. }) = new_def { - reloc.addend = new_offset as i64 - *new_val as i64 - pc_adjust; - } - } - } - - // Remove replaced sections and remap all indices - if !replaced_sections.is_empty() { - let mut index_map: Vec> = Vec::with_capacity(state.sections.len()); - let mut new_idx = 0; - for i in 0..state.sections.len() { - if replaced_sections.contains(&SectionIdx(i)) { - index_map.push(None); - } else { - index_map.push(Some(SectionIdx(new_idx))); - new_idx += 1; - } - } - - // Remap section indices in symbols - for def in state.globals.values_mut() { - if let SymbolDef::Defined { section, .. } = def { - if let Some(new) = index_map.get(section.0).and_then(|x| *x) { - *section = new; - } - } - } - for def in state.locals.values_mut() { - if let SymbolDef::Defined { section, .. } = def { - if let Some(new) = index_map.get(section.0).and_then(|x| *x) { - *section = new; - } - } - } - - // Remap section indices in relocations, drop relocs targeting removed sections - state.relocs.retain(|r| { - index_map.get(r.section.0).and_then(|x| *x).is_some() - }); - for reloc in &mut state.relocs { - if let Some(new) = index_map.get(reloc.section.0).and_then(|x| *x) { - reloc.section = new; - } - } - - // Remap TLS section indices - state.tls_sections = state.tls_sections.iter() - .filter_map(|idx| index_map.get(idx.0).and_then(|x| *x)) - .collect(); - - // Remove the sections - let mut i = 0; - state.sections.retain(|_| { - let keep = !replaced_sections.contains(&SectionIdx(i)); - i += 1; - keep - }); - } -} - -/// Collect unique symbols in insertion order (deduplicating with a HashSet). -pub(crate) fn collect_unique_symbols<'a>( - relocs: impl Iterator, - predicate: impl Fn(&InputReloc) -> bool, -) -> Vec { - let mut seen = HashSet::new(); - let mut result = Vec::new(); - for reloc in relocs { - if predicate(reloc) && seen.insert(reloc.target.clone()) { - result.push(reloc.target.clone()); - } - } - result -} diff --git a/toyos-ld/src/emit_elf.rs b/toyos-ld/src/emit_elf.rs deleted file mode 100644 index e119a0a372b..00000000000 --- a/toyos-ld/src/emit_elf.rs +++ /dev/null @@ -1,1547 +0,0 @@ -use object::write::elf::{FileHeader, ProgramHeader, SectionHeader, SectionIndex, Sym, Rel, Writer, SymbolIndex}; -use object::write::StringId; -use object::Endianness; -use crate::collect::{collect_unique_symbols, InputSection, LinkState, RelocType, SectionKind, SymbolDef, SymbolRef}; -use crate::reloc::{RelocOutput, resolve_symbol, tpoff}; -use crate::{align_up, classify_sections, LinkError, BASE_VADDR, PAGE_SIZE}; -use object::elf; -use std::collections::{BTreeMap, HashMap, HashSet}; - -// ── Layout ─────────────────────────────────────────────────────────────── - -pub(crate) struct ElfLayout { - pub(crate) base_addr: u64, - pub(crate) rx_start: u64, - pub(crate) rx_end: u64, - pub(crate) rw_start: u64, - pub(crate) rw_filesz: u64, - pub(crate) rw_end: u64, - pub(crate) tls_start: u64, - pub(crate) tls_filesz: u64, - pub(crate) tls_memsz: u64, - pub(crate) got: BTreeMap, - /// Shared LD GOT pair (16 bytes) for all TLSLD accesses in shared mode. - pub(crate) ld_got_pair: Option, - /// Per-symbol GD GOT pairs (16 bytes each) for TLSGD in shared mode. - pub(crate) gd_got: BTreeMap, - pub(crate) plt: BTreeMap, - pub(crate) plt_data: Vec, - pub(crate) plt_vaddr: u64, - pub(crate) dyn_got: BTreeMap, - pub(crate) init_array_vaddr: u64, - pub(crate) init_array_size: u64, - pub(crate) fini_array_vaddr: u64, - pub(crate) fini_array_size: u64, - pub(crate) eh_frame_hdr_vaddr: u64, - pub(crate) eh_frame_hdr_size: u64, - pub(crate) eh_frame_vaddr: u64, - pub(crate) build_id_note_vaddr: u64, -} - -/// Size of the .note.gnu.build-id section: namesz(4) + descsz(4) + type(4) + "GNU\0"(4) + 20-byte hash -pub(crate) const BUILD_ID_NOTE_SIZE: u64 = 36; -/// Offset of the hash descriptor within the note -const BUILD_ID_DESC_OFFSET: usize = 16; -/// Size of the hash descriptor (20 bytes, same as SHA-1) -const BUILD_ID_DESC_SIZE: usize = 20; - -// ── .eh_frame parsing ───────────────────────────────────────────────────── - -fn read_uleb128(data: &[u8], mut offset: usize) -> (u64, usize) { - let start = offset; - let mut result = 0u64; - let mut shift = 0; - loop { - let byte = data[offset]; - offset += 1; - result |= ((byte & 0x7F) as u64) << shift; - if byte & 0x80 == 0 { break; } - shift += 7; - } - (result, offset - start) -} - -fn read_sleb128(data: &[u8], mut offset: usize) -> (i64, usize) { - let start = offset; - let mut result = 0i64; - let mut shift = 0; - loop { - let byte = data[offset]; - offset += 1; - result |= ((byte & 0x7F) as i64) << shift; - shift += 7; - if byte & 0x80 == 0 { - if shift < 64 && byte & 0x40 != 0 { - result |= -(1i64 << shift); - } - break; - } - } - (result, offset - start) -} - -/// Size of an encoded pointer in bytes, based on the low 4 bits of the encoding. -fn eh_pointer_size(enc: u8) -> usize { - match enc & 0x0F { - 0x00 => 8, // DW_EH_PE_absptr (native = 8 on x86_64) - 0x02 | 0x0A => 2, // udata2 / sdata2 - 0x03 | 0x0B => 4, // udata4 / sdata4 - 0x04 | 0x0C => 8, // udata8 / sdata8 - // DWARF encoding low nibble: only 0x00-0x04 and 0x0A-0x0C are defined; - // remaining values are reserved. GCC/LLVM only emit standard encodings. - _ => 0, - } -} - -/// Parse a CIE record to extract the FDE pointer encoding ('R' augmentation). -/// `cie_data` starts at the first byte after the CIE_id field. -fn parse_cie_fde_encoding(cie_data: &[u8]) -> u8 { - let mut off = 0; - let version = cie_data[off]; off += 1; - - // Augmentation string (null-terminated) - let aug_start = off; - while off < cie_data.len() && cie_data[off] != 0 { off += 1; } - let augmentation = cie_data[aug_start..off].to_vec(); - off += 1; // skip null terminator - - // Code alignment factor (ULEB128) - let (_, n) = read_uleb128(cie_data, off); off += n; - // Data alignment factor (SLEB128) - let (_, n) = read_sleb128(cie_data, off); off += n; - // Return address register - if version == 1 { - off += 1; - } else { - let (_, n) = read_uleb128(cie_data, off); off += n; - } - - if augmentation.first() != Some(&b'z') { - return 0x00; // no augmentation data, assume absptr - } - - // Augmentation data length (ULEB128) - let (_aug_len, n) = read_uleb128(cie_data, off); off += n; - - // Parse augmentation data for each char after 'z' - for &c in &augmentation[1..] { - match c { - b'L' => { off += 1; } // LSDA encoding byte - b'P' => { - let enc = cie_data[off]; off += 1; - off += eh_pointer_size(enc); - } - b'R' => { - return cie_data[off]; - } - // Unknown augmentation char: stop parsing per DWARF spec - _ => break, - } - } - - 0x00 // default: absptr -} - -/// Count FDE entries in a single .eh_frame section's data. -fn count_fdes(data: &[u8]) -> usize { - let mut count = 0; - let mut offset = 0; - while offset + 4 <= data.len() { - let length = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()); - if length == 0 { break; } - let record_start = offset + 4; - let record_end = record_start + length as usize; - if record_end > data.len() { break; } - let cie_id = u32::from_le_bytes(data[record_start..record_start + 4].try_into().unwrap()); - if cie_id != 0 { count += 1; } - offset = record_end; - } - count -} - -/// Build .eh_frame_hdr content from relocated .eh_frame sections. -/// Returns the raw bytes for the .eh_frame_hdr section. -pub(crate) fn build_eh_frame_hdr(state: &LinkState, layout: &ElfLayout) -> Vec { - let hdr_vaddr = layout.eh_frame_hdr_vaddr; - if hdr_vaddr == 0 { return Vec::new(); } - - // Parse each .eh_frame section to find CIE encodings and FDE entries - let mut fdes: Vec<(u64, u64)> = Vec::new(); // (initial_location, fde_vaddr) - - for sec in &state.sections { - if sec.name != ".eh_frame" { continue; } - let data = &sec.data; - let base_vaddr = sec.vaddr.unwrap(); - - // First pass: build CIE offset → fde_encoding map - let mut cie_encodings: HashMap = HashMap::new(); - let mut offset = 0; - while offset + 4 <= data.len() { - let length = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()); - if length == 0 { break; } - let record_start = offset + 4; - let record_end = record_start + length as usize; - if record_end > data.len() { break; } - let cie_id = u32::from_le_bytes(data[record_start..record_start + 4].try_into().unwrap()); - if cie_id == 0 { - // CIE: parse to find FDE encoding - let enc = parse_cie_fde_encoding(&data[record_start + 4..record_end]); - cie_encodings.insert(offset, enc); - } - offset = record_end; - } - - // Second pass: extract FDE initial_location values - offset = 0; - while offset + 4 <= data.len() { - let length = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()); - if length == 0 { break; } - let record_start = offset + 4; - let record_end = record_start + length as usize; - if record_end > data.len() { break; } - let cie_id = u32::from_le_bytes(data[record_start..record_start + 4].try_into().unwrap()); - if cie_id != 0 { - // FDE: cie_pointer is relative to current position - let cie_offset = record_start - cie_id as usize; - let enc = cie_encodings.get(&cie_offset).copied().unwrap_or(0x1B); - let loc_offset = record_start + 4; - let app = enc & 0x70; - let initial_location = match enc & 0x0F { - 0x0B => { // sdata4 - let val = i32::from_le_bytes(data[loc_offset..loc_offset + 4].try_into().unwrap()) as i64; - match app { - 0x10 => (base_vaddr as i64 + loc_offset as i64 + val) as u64, // pcrel - _ => val as u64, - } - } - 0x03 => { // udata4 - let val = u32::from_le_bytes(data[loc_offset..loc_offset + 4].try_into().unwrap()) as i64; - match app { - 0x10 => (base_vaddr as i64 + loc_offset as i64 + val) as u64, - _ => val as u64, - } - } - _ => { - // Default: assume sdata4 pcrel (most common on x86_64) - let val = i32::from_le_bytes(data[loc_offset..loc_offset + 4].try_into().unwrap()) as i64; - (base_vaddr as i64 + loc_offset as i64 + val) as u64 - } - }; - let fde_vaddr = base_vaddr + offset as u64; - fdes.push((initial_location, fde_vaddr)); - } - offset = record_end; - } - } - - fdes.sort_by_key(|&(loc, _)| loc); - - // Build .eh_frame_hdr - let mut hdr = vec![ - 1u8, // version - 0x1B, // eh_frame_ptr encoding: DW_EH_PE_pcrel | DW_EH_PE_sdata4 - 0x03, // fde_count encoding: DW_EH_PE_udata4 - 0x3B, // table encoding: DW_EH_PE_datarel | DW_EH_PE_sdata4 - ]; - - // eh_frame_ptr: PC-relative offset from this field to .eh_frame start - let eh_frame_ptr = (layout.eh_frame_vaddr as i64 - (hdr_vaddr as i64 + 4)) as i32; - hdr.extend_from_slice(&eh_frame_ptr.to_le_bytes()); - - // fde_count - hdr.extend_from_slice(&(fdes.len() as u32).to_le_bytes()); - - // Sorted table: (initial_location, fde_offset) both datarel from eh_frame_hdr start - for &(initial_location, fde_vaddr) in &fdes { - let loc_rel = (initial_location as i64 - hdr_vaddr as i64) as i32; - let fde_rel = (fde_vaddr as i64 - hdr_vaddr as i64) as i32; - hdr.extend_from_slice(&loc_rel.to_le_bytes()); - hdr.extend_from_slice(&fde_rel.to_le_bytes()); - } - - hdr -} - -pub(crate) fn layout_elf(state: &mut LinkState, base_addr: u64, entry_name: Option<&str>, build_id: bool) -> ElfLayout { - let headers_size = 0x1000u64; - let buckets = classify_sections(state); - - let mut cursor = base_addr + headers_size; - - let rx_start = cursor; - for &idx in &buckets.rx { - let sec = &mut state.sections[idx]; - cursor = align_up(cursor, sec.align); - sec.vaddr = Some(cursor); - cursor += sec.size; - } - - // PLT stubs for dynamic symbols (PIE mode only). - // Deduplicate by name and normalize to Global, since Local and Global - // variants of the same symbol name refer to the same dynamic import. - // Exclude TLS symbols — they need TPOFF64 runtime relocs, not GLOB_DAT, - // so they use the regular GOT instead of dyn_got. - let dyn_syms_raw = collect_unique_symbols( - state.relocs.iter(), - |r| { - let name = r.target.name(); - state.dynamic_imports.contains(name) - && !matches!(state.globals.get(name), Some(SymbolDef::Dynamic { is_tls: true, .. })) - }, - ); - let mut dyn_seen = std::collections::HashSet::new(); - let mut dyn_syms: Vec = Vec::new(); - for sym in &dyn_syms_raw { - if dyn_seen.insert(sym.name().to_string()) { - dyn_syms.push(SymbolRef::Global(sym.name().to_string())); - } - } - if let Some(entry) = entry_name { - let entry_ref = SymbolRef::Global(entry.to_string()); - if state.dynamic_imports.contains(entry) && !dyn_syms.contains(&entry_ref) { - dyn_syms.push(entry_ref); - } - } - - const PLT_STUB_SIZE: u64 = 6; - let plt_vaddr = if dyn_syms.is_empty() { cursor } else { align_up(cursor, 16) }; - cursor = plt_vaddr + dyn_syms.len() as u64 * PLT_STUB_SIZE; - - // .eh_frame_hdr: placed after PLT stubs, before page alignment - let mut eh_frame_vaddr = 0u64; - let mut fde_count = 0usize; - for &idx in &buckets.rx { - if state.sections[idx].name == ".eh_frame" { - if fde_count == 0 { eh_frame_vaddr = state.sections[idx].vaddr.unwrap(); } - fde_count += count_fdes(&state.sections[idx].data); - } - } - let (eh_frame_hdr_vaddr, eh_frame_hdr_size) = if fde_count > 0 { - let vaddr = align_up(cursor, 4); - let size = (12 + fde_count * 8) as u64; - cursor = vaddr + size; - (vaddr, size) - } else { - (0, 0) - }; - - // .note.gnu.build-id: placed after .eh_frame_hdr - let build_id_note_vaddr = if build_id { - let vaddr = align_up(cursor, 4); - cursor = vaddr + BUILD_ID_NOTE_SIZE; - vaddr - } else { 0 }; - - let rx_end = align_up(cursor, PAGE_SIZE); - - cursor = rx_end; - let rw_start = cursor; - - // Place PROGBITS RW sections first - for &idx in &buckets.rw { - if state.sections[idx].kind.is_nobits() { continue; } - let sec = &mut state.sections[idx]; - cursor = align_up(cursor, sec.align); - sec.vaddr = Some(cursor); - cursor += sec.size; - } - - // Compute init/fini array ranges (sections are contiguous due to RW sort order) - let mut init_array_vaddr = 0u64; - let mut init_array_size = 0u64; - let mut fini_array_vaddr = 0u64; - let mut fini_array_size = 0u64; - for &idx in &buckets.rw { - let sec = &state.sections[idx]; - let Some(sec_vaddr) = sec.vaddr else { continue; }; - if sec.kind == SectionKind::InitArray { - if init_array_size == 0 { init_array_vaddr = sec_vaddr; } - init_array_size = (sec_vaddr + sec.size) - init_array_vaddr; - } else if sec.kind == SectionKind::FiniArray { - if fini_array_size == 0 { fini_array_vaddr = sec_vaddr; } - fini_array_size = (sec_vaddr + sec.size) - fini_array_vaddr; - } - } - - let is_shared = entry_name.is_none(); - - // 8-byte GOT entries: GOTPCREL, GOTTPOFF, and PIE-mode GD→IE (dynamic TLS imports). - // In shared mode, all TLS GD/LD 16-byte pairs are allocated separately below. - let got_symbols = collect_unique_symbols(state.relocs.iter(), |r| { - matches!(r.r_type, - RelocType::X86Gotpcrel | RelocType::X86Gotpcrelx - | RelocType::X86RexGotpcrelx | RelocType::X86Gottpoff) - || (r.r_type == RelocType::X86Tlsgd && !is_shared - && state.dynamic_imports.contains(r.target.name())) - }); - - cursor = align_up(cursor, 8); - - // In shared mode, TLS GD/LD 16-byte GOT pairs live in their own address ranges, - // separate from the 8-byte per-symbol `got` map. This prevents DTPMOD64 from - // clobbering GOTTPOFF slots when both access the same symbol. - - let ld_got_pair: Option = if is_shared && state.relocs.iter().any(|r| r.r_type == RelocType::X86Tlsld) { - let slot = cursor; - cursor += 16; - Some(slot) - } else { - None - }; - - let mut gd_got: BTreeMap = BTreeMap::new(); - if is_shared { - let gd_symbols = collect_unique_symbols(state.relocs.iter(), |r| { - r.r_type == RelocType::X86Tlsgd - }); - for sym in &gd_symbols { - gd_got.insert(sym.clone(), cursor); - cursor += 16; - } - } - - let mut got = BTreeMap::new(); - let dyn_sym_names: std::collections::HashSet<&str> = dyn_syms.iter().map(|s| s.name()).collect(); - for sym in &got_symbols { - if dyn_sym_names.contains(sym.name()) { continue; } - got.insert(sym.clone(), cursor); - cursor += 8; - } - - let mut dyn_got = BTreeMap::new(); - for sym in &dyn_syms { - dyn_got.insert(sym.clone(), cursor); - cursor += 8; - } - - // TLS file-backed sections (.tdata) placed within RW segment for correct - // PT_TLS p_offset. PT_TLS overlaps with PT_LOAD(RW) in the file. - let tls_start = if buckets.tls.is_empty() { cursor } else { align_up(cursor, 64) }; - let mut tls_cursor = tls_start; - for &idx in &buckets.tls { - if state.sections[idx].kind == SectionKind::TlsBss { continue; } - let sec = &mut state.sections[idx]; - tls_cursor = align_up(tls_cursor, sec.align); - sec.vaddr = Some(tls_cursor); - tls_cursor += sec.size; - } - cursor = tls_cursor; - let tls_filesz = tls_cursor.saturating_sub(tls_start); - - let rw_filesz = cursor - rw_start; - - // Place NOBITS RW sections (.bss) after all file-backed data - for &idx in &buckets.rw { - if !state.sections[idx].kind.is_nobits() { continue; } - let sec = &mut state.sections[idx]; - cursor = align_up(cursor, sec.align); - sec.vaddr = Some(cursor); - cursor += sec.size; - } - - // TLS BSS (.tbss) — only occupies space in the TLS block, not regular address space - for &idx in &buckets.tls { - if state.sections[idx].kind != SectionKind::TlsBss { continue; } - let sec = &mut state.sections[idx]; - tls_cursor = align_up(tls_cursor, sec.align); - sec.vaddr = Some(tls_cursor); - tls_cursor += sec.size; - } - let tls_memsz = tls_cursor.saturating_sub(tls_start); - - let rw_end = align_up(cursor, PAGE_SIZE); - - // Build PLT stub code: `jmp *[rip + offset]` (FF 25 xx xx xx xx) - let mut plt = BTreeMap::new(); - let mut plt_data = Vec::new(); - for (i, sym) in dyn_syms.iter().enumerate() { - let stub_vaddr = plt_vaddr + i as u64 * PLT_STUB_SIZE; - plt.insert(sym.clone(), stub_vaddr); - let rip = stub_vaddr + 6; - let offset = (dyn_got[sym] as i64 - rip as i64) as i32; - plt_data.extend_from_slice(&[0xFF, 0x25]); - plt_data.extend_from_slice(&offset.to_le_bytes()); - } - - ElfLayout { - base_addr, rx_start, rx_end, rw_start, rw_filesz, rw_end, - tls_start, tls_filesz, tls_memsz, - got, ld_got_pair, gd_got, plt, plt_data, plt_vaddr, dyn_got, - init_array_vaddr, init_array_size, - fini_array_vaddr, fini_array_size, - eh_frame_hdr_vaddr, eh_frame_hdr_size, eh_frame_vaddr, - build_id_note_vaddr, - } -} - -// ── Helpers ────────────────────────────────────────────────────────────── - -/// GNU hash function for .gnu.hash section. -fn gnu_hash(name: &[u8]) -> u32 { - let mut h = 5381u32; - for &b in name { - h = h.wrapping_mul(33).wrapping_add(b as u32); - } - h -} - -/// Build the .note.gnu.build-id note content with a zero descriptor placeholder. -fn build_id_note_placeholder() -> Vec { - let mut note = Vec::with_capacity(BUILD_ID_NOTE_SIZE as usize); - note.extend_from_slice(&4u32.to_le_bytes()); // namesz - note.extend_from_slice(&(BUILD_ID_DESC_SIZE as u32).to_le_bytes()); // descsz - note.extend_from_slice(&3u32.to_le_bytes()); // type = NT_GNU_BUILD_ID - note.extend_from_slice(b"GNU\0"); // name - note.extend_from_slice(&[0u8; BUILD_ID_DESC_SIZE]); // descriptor (placeholder) - note -} - -/// Compute a 20-byte build-id hash from the output binary. -/// Uses multiple rounds of SipHash with different seeds. -pub(crate) fn compute_build_id(data: &[u8]) -> [u8; BUILD_ID_DESC_SIZE] { - use std::hash::{Hash, Hasher}; - use std::collections::hash_map::DefaultHasher; - let mut result = [0u8; BUILD_ID_DESC_SIZE]; - for chunk in 0..3u64 { - let mut hasher = DefaultHasher::new(); - chunk.hash(&mut hasher); - data.hash(&mut hasher); - let h = hasher.finish().to_le_bytes(); - let start = (chunk as usize) * 8; - let end = (start + 8).min(BUILD_ID_DESC_SIZE); - result[start..end].copy_from_slice(&h[..end - start]); - } - result -} - -/// Patch the build-id descriptor in an ELF binary at the given file offset. -pub(crate) fn patch_build_id(buf: &mut [u8], note_file_offset: usize) { - // Zero out the descriptor before computing hash (it's already zero for fresh emit) - let desc_start = note_file_offset + BUILD_ID_DESC_OFFSET; - let desc_end = desc_start + BUILD_ID_DESC_SIZE; - for b in &mut buf[desc_start..desc_end] { *b = 0; } - let hash = compute_build_id(buf); - buf[desc_start..desc_end].copy_from_slice(&hash); -} - -fn resolve_entry(state: &LinkState, entry_name: &str, plt: Option<&BTreeMap>) -> Result { - state - .globals - .get(entry_name) - .map(|def| match def { - SymbolDef::Dynamic { .. } => { - let entry_ref = SymbolRef::Global(entry_name.to_string()); - plt.and_then(|p| p.get(&entry_ref).copied()) - .ok_or_else(|| LinkError::MissingEntry(entry_name.to_string())) - } - SymbolDef::Defined { section, value, .. } => { - Ok(state.sections[*section].vaddr.unwrap() + value) - } - }) - .unwrap_or_else(|| Err(LinkError::MissingEntry(entry_name.to_string()))) -} - -/// Map an input section to an output section index (1 = .text, 2 = .data). -fn output_section_index(sec: &InputSection, text_idx: SectionIndex, data_idx: SectionIndex) -> SectionIndex { - if sec.kind.is_writable() || sec.kind.is_nobits() || sec.kind.is_tls() { - data_idx - } else { - text_idx - } -} - -/// A prepared symbol for the .symtab output. -struct SymEntry { - name_id: StringId, - st_value: u64, - st_size: u64, - st_info: u8, - section_idx: SectionIndex, - is_local: bool, -} - -/// Collect symbols from LinkState and prepare them for .symtab emission. -/// Returns (entries sorted locals-first, num_local including null symbol). -fn collect_symtab_entries<'a>( - state: &'a LinkState, - w: &mut Writer<'a>, - text_idx: SectionIndex, - data_idx: SectionIndex, -) -> (Vec, u32) { - let mut entries = Vec::new(); - - // Locals (skip __section_sym_* — internal linker bookkeeping, not real symbols) - for ((_, name), def) in &state.locals { - if name.starts_with("__section_sym_") { continue; } - let SymbolDef::Defined { section, value, size } = def else { continue; }; - let sec = &state.sections[*section]; - let st_value = sec.vaddr.unwrap() + value; - let out_idx = output_section_index(sec, text_idx, data_idx); - let st_type = if sec.kind.is_writable() || sec.kind.is_nobits() || sec.kind.is_tls() { - elf::STT_OBJECT - } else { - elf::STT_FUNC - }; - entries.push(SymEntry { - name_id: w.add_string(name.as_bytes()), - st_value, - st_size: *size, - st_info: (elf::STB_LOCAL << 4) | st_type, - section_idx: out_idx, - is_local: true, - }); - } - - for (name, def) in &state.globals { - let SymbolDef::Defined { section, value, size } = def else { continue; }; - let sec = &state.sections[*section]; - let st_value = sec.vaddr.unwrap() + value; - let out_idx = output_section_index(sec, text_idx, data_idx); - let st_type = if sec.kind.is_writable() || sec.kind.is_nobits() || sec.kind.is_tls() { - elf::STT_OBJECT - } else { - elf::STT_FUNC - }; - entries.push(SymEntry { - name_id: w.add_string(name.as_bytes()), - st_value, - st_size: *size, - st_info: (elf::STB_GLOBAL << 4) | st_type, - section_idx: out_idx, - is_local: false, - }); - } - - // Sort: locals first, then globals - entries.sort_by_key(|e| !e.is_local); - let num_local = entries.iter().filter(|e| e.is_local).count() as u32 + 1; // +1 for null symbol - - (entries, num_local) -} - -/// Reserve section indices and symbol indices for symtab/strtab. -/// Call after add_string, before reserve_shstrtab_section_index. -fn reserve_symtab_indices(w: &mut Writer, entries: &[SymEntry]) { - for e in entries { - w.reserve_symbol_index(Some(e.section_idx)); - } - w.reserve_symtab_section_index(); - w.reserve_strtab_section_index(); -} - -/// Reserve file ranges for symtab/strtab data. -/// Call after reserving loadable content, before reserve_shstrtab. -fn reserve_symtab_data(w: &mut Writer) { - w.reserve_symtab(); - w.reserve_strtab(); -} - -/// Write symtab/strtab data. -fn write_symtab(w: &mut Writer, entries: &[SymEntry]) { - w.write_null_symbol(); - for e in entries { - w.write_symbol(&Sym { - name: Some(e.name_id), - section: Some(e.section_idx), - st_info: e.st_info, - st_other: elf::STV_DEFAULT, - st_shndx: 0, - st_value: e.st_value, - st_size: e.st_size, - }); - } - w.write_strtab(); -} - -/// Write section data to the Writer in file-offset order. Skips NOBITS sections. -/// Only writes sections whose vaddr is in [vaddr_min, vaddr_max). -fn write_sections_data(w: &mut Writer, sections: &[InputSection], base: u64, vaddr_min: u64, vaddr_max: u64) { - let mut indices: Vec = (0..sections.len()) - .filter(|&i| { - let Some(vaddr) = sections[i].vaddr else { return false }; - !sections[i].data.is_empty() && !sections[i].kind.is_nobits() - && vaddr >= vaddr_min && vaddr < vaddr_max - }) - .collect(); - indices.sort_by_key(|&i| sections[i].vaddr.unwrap()); - for i in indices { - let file_off = (sections[i].vaddr.unwrap() - base) as usize; - w.pad_until(file_off); - w.write(§ions[i].data); - } -} - -// ── ELF emit modes ────────────────────────────────────────────────────── - -pub(crate) enum ElfEmitMode<'a> { - /// PIE executable (ET_DYN with entry point) - Pie { - entry_name: &'a str, - relocs: &'a RelocOutput, - eh_frame_hdr: &'a [u8], - }, - /// Static executable (ET_EXEC, GOT filled directly) - Static { - entry_name: &'a str, - }, - /// Shared library (ET_DYN, exports symbols, .gnu.hash) - Shared { - relocs: &'a RelocOutput, - eh_frame_hdr: &'a [u8], - }, -} - -// ── Unified ELF emitter ───────────────────────────────────────────────── - -pub(crate) fn emit_elf( - state: &LinkState, - layout: &ElfLayout, - mode: ElfEmitMode, -) -> Result, LinkError> { - // ── Derive parameters from mode ── - - let is_pie = matches!(mode, ElfEmitMode::Pie { .. }); - let is_static = matches!(mode, ElfEmitMode::Static { .. }); - let is_shared = matches!(mode, ElfEmitMode::Shared { .. }); - - let base = if is_static { layout.base_addr } else { BASE_VADDR }; - let e_type = if is_static { elf::ET_EXEC } else { elf::ET_DYN }; - let entry = match &mode { - ElfEmitMode::Pie { entry_name, .. } => resolve_entry(state, entry_name, Some(&layout.plt))?, - ElfEmitMode::Static { entry_name } => resolve_entry(state, entry_name, None)?, - ElfEmitMode::Shared { .. } => 0, - }; - let relocs = match &mode { - ElfEmitMode::Pie { relocs, .. } | ElfEmitMode::Shared { relocs, .. } => Some(*relocs), - ElfEmitMode::Static { .. } => None, - }; - let eh_frame_hdr: &[u8] = match &mode { - ElfEmitMode::Pie { eh_frame_hdr, .. } | ElfEmitMode::Shared { eh_frame_hdr, .. } => eh_frame_hdr, - ElfEmitMode::Static { .. } => &[], - }; - - let has_dynamic_libs = is_pie && !state.dynamic_libs.is_empty(); - let has_relocs = relocs.as_ref().is_some_and(|r| { - !r.relatives.is_empty() || !r.glob_dats.is_empty() || !r.tpoff64s.is_empty() - || !r.named_tpoff64s.is_empty() || !r.tpoff32s.is_empty() - }); - let needs_dynamic = is_shared || has_dynamic_libs - || (is_pie && (layout.init_array_size > 0 || layout.fini_array_size > 0 || has_relocs)); - let has_eh_frame_hdr = !eh_frame_hdr.is_empty(); - let has_build_id = layout.build_id_note_vaddr != 0; - let has_plt = (is_pie || is_shared) && !layout.plt_data.is_empty(); - let has_rela = relocs.is_some_and(|r| !r.relatives.is_empty()) - || relocs.is_some_and(|r| !r.glob_dats.is_empty()) - || relocs.is_some_and(|r| !r.tpoff64s.is_empty()) - || relocs.is_some_and(|r| !r.named_tpoff64s.is_empty()) - || relocs.is_some_and(|r| !r.tpoff32s.is_empty()); - let file_rw_end = layout.rw_start + layout.rw_filesz; - - let mut buf = Vec::new(); - let mut w = Writer::new(Endianness::Little, true, &mut buf); - - // ── Phase 1: Add strings and reserve ── - - let text_name = w.add_section_name(b".text"); - let data_name = w.add_section_name(b".data"); - let rela_name = if has_rela || is_pie || is_shared { - Some(w.add_section_name(b".rela.dyn")) - } else { - None - }; - let eh_frame_hdr_name = if has_eh_frame_hdr { - Some(w.add_section_name(b".eh_frame_hdr")) - } else { None }; - let build_id_name = if has_build_id { - Some(w.add_section_name(b".note.gnu.build-id")) - } else { None }; - - // Dynamic strings: PIE imports vs shared exports - let mut needed_str_ids = Vec::new(); - let mut import_str_ids: Vec<(String, StringId)> = Vec::new(); // PIE: imported symbols - let mut export_str_ids: Vec<(String, StringId, u64, u32, bool)> = Vec::new(); // Shared: exported symbols (name, strid, value, hash, is_tls) - let mut sym_to_writer_idx: HashMap = HashMap::new(); - - if has_dynamic_libs { - for lib in &state.dynamic_libs { - needed_str_ids.push(w.add_dynamic_string(lib.as_bytes())); - } - if let Some(relocs) = relocs { - for (_, sym_name) in relocs.glob_dats.iter() - .chain(relocs.named_tpoff64s.iter()) - .chain(relocs.named_dtpmod64s.iter()) - .chain(relocs.named_dtpoff64s.iter()) - { - if !sym_to_writer_idx.contains_key(sym_name) { - let str_id = w.add_dynamic_string(sym_name.as_bytes()); - import_str_ids.push((sym_name.clone(), str_id)); - sym_to_writer_idx.insert(sym_name.clone(), SymbolIndex(0)); - } - } - } - } - - let (gnu_hash_sym_count, gnu_hash_bucket_count, gnu_hash_bloom_count); - // Shared library import symbols (for GLOB_DAT — resolved at load time) - let mut shared_import_str_ids: Vec<(String, StringId)> = Vec::new(); - if is_shared { - let mut symbols: Vec<_> = state.globals.iter().collect(); - symbols.sort_by_key(|(name, _)| *name); - for (name, def) in &symbols { - let SymbolDef::Defined { section, value, .. } = def else { continue; }; - let str_id = w.add_dynamic_string(name.as_bytes()); - let is_tls = state.sections[*section].kind.is_tls(); - let st_value = if is_tls { - // TLS symbols: st_value is offset within the TLS segment - state.sections[*section].vaddr.unwrap() - layout.tls_start + *value - } else { - state.sections[*section].vaddr.unwrap() + value - }; - let hash = gnu_hash(name.as_bytes()); - export_str_ids.push((name.to_string(), str_id, st_value, hash, is_tls)); - } - // Add undefined (imported) symbols that have dynamic relocations - if let Some(relocs) = relocs { - for (_, sym_name) in relocs.glob_dats.iter() - .chain(relocs.named_tpoff64s.iter()) - .chain(relocs.named_dtpmod64s.iter()) - .chain(relocs.named_dtpoff64s.iter()) - { - if !sym_to_writer_idx.contains_key(sym_name) { - let str_id = w.add_dynamic_string(sym_name.as_bytes()); - shared_import_str_ids.push((sym_name.clone(), str_id)); - sym_to_writer_idx.insert(sym_name.clone(), SymbolIndex(0)); // placeholder - } - } - } - gnu_hash_sym_count = export_str_ids.len() as u32; - gnu_hash_bucket_count = gnu_hash_sym_count.max(1); - gnu_hash_bloom_count = 1u32; - export_str_ids.sort_by_key(|&(_, _, _, h, _)| h % gnu_hash_bucket_count); - } else { - gnu_hash_sym_count = 0; - gnu_hash_bucket_count = 0; - gnu_hash_bloom_count = 0; - } - - // Metadata section names (shared only) - let mut meta_names: Vec = Vec::new(); - if is_shared { - for (name, _) in &state.metadata { - meta_names.push(w.add_section_name(name.as_bytes())); - } - } - - // Reserve section indices - w.reserve_null_section_index(); - let text_sec_idx = w.reserve_section_index(); // .text - let data_sec_idx = w.reserve_section_index(); // .data - - if rela_name.is_some() && !is_shared { - // PIE: .rela.dyn comes before dynsym - } - - let dynsym_sec_idx = if has_dynamic_libs { - Some(w.reserve_dynsym_section_index()) - } else if is_shared { - w.reserve_section_index(); // .rela.dyn (shared reserves this first) - Some(w.reserve_dynsym_section_index()) - } else { - None - }; - if has_dynamic_libs { - w.reserve_dynstr_section_index(); - } - if is_pie { - w.reserve_section_index(); // .rela.dyn - } - if (has_dynamic_libs || (is_pie && needs_dynamic)) && needs_dynamic && !is_shared { - w.reserve_dynamic_section_index(); - } - if is_shared { - w.reserve_dynstr_section_index(); - w.reserve_dynamic_section_index(); - w.reserve_gnu_hash_section_index(); - } - if has_eh_frame_hdr { - w.reserve_section_index(); - } - if has_build_id { - w.reserve_section_index(); - } - if is_shared { - for _ in &state.metadata { - w.reserve_section_index(); - } - } - - let (sym_entries, num_local) = collect_symtab_entries(state, &mut w, text_sec_idx, data_sec_idx); - reserve_symtab_indices(&mut w, &sym_entries); - - w.reserve_shstrtab_section_index(); - - // Reserve dynamic symbol indices - let dynsym_count = if has_dynamic_libs { - import_str_ids.len() - } else if is_shared { - export_str_ids.len() + shared_import_str_ids.len() - } else { - 0 - }; - if dynsym_count > 0 || is_shared { - w.reserve_null_dynamic_symbol_index(); - // Not a walk of `import_str_ids`: `dynsym_count` counts the exports and - // the shared imports too, and `import_str_ids` is indexed only on the - // `has_dynamic_libs` arm. Iterating that vector instead would end the - // loop early for a shared object, which is a different file. - #[allow(clippy::needless_range_loop)] - for i in 0..dynsym_count { - let idx = w.reserve_dynamic_symbol_index(); - if has_dynamic_libs { - sym_to_writer_idx.insert(import_str_ids[i].0.clone(), idx); - } - // Track writer indices for shared library import symbols - if is_shared && i >= export_str_ids.len() { - let import_i = i - export_str_ids.len(); - sym_to_writer_idx.insert(shared_import_str_ids[import_i].0.clone(), idx); - } - } - } - - // Reserve file layout - w.reserve_file_header(); - let phdr_count = 2 - + if layout.tls_memsz > 0 { 1 } else { 0 } - + if needs_dynamic { 2 } else { 0 } - + if has_eh_frame_hdr { 1 } else { 0 } - + if has_build_id { 1 } else { 0 }; - w.reserve_program_headers(phdr_count as u32); - - w.reserve_until((file_rw_end - base) as usize); - - let rela_count = if let Some(relocs) = relocs { - relocs.relatives.len() + relocs.glob_dats.len() - + relocs.tpoff64s.len() + relocs.named_tpoff64s.len() + relocs.tpoff32s.len() - + relocs.dtpmod64s.len() + relocs.dtpoff64s.len() - + relocs.named_dtpmod64s.len() + relocs.named_dtpoff64s.len() - } else { 0 }; - let rela_size = rela_count as u64 * 24; - - // Pad file so dynamic metadata starts past rw_end, avoiding virtual address - // overlap between the RW segment's .bss and the R segment's dynsym/dynstr/dynamic. - if needs_dynamic { - let rw_end_off = (layout.rw_end - base) as usize; - if w.reserved_len() < rw_end_off { - w.reserve_until(rw_end_off); - } - } - - // Dynamic metadata reservations - let (dynsym_off, dynstr_off, gnu_hash_off); - if has_dynamic_libs || is_shared { - dynsym_off = w.reserve_dynsym() as u64; - dynstr_off = w.reserve_dynstr() as u64; - } else { - dynsym_off = 0; - dynstr_off = 0; - } - if is_shared { - gnu_hash_off = w.reserve_gnu_hash(gnu_hash_bloom_count, gnu_hash_bucket_count, gnu_hash_sym_count) as u64; - } else { - gnu_hash_off = 0; - } - - // For shared libraries, reserve RELA before the dynamic section so it's - // within the R PT_LOAD segment and accessible from the loaded image. - let rela_off_shared = if is_shared && rela_count > 0 { - w.reserve_relocations(rela_count, true) as u64 - } else { - 0 - }; - - let (dynamic_count, dynamic_off, dyn_segment_end); - // For non-shared dynamic binaries, reserve relocations before the dynamic - // section so they're in the same PT_LOAD segment and accessible at runtime. - let rela_before_dynamic = !is_shared && !has_dynamic_libs && needs_dynamic && rela_count > 0; - let rela_off_pre = if rela_before_dynamic { - w.reserve_relocations(rela_count, true) as u64 - } else { - 0 - }; - if needs_dynamic { - let mut dc = 1; // DT_NULL - if has_dynamic_libs { - dc += needed_str_ids.len() + 4; // DT_NEEDED * N + SYMTAB/STRTAB/STRSZ/SYMENT - } - if is_shared { - dc += 5; // SYMTAB + STRTAB + STRSZ + SYMENT + GNU_HASH - } - if rela_count > 0 { dc += 3; } // DT_RELA + DT_RELASZ + DT_RELAENT - if layout.init_array_size > 0 { dc += 2; } - if layout.fini_array_size > 0 { dc += 2; } - dynamic_count = dc; - dynamic_off = w.reserve_dynamic(dc) as u64; - dyn_segment_end = align_up(w.reserved_len() as u64, PAGE_SIZE); - w.reserve_until(dyn_segment_end as usize); - } else { - dynamic_count = 0; - dynamic_off = 0; - dyn_segment_end = 0; - } - - let rela_off = if is_shared { - rela_off_shared - } else if rela_off_pre != 0 { - rela_off_pre - } else if rela_count > 0 || is_pie { - w.reserve_relocations(rela_count, true) as u64 - } else { - 0 - }; - - // Metadata section data (shared only) - let mut meta_offsets = Vec::new(); - if is_shared { - for (_, data) in &state.metadata { - let off = w.reserve(data.len(), 8); - meta_offsets.push(off as u64); - } - } - - reserve_symtab_data(&mut w); - w.reserve_shstrtab(); - w.reserve_section_headers(); - - // ── Phase 2: Write ── - - w.write_file_header(&FileHeader { - os_abi: 0, - abi_version: 0, - e_type, - e_machine: elf::EM_X86_64, - e_entry: entry, - e_flags: 0, - }).unwrap(); - - w.write_align_program_headers(); - w.write_program_header(&ProgramHeader { - p_type: elf::PT_LOAD, - p_flags: elf::PF_R | elf::PF_X, - p_offset: 0, - p_vaddr: base, - p_paddr: base, - p_filesz: layout.rx_end - base, - p_memsz: layout.rx_end - base, - p_align: PAGE_SIZE, - }); - w.write_program_header(&ProgramHeader { - p_type: elf::PT_LOAD, - p_flags: elf::PF_R | elf::PF_W, - p_offset: layout.rw_start - base, - p_vaddr: layout.rw_start, - p_paddr: layout.rw_start, - p_filesz: layout.rw_filesz, - p_memsz: layout.rw_end - layout.rw_start, - p_align: PAGE_SIZE, - }); - if layout.tls_memsz > 0 { - w.write_program_header(&ProgramHeader { - p_type: elf::PT_TLS, - p_flags: elf::PF_R, - p_offset: layout.tls_start - base, - p_vaddr: layout.tls_start, - p_paddr: layout.tls_start, - p_filesz: layout.tls_filesz, - p_memsz: layout.tls_memsz, - p_align: 64, - }); - } - if has_eh_frame_hdr { - w.write_program_header(&ProgramHeader { - p_type: 0x6474_e550, // PT_GNU_EH_FRAME - p_flags: elf::PF_R, - p_offset: layout.eh_frame_hdr_vaddr - base, - p_vaddr: layout.eh_frame_hdr_vaddr, - p_paddr: layout.eh_frame_hdr_vaddr, - p_filesz: layout.eh_frame_hdr_size, - p_memsz: layout.eh_frame_hdr_size, - p_align: 4, - }); - } - if has_build_id { - w.write_program_header(&ProgramHeader { - p_type: elf::PT_NOTE, - p_flags: elf::PF_R, - p_offset: layout.build_id_note_vaddr - base, - p_vaddr: layout.build_id_note_vaddr, - p_paddr: layout.build_id_note_vaddr, - p_filesz: BUILD_ID_NOTE_SIZE, - p_memsz: BUILD_ID_NOTE_SIZE, - p_align: 4, - }); - } - if needs_dynamic { - let dyn_load_start = if has_dynamic_libs || is_shared { - dynsym_off - } else if rela_count > 0 { - rela_off - } else { - dynamic_off - }; - let dynamic_size = dynamic_count as u64 * 16; - w.write_program_header(&ProgramHeader { - p_type: elf::PT_LOAD, - p_flags: elf::PF_R, - p_offset: dyn_load_start, - p_vaddr: dyn_load_start, - p_paddr: dyn_load_start, - p_filesz: dyn_segment_end - dyn_load_start, - p_memsz: dyn_segment_end - dyn_load_start, - p_align: PAGE_SIZE, - }); - w.write_program_header(&ProgramHeader { - p_type: elf::PT_DYNAMIC, - p_flags: elf::PF_R, - p_offset: dynamic_off, - p_vaddr: dynamic_off, - p_paddr: dynamic_off, - p_filesz: dynamic_size, - p_memsz: dynamic_size, - p_align: 8, - }); - } - - // Write RX (text) segment sections, then PLT/eh_frame_hdr/build_id (also in - // the RX range), then RW (data) segment sections. Everything must be written - // in ascending vaddr order because the Writer only supports forward appending. - write_sections_data(&mut w, &state.sections, base, base, layout.rx_end); - if has_plt { - w.pad_until((layout.plt_vaddr - base) as usize); - w.write(&layout.plt_data); - } - if has_eh_frame_hdr { - w.pad_until((layout.eh_frame_hdr_vaddr - base) as usize); - w.write(eh_frame_hdr); - } - if has_build_id { - w.pad_until((layout.build_id_note_vaddr - base) as usize); - w.write(&build_id_note_placeholder()); - } - // Write non-TLS RW sections first (up to TLS start) - write_sections_data(&mut w, &state.sections, base, layout.rx_end, layout.tls_start); - - // Static mode: fill GOT entries directly (GOT is before TLS in the layout) - if is_static { - let gottpoff_syms: HashSet = state.relocs.iter() - .filter(|r| r.r_type == RelocType::X86Gottpoff) - .map(|r| r.target.clone()).collect(); - let mut got_entries: Vec<_> = layout.got.iter().collect(); - got_entries.sort_by_key(|(_, &vaddr)| vaddr); - for (sym_ref, &got_vaddr) in got_entries { - let sym_addr = resolve_symbol(state, sym_ref, None) - .ok_or_else(|| LinkError::UndefinedSymbols(vec![sym_ref.name().to_string()]))?; - let value = if gottpoff_syms.contains(sym_ref) { - tpoff(sym_addr, layout.tls_start, layout.tls_memsz) as u64 - } else { sym_addr }; - let file_off = (got_vaddr - base) as usize; - w.pad_until(file_off); - w.write(&value.to_le_bytes()); - } - } - - // PIE mode: write GOTTPOFF GOT entries directly (GOT is before TLS in the layout) - if let Some(relocs) = relocs { - let mut fills: Vec<_> = relocs.tpoff_fills.iter().collect(); - fills.sort_by_key(|&&(vaddr, _)| vaddr); - for &(got_vaddr, tp) in fills { - let file_off = (got_vaddr - base) as usize; - w.pad_until(file_off); - w.write(&(tp as u64).to_le_bytes()); - } - } - - // Now write TLS sections (after GOT fills to avoid truncation) - write_sections_data(&mut w, &state.sections, base, layout.tls_start, u64::MAX); - - w.pad_until((file_rw_end - base) as usize); - - // Pad to rw_end so dynamic metadata starts past .bss virtual addresses - if needs_dynamic { - let rw_end_off = (layout.rw_end - base) as usize; - w.pad_until(rw_end_off); - } - - if has_dynamic_libs { - w.write_null_dynamic_symbol(); - for (_, str_id) in &import_str_ids { - w.write_dynamic_symbol(&Sym { - name: Some(*str_id), - section: None, - st_info: (elf::STB_GLOBAL << 4) | elf::STT_NOTYPE, - st_other: elf::STV_DEFAULT, - st_shndx: 0, - st_value: 0, - st_size: 0, - }); - } - w.write_dynstr(); - } - if is_shared { - w.write_null_dynamic_symbol(); - for (_, str_id, st_value, _, is_tls) in &export_str_ids { - let st_type = if *is_tls { elf::STT_TLS } else { elf::STT_NOTYPE }; - w.write_dynamic_symbol(&Sym { - name: Some(*str_id), - section: None, - st_info: (elf::STB_GLOBAL << 4) | st_type, - st_other: elf::STV_DEFAULT, - st_shndx: 1, // defined - st_value: *st_value, - st_size: 0, - }); - } - // Undefined (imported) symbols — resolved at load time via GLOB_DAT - for (_, str_id) in &shared_import_str_ids { - w.write_dynamic_symbol(&Sym { - name: Some(*str_id), - section: None, - st_info: (elf::STB_GLOBAL << 4) | elf::STT_NOTYPE, - st_other: elf::STV_DEFAULT, - st_shndx: 0, // undefined - st_value: 0, - st_size: 0, - }); - } - w.write_dynstr(); - - let sym_hashes: Vec = export_str_ids.iter().map(|(_, _, _, h, _)| *h).collect(); - w.write_gnu_hash(1, 6, gnu_hash_bloom_count, gnu_hash_bucket_count, gnu_hash_sym_count, |i| sym_hashes[i as usize]); - - // Shared library RELA (reserved before dynamic section, must be written here) - if let Some(relocs) = relocs { - if rela_count > 0 { - w.write_align_relocation(); - for &(offset, addend) in &relocs.relatives { - w.write_relocation(true, &Rel { - r_offset: offset, - r_sym: 0, - r_type: elf::R_X86_64_RELATIVE, - r_addend: addend, - }); - } - for (got_vaddr, sym_name) in &relocs.glob_dats { - let sym_idx = sym_to_writer_idx[sym_name]; - w.write_relocation(true, &Rel { - r_offset: *got_vaddr, - r_sym: sym_idx.0, - r_type: elf::R_X86_64_GLOB_DAT, - r_addend: 0, - }); - } - for &(got_vaddr, addend) in &relocs.tpoff64s { - w.write_relocation(true, &Rel { - r_offset: got_vaddr, - r_sym: 0, - r_type: elf::R_X86_64_TPOFF64, - r_addend: addend, - }); - } - for (got_vaddr, sym_name) in &relocs.named_tpoff64s { - let sym_idx = sym_to_writer_idx[sym_name]; - w.write_relocation(true, &Rel { - r_offset: *got_vaddr, - r_sym: sym_idx.0, - r_type: elf::R_X86_64_TPOFF64, - r_addend: 0, - }); - } - for &(vaddr, addend) in &relocs.tpoff32s { - w.write_relocation(true, &Rel { - r_offset: vaddr, - r_sym: 0, - r_type: elf::R_X86_64_TPOFF32, - r_addend: addend, - }); - } - for &(got_vaddr, addend) in &relocs.dtpmod64s { - w.write_relocation(true, &Rel { - r_offset: got_vaddr, - r_sym: 0, - r_type: elf::R_X86_64_DTPMOD64, - r_addend: addend, - }); - } - for &(got_vaddr, addend) in &relocs.dtpoff64s { - w.write_relocation(true, &Rel { - r_offset: got_vaddr, - r_sym: 0, - r_type: elf::R_X86_64_DTPOFF64, - r_addend: addend, - }); - } - for (got_vaddr, sym_name) in &relocs.named_dtpmod64s { - let sym_idx = sym_to_writer_idx[sym_name]; - w.write_relocation(true, &Rel { - r_offset: *got_vaddr, - r_sym: sym_idx.0, - r_type: elf::R_X86_64_DTPMOD64, - r_addend: 0, - }); - } - for (got_vaddr, sym_name) in &relocs.named_dtpoff64s { - let sym_idx = sym_to_writer_idx[sym_name]; - w.write_relocation(true, &Rel { - r_offset: *got_vaddr, - r_sym: sym_idx.0, - r_type: elf::R_X86_64_DTPOFF64, - r_addend: 0, - }); - } - } - } - } - - // For non-shared dynamic binaries without DT_NEEDED, write RELA before dynamic - // section so relocations are within the PT_LOAD segment for the dynamic area. - if rela_before_dynamic { - if let Some(relocs) = relocs { - w.write_align_relocation(); - for &(offset, addend) in &relocs.relatives { - w.write_relocation(true, &Rel { - r_offset: offset, - r_sym: 0, - r_type: elf::R_X86_64_RELATIVE, - r_addend: addend, - }); - } - for (got_vaddr, sym_name) in &relocs.glob_dats { - let sym_idx = sym_to_writer_idx[sym_name]; - w.write_relocation(true, &Rel { - r_offset: *got_vaddr, - r_sym: sym_idx.0, - r_type: elf::R_X86_64_GLOB_DAT, - r_addend: 0, - }); - } - for &(got_vaddr, addend) in &relocs.tpoff64s { - w.write_relocation(true, &Rel { - r_offset: got_vaddr, - r_sym: 0, - r_type: elf::R_X86_64_TPOFF64, - r_addend: addend, - }); - } - for (got_vaddr, sym_name) in &relocs.named_tpoff64s { - let sym_idx = sym_to_writer_idx[sym_name]; - w.write_relocation(true, &Rel { - r_offset: *got_vaddr, - r_sym: sym_idx.0, - r_type: elf::R_X86_64_TPOFF64, - r_addend: 0, - }); - } - for &(vaddr, addend) in &relocs.tpoff32s { - w.write_relocation(true, &Rel { - r_offset: vaddr, - r_sym: 0, - r_type: elf::R_X86_64_TPOFF32, - r_addend: addend, - }); - } - } - } - - if needs_dynamic { - w.write_align_dynamic(); - if has_dynamic_libs { - for &str_id in &needed_str_ids { - w.write_dynamic_string(elf::DT_NEEDED, str_id); - } - w.write_dynamic(elf::DT_SYMTAB, dynsym_off); - w.write_dynamic(elf::DT_STRTAB, dynstr_off); - let strsz = w.dynstr_len() as u64; - w.write_dynamic(elf::DT_STRSZ, strsz); - w.write_dynamic(elf::DT_SYMENT, 24); - } - if is_shared { - w.write_dynamic(elf::DT_SYMTAB, dynsym_off); - w.write_dynamic(elf::DT_STRTAB, dynstr_off); - let strsz = w.dynstr_len() as u64; - w.write_dynamic(elf::DT_STRSZ, strsz); - w.write_dynamic(elf::DT_SYMENT, 24); - w.write_dynamic(elf::DT_GNU_HASH, gnu_hash_off); - } - if rela_count > 0 { - w.write_dynamic(elf::DT_RELA, rela_off); - w.write_dynamic(elf::DT_RELASZ, rela_size); - w.write_dynamic(elf::DT_RELAENT, 24); - } - if layout.init_array_size > 0 { - w.write_dynamic(elf::DT_INIT_ARRAY, layout.init_array_vaddr); - w.write_dynamic(elf::DT_INIT_ARRAYSZ, layout.init_array_size); - } - if layout.fini_array_size > 0 { - w.write_dynamic(elf::DT_FINI_ARRAY, layout.fini_array_vaddr); - w.write_dynamic(elf::DT_FINI_ARRAYSZ, layout.fini_array_size); - } - w.write_dynamic(elf::DT_NULL, 0); - w.pad_until(dyn_segment_end as usize); - } - - // Relocations (PIE / non-shared — shared writes RELA before dynamic section) - if !is_shared && !rela_before_dynamic { - if let Some(relocs) = relocs { - w.write_align_relocation(); - for &(offset, addend) in &relocs.relatives { - w.write_relocation(true, &Rel { - r_offset: offset, - r_sym: 0, - r_type: elf::R_X86_64_RELATIVE, - r_addend: addend, - }); - } - for (got_vaddr, sym_name) in &relocs.glob_dats { - let sym_idx = sym_to_writer_idx[sym_name]; - w.write_relocation(true, &Rel { - r_offset: *got_vaddr, - r_sym: sym_idx.0, - r_type: elf::R_X86_64_GLOB_DAT, - r_addend: 0, - }); - } - for &(got_vaddr, addend) in &relocs.tpoff64s { - w.write_relocation(true, &Rel { - r_offset: got_vaddr, - r_sym: 0, - r_type: elf::R_X86_64_TPOFF64, - r_addend: addend, - }); - } - for (got_vaddr, sym_name) in &relocs.named_tpoff64s { - let sym_idx = sym_to_writer_idx[sym_name]; - w.write_relocation(true, &Rel { - r_offset: *got_vaddr, - r_sym: sym_idx.0, - r_type: elf::R_X86_64_TPOFF64, - r_addend: 0, - }); - } - for &(vaddr, addend) in &relocs.tpoff32s { - w.write_relocation(true, &Rel { - r_offset: vaddr, - r_sym: 0, - r_type: elf::R_X86_64_TPOFF32, - r_addend: addend, - }); - } - } - } - - // Metadata sections (shared only) - if is_shared { - for (i, (_, data)) in state.metadata.iter().enumerate() { - w.pad_until(meta_offsets[i] as usize); - w.write(data); - } - } - - write_symtab(&mut w, &sym_entries); - - w.write_shstrtab(); - - w.write_null_section_header(); - w.write_section_header(&SectionHeader { - name: Some(text_name), - sh_type: elf::SHT_PROGBITS, - sh_flags: (elf::SHF_ALLOC | elf::SHF_EXECINSTR) as u64, - sh_addr: layout.rx_start, - sh_offset: layout.rx_start - base, - sh_size: layout.rx_end - layout.rx_start, - sh_link: 0, sh_info: 0, sh_addralign: 1, sh_entsize: 0, - }); - w.write_section_header(&SectionHeader { - name: Some(data_name), - sh_type: elf::SHT_PROGBITS, - sh_flags: (elf::SHF_ALLOC | elf::SHF_WRITE) as u64, - sh_addr: layout.rw_start, - sh_offset: layout.rw_start - base, - sh_size: layout.rw_filesz, - sh_link: 0, sh_info: 0, sh_addralign: 1, sh_entsize: 0, - }); - - if has_dynamic_libs { - w.write_dynsym_section_header(dynsym_off, 1); - w.write_dynstr_section_header(dynstr_off); - } - - if let Some(rela_name) = rela_name { - w.write_section_header(&SectionHeader { - name: Some(rela_name), - sh_type: elf::SHT_RELA, - sh_flags: elf::SHF_ALLOC as u64, - sh_addr: if needs_dynamic { rela_off } else { 0 }, - sh_offset: rela_off, - sh_size: rela_size, - sh_link: dynsym_sec_idx.map_or(0, |i| i.0), - sh_info: 0, - sh_addralign: 8, - sh_entsize: 24, - }); - } - - if needs_dynamic && !is_shared { - w.write_dynamic_section_header(dynamic_off); - } - if is_shared { - w.write_dynsym_section_header(dynsym_off, 1); - w.write_dynstr_section_header(dynstr_off); - w.write_dynamic_section_header(dynamic_off); - w.write_gnu_hash_section_header(gnu_hash_off); - } - if has_eh_frame_hdr { - w.write_section_header(&SectionHeader { - name: eh_frame_hdr_name, - sh_type: elf::SHT_PROGBITS, - sh_flags: elf::SHF_ALLOC as u64, - sh_addr: layout.eh_frame_hdr_vaddr, - sh_offset: layout.eh_frame_hdr_vaddr - base, - sh_size: layout.eh_frame_hdr_size, - sh_link: 0, sh_info: 0, sh_addralign: 4, sh_entsize: 0, - }); - } - if has_build_id { - w.write_section_header(&SectionHeader { - name: build_id_name, - sh_type: elf::SHT_NOTE, - sh_flags: elf::SHF_ALLOC as u64, - sh_addr: layout.build_id_note_vaddr, - sh_offset: layout.build_id_note_vaddr - base, - sh_size: BUILD_ID_NOTE_SIZE, - sh_link: 0, sh_info: 0, sh_addralign: 4, sh_entsize: 0, - }); - } - if is_shared { - for (i, (_, data)) in state.metadata.iter().enumerate() { - w.write_section_header(&SectionHeader { - name: Some(meta_names[i]), - sh_type: elf::SHT_PROGBITS, - sh_flags: 0, - sh_addr: 0, - sh_offset: meta_offsets[i], - sh_size: data.len() as u64, - sh_link: 0, sh_info: 0, sh_addralign: 1, sh_entsize: 0, - }); - } - } - - w.write_symtab_section_header(num_local); - w.write_strtab_section_header(); - w.write_shstrtab_section_header(); - - // Patch build-id descriptor with computed hash - if has_build_id { - patch_build_id(&mut buf, (layout.build_id_note_vaddr - base) as usize); - } - - Ok(buf) -} diff --git a/toyos-ld/src/emit_macho.rs b/toyos-ld/src/emit_macho.rs deleted file mode 100644 index e72ebb1e883..00000000000 --- a/toyos-ld/src/emit_macho.rs +++ /dev/null @@ -1,1294 +0,0 @@ -use crate::collect::{Arch, collect_unique_symbols, LinkState, RelocType, SectionKind, SymbolDef, SymbolRef}; -use crate::reloc::resolve_symbol; -use crate::{align_up, classify_sections, LinkError}; -use sha2::{Sha256, Digest}; -use std::collections::BTreeMap; -use std::mem::size_of; -use zerocopy::{FromZeros, Immutable, IntoBytes}; -use zerocopy::little_endian::{U16 as U16Le, U32 as U32Le, U64 as U64Le}; -use zerocopy::big_endian::{U32 as U32Be, U64 as U64Be}; - -// ── Mach-O constants ──────────────────────────────────────────────────── - -const MH_MAGIC_64: u32 = 0xFEEDFACF; -const CPU_TYPE_ARM64: u32 = 0x0100000C; -const CPU_SUBTYPE_ARM64_ALL: u32 = 0; -const CPU_TYPE_X86_64: u32 = 0x01000007; -const CPU_SUBTYPE_X86_64_ALL: u32 = 3; -const MH_EXECUTE: u32 = 2; -const MH_PIE: u32 = 0x0020_0000; -const MH_HAS_TLV_DESCRIPTORS: u32 = 0x0080_0000; -const MH_TWOLEVEL: u32 = 0x80; -const MH_DYLDLINK: u32 = 0x4; - -const LC_SEGMENT_64: u32 = 0x19; -const LC_SYMTAB: u32 = 0x02; -const LC_DYSYMTAB: u32 = 0x0B; -const LC_LOAD_DYLIB: u32 = 0x0C; -const LC_LOAD_DYLINKER: u32 = 0x0E; -const LC_MAIN: u32 = 0x80000028; -const LC_DYLD_INFO_ONLY: u32 = 0x80000022; -const LC_BUILD_VERSION: u32 = 0x32; -const LC_CODE_SIGNATURE: u32 = 0x1D; -const REBASE_OPCODE_SET_TYPE_IMM: u8 = 0x10; -const REBASE_TYPE_POINTER: u8 = 1; -const REBASE_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB: u8 = 0x20; -const REBASE_OPCODE_DO_REBASE_IMM_TIMES: u8 = 0x50; -const REBASE_OPCODE_DONE: u8 = 0x00; - -const BIND_OPCODE_SET_DYLIB_ORDINAL_IMM: u8 = 0x10; -const BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM: u8 = 0x40; -const BIND_OPCODE_SET_TYPE_IMM: u8 = 0x50; -const BIND_TYPE_POINTER: u8 = 1; -const BIND_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB: u8 = 0x70; -const BIND_OPCODE_DO_BIND: u8 = 0x90; -const BIND_OPCODE_DONE: u8 = 0x00; - -const S_REGULAR: u32 = 0x0; -const S_ZEROFILL: u32 = 0x1; -const S_NON_LAZY_SYMBOL_POINTERS: u32 = 0x6; -const S_THREAD_LOCAL_REGULAR: u32 = 0x11; -const S_THREAD_LOCAL_ZEROFILL: u32 = 0x12; -const S_THREAD_LOCAL_VARIABLES: u32 = 0x13; -const S_ATTR_PURE_INSTRUCTIONS: u32 = 0x8000_0000; -const S_ATTR_SOME_INSTRUCTIONS: u32 = 0x0000_0400; - -const VM_PROT_READ: u32 = 1; -const VM_PROT_WRITE: u32 = 2; -const VM_PROT_EXECUTE: u32 = 4; -const VM_PROT_READ_WRITE: u32 = VM_PROT_READ | VM_PROT_WRITE; -const VM_PROT_READ_EXECUTE: u32 = VM_PROT_READ | VM_PROT_EXECUTE; - -const N_EXT: u8 = 0x01; -const N_SECT: u8 = 0x0E; - -const PLATFORM_MACOS: u32 = 1; -const TOOL_LD: u32 = 3; -const MACOS_14_0: u32 = 0x000E0000; - -const PAGE_SIZE_ARM64: u64 = 0x4000; // 16KB -const PAGE_SIZE_X86_64: u64 = 0x1000; // 4KB -const PAGEZERO_SIZE: u64 = 0x1_0000_0000; // 4GB - -const SEGMENT_CMD_SIZE: u32 = size_of::() as u32; -const SECTION_HEADER_SIZE: u32 = size_of::() as u32; -const MACHO_HEADER_SIZE: u32 = size_of::() as u32; - -// ── Mach-O binary structs ─────────────────────────────────────────────── - -#[repr(C)] -#[derive(IntoBytes, Immutable)] -struct MachHeader64 { - magic: U32Le, - cputype: U32Le, - cpusubtype: U32Le, - filetype: U32Le, - ncmds: U32Le, - sizeofcmds: U32Le, - flags: U32Le, - reserved: U32Le, -} - -#[repr(C)] -#[derive(IntoBytes, Immutable)] -struct SegmentCommand64 { - cmd: U32Le, - cmdsize: U32Le, - segname: [u8; 16], - vmaddr: U64Le, - vmsize: U64Le, - fileoff: U64Le, - filesize: U64Le, - maxprot: U32Le, - initprot: U32Le, - nsects: U32Le, - flags: U32Le, -} - -#[repr(C)] -#[derive(IntoBytes, Immutable)] -struct Section64 { - sectname: [u8; 16], - segname: [u8; 16], - addr: U64Le, - size: U64Le, - offset: U32Le, - align: U32Le, - reloff: U32Le, - nreloc: U32Le, - flags: U32Le, - reserved1: U32Le, - reserved2: U32Le, - reserved3: U32Le, -} - -#[repr(C)] -#[derive(IntoBytes, Immutable)] -struct Nlist64 { - n_strx: U32Le, - n_type: u8, - n_sect: u8, - n_desc: U16Le, - n_value: U64Le, -} - -#[repr(C)] -#[derive(IntoBytes, Immutable)] -struct LoadDylinkerCmd { - cmd: U32Le, - cmdsize: U32Le, - name_offset: U32Le, -} - -#[repr(C)] -#[derive(IntoBytes, Immutable)] -struct EntryPointCmd { - cmd: U32Le, - cmdsize: U32Le, - entryoff: U64Le, - stacksize: U64Le, -} - -#[repr(C)] -#[derive(IntoBytes, Immutable)] -struct DylibCmd { - cmd: U32Le, - cmdsize: U32Le, - name_offset: U32Le, - timestamp: U32Le, - current_version: U32Le, - compat_version: U32Le, -} - -#[repr(C)] -#[derive(IntoBytes, Immutable)] -struct DyldInfoCmd { - cmd: U32Le, - cmdsize: U32Le, - rebase_off: U32Le, - rebase_size: U32Le, - bind_off: U32Le, - bind_size: U32Le, - weak_bind_off: U32Le, - weak_bind_size: U32Le, - lazy_bind_off: U32Le, - lazy_bind_size: U32Le, - export_off: U32Le, - export_size: U32Le, -} - -#[repr(C)] -#[derive(IntoBytes, Immutable)] -struct SymtabCmd { - cmd: U32Le, - cmdsize: U32Le, - symoff: U32Le, - nsyms: U32Le, - stroff: U32Le, - strsize: U32Le, -} - -#[repr(C)] -#[derive(IntoBytes, Immutable, FromZeros)] -struct DysymtabCmd { - cmd: U32Le, - cmdsize: U32Le, - ilocalsym: U32Le, - nlocalsym: U32Le, - iextdefsym: U32Le, - nextdefsym: U32Le, - iundefsym: U32Le, - nundefsym: U32Le, - tocoff: U32Le, - ntoc: U32Le, - modtaboff: U32Le, - nmodtab: U32Le, - extrefsymoff: U32Le, - nextrefsyms: U32Le, - indirectsymoff: U32Le, - nindirectsyms: U32Le, - extreloff: U32Le, - nextrel: U32Le, - locreloff: U32Le, - nlocrel: U32Le, -} - -#[repr(C)] -#[derive(IntoBytes, Immutable)] -struct BuildVersionCmd { - cmd: U32Le, - cmdsize: U32Le, - platform: U32Le, - minos: U32Le, - sdk: U32Le, - ntools: U32Le, -} - -#[repr(C)] -#[derive(IntoBytes, Immutable)] -struct BuildToolVersion { - tool: U32Le, - version: U32Le, -} - -#[repr(C)] -#[derive(IntoBytes, Immutable)] -struct LinkeditDataCmd { - cmd: U32Le, - cmdsize: U32Le, - dataoff: U32Le, - datasize: U32Le, -} - -// Code signature (big-endian, matching lld's minimal approach) - -#[repr(C)] -#[derive(IntoBytes, Immutable)] -struct CsCodeDirectory { - magic: U32Be, - length: U32Be, - version: U32Be, - flags: U32Be, - hash_offset: U32Be, - ident_offset: U32Be, - n_special_slots: U32Be, - n_code_slots: U32Be, - code_limit: U32Be, - hash_size: u8, - hash_type: u8, - platform: u8, - page_size: u8, - spare2: U32Be, - scatter_offset: U32Be, - team_offset: U32Be, - spare3: U32Be, - code_limit_64: U64Be, - exec_seg_base: U64Be, - exec_seg_limit: U64Be, - exec_seg_flags: U64Be, -} - -// ── Helpers ───────────────────────────────────────────────────────────── - -fn pad16(name: &[u8]) -> [u8; 16] { - let mut out = [0u8; 16]; - let len = name.len().min(16); - out[..len].copy_from_slice(&name[..len]); - out -} - -fn write_struct(buf: &mut [u8], off: usize, val: &T) -> usize { - let bytes = val.as_bytes(); - buf[off..off + bytes.len()].copy_from_slice(bytes); - off + bytes.len() -} - -fn write_at(buf: &mut [u8], off: usize, data: &[u8]) { - buf[off..off + data.len()].copy_from_slice(data); -} - -fn encode_uleb128(buf: &mut Vec, mut value: u64) { - loop { - let byte = (value & 0x7F) as u8; - value >>= 7; - if value == 0 { - buf.push(byte); - break; - } - buf.push(byte | 0x80); - } -} - -fn uleb_size(mut value: u64) -> usize { - let mut size = 0; - loop { - value >>= 7; - size += 1; - if value == 0 { break; } - } - size -} - -// ── Layout ────────────────────────────────────────────────────────────── - -/// A Mach-O output section within a segment. -#[derive(Clone)] -pub(crate) struct MachOSection { - pub(crate) sectname: [u8; 16], - pub(crate) segname: [u8; 16], - pub(crate) vmaddr: u64, - pub(crate) size: u64, - /// File offset (0 for zerofill/nobits sections). - pub(crate) offset: u32, - pub(crate) align: u32, - pub(crate) flags: u32, - /// Index into the indirect symbol table (for S_NON_LAZY_SYMBOL_POINTERS etc.). - pub(crate) reserved1: u32, -} - -pub(crate) struct MachOLayout { - pub(crate) text_vmaddr: u64, - pub(crate) text_vmsize: u64, - pub(crate) text_filesize: u64, - /// Sections in __TEXT, sorted by vmaddr. - pub(crate) text_sections: Vec, - - pub(crate) data_vmaddr: u64, - pub(crate) data_vmsize: u64, - pub(crate) data_fileoff: u64, - pub(crate) data_filesize: u64, - /// Sections in __DATA, sorted by vmaddr. - pub(crate) data_sections: Vec, - - pub(crate) linkedit_vmaddr: u64, - pub(crate) linkedit_fileoff: u64, - - pub(crate) got: BTreeMap, - pub(crate) got_entries: Vec<(SymbolRef, bool)>, - - pub(crate) sizeofcmds: u32, - pub(crate) ncmds: u32, - pub(crate) arch: Arch, - pub(crate) page_size: u64, - - /// Start of the TLS template (__thread_data vmaddr) for TLV offset computation. - pub(crate) tls_template_start: u64, - /// Whether the binary has TLV descriptors (sets MH_HAS_TLV_DESCRIPTORS). - pub(crate) has_tlv: bool, -} - -impl MachOLayout { - /// Find the 1-based section number for a symbol at `vaddr`. - /// Searches both __TEXT and __DATA sections. - pub(crate) fn section_for_addr(&self, vaddr: u64) -> Option { - let mut sect_num = 1u8; - for sec in &self.text_sections { - if sec.size > 0 && vaddr >= sec.vmaddr && vaddr < sec.vmaddr + sec.size { - return Some(sect_num); - } - sect_num += 1; - } - for sec in &self.data_sections { - if sec.size > 0 && vaddr >= sec.vmaddr && vaddr < sec.vmaddr + sec.size { - return Some(sect_num); - } - sect_num += 1; - } - None - } -} - -/// Compute the Mach-O layout: segment placement, GOT allocation. -pub(crate) fn layout_macho(state: &mut LinkState) -> MachOLayout { - let arch = state.arch; - let page_size = match arch { Arch::X86_64 => PAGE_SIZE_X86_64, Arch::Aarch64 => PAGE_SIZE_ARM64 }; - let buckets = classify_sections(state); - - let got_symbols = collect_unique_symbols(state.relocs.iter(), |r| { - // Standard GOT relocation types - if matches!(r.r_type, - RelocType::Aarch64AdrGotPage | RelocType::Aarch64Ld64GotLo12Nc - | RelocType::Aarch64GotPcrel32 - | RelocType::X86Gotpcrel | RelocType::X86Gotpcrelx - | RelocType::X86RexGotpcrelx) - { - return true; - } - // MOVW relocations targeting dynamic symbols need GOT slots so we can - // rewrite MOVZ/MOVK → ADRP+LDR at relocation time. - if matches!(r.r_type, - RelocType::Aarch64MovwUabsG0Nc | RelocType::Aarch64MovwUabsG1Nc - | RelocType::Aarch64MovwUabsG2Nc | RelocType::Aarch64MovwUabsG3) - { - let is_dynamic = match &r.target { - SymbolRef::Global(name) => matches!( - state.globals.get(name), - Some(SymbolDef::Dynamic { .. }) | None - ), - _ => false, - }; - return is_dynamic; - } - false - }); - - // sizeofcmds and the section vector must be built from the same answer, - // so both read these flags rather than recomputing emptiness. - let has_const = buckets.rx.iter().any(|&i| state.sections[i].kind == SectionKind::ReadOnly && state.sections[i].size > 0); - let has_data = buckets.rw.iter().any(|&i| !state.sections[i].kind.is_nobits() && state.sections[i].size > 0); - let has_bss = buckets.rw.iter().any(|&i| state.sections[i].kind.is_nobits() && state.sections[i].size > 0); - let has_got = !got_symbols.is_empty(); - let has_thread_vars = buckets.tls.iter().any(|&i| state.sections[i].kind == SectionKind::TlsVariables && state.sections[i].size > 0); - let has_thread_data = buckets.tls.iter().any(|&i| state.sections[i].kind == SectionKind::Tls && state.sections[i].size > 0); - let has_thread_bss = buckets.tls.iter().any(|&i| state.sections[i].kind == SectionKind::TlsBss && state.sections[i].size > 0); - - let has_data_segment = has_data || has_got || has_bss - || has_thread_vars || has_thread_data || has_thread_bss; - let text_nsects = 1 + has_const as u32; - let data_nsects = has_data as u32 + has_got as u32 + has_bss as u32 - + has_thread_vars as u32 + has_thread_data as u32 + has_thread_bss as u32; - - let dylib_name = "/usr/lib/libSystem.B.dylib\0"; - let dylib_cmd_size = align_up((size_of::() + dylib_name.len()) as u64, 8) as u32; - let dylinker_name = "/usr/lib/dyld\0"; - let dylinker_cmd_size = align_up((size_of::() + dylinker_name.len()) as u64, 8) as u32; - let build_version_size = size_of::() as u32 + size_of::() as u32; - - let sizeofcmds: u32 = - SEGMENT_CMD_SIZE // __PAGEZERO - + SEGMENT_CMD_SIZE + SECTION_HEADER_SIZE * text_nsects // __TEXT - + if has_data_segment { SEGMENT_CMD_SIZE + SECTION_HEADER_SIZE * data_nsects } else { 0 } - + SEGMENT_CMD_SIZE // __LINKEDIT - + dylinker_cmd_size - + size_of::() as u32 - + dylib_cmd_size - + size_of::() as u32 - + size_of::() as u32 - + size_of::() as u32 - + build_version_size - + size_of::() as u32; - let ncmds: u32 = 11 + has_data_segment as u32; - let header_size = MACHO_HEADER_SIZE as u64 + sizeofcmds as u64; - - // Phase 2: Layout sections with actual addresses - let text_vmaddr = PAGEZERO_SIZE; - - let mut cursor = text_vmaddr + align_up(header_size, 16); - let text_sec_vmaddr = cursor; - let text_sec_offset = cursor - text_vmaddr; - for &idx in &buckets.rx { - let sec = &mut state.sections[idx]; - if sec.kind == SectionKind::ReadOnly { - continue; - } - cursor = align_up(cursor, sec.align); - sec.vaddr = Some(cursor); - cursor += sec.size; - } - let text_sec_size = cursor - text_sec_vmaddr; - - let const_sec_vmaddr = align_up(cursor, 8); - let const_sec_offset = const_sec_vmaddr - text_vmaddr; - cursor = const_sec_vmaddr; - for &idx in &buckets.rx { - let sec = &mut state.sections[idx]; - if sec.kind == SectionKind::ReadOnly { - cursor = align_up(cursor, sec.align); - sec.vaddr = Some(cursor); - cursor += sec.size; - } - } - let const_sec_size = cursor - const_sec_vmaddr; - - let text_vmsize = align_up(cursor - text_vmaddr, page_size); - let text_filesize = text_vmsize; - - let data_vmaddr = text_vmaddr + text_vmsize; - let data_fileoff = text_filesize; - let mut data_cursor = data_vmaddr; - - let data_sec_vmaddr = data_cursor; - let data_sec_offset = data_fileoff; - for &idx in &buckets.rw { - let sec = &mut state.sections[idx]; - if sec.kind.is_nobits() { continue; } - data_cursor = align_up(data_cursor, sec.align); - sec.vaddr = Some(data_cursor); - data_cursor += sec.size; - } - let data_sec_size = data_cursor - data_sec_vmaddr; - - data_cursor = align_up(data_cursor, 8); - let got_sec_vmaddr = data_cursor; - let got_sec_offset = data_fileoff + (got_sec_vmaddr - data_vmaddr); - - let mut got_entries = Vec::new(); - let mut got = BTreeMap::new(); - for sym in &got_symbols { - let is_external = match sym { - SymbolRef::Global(name) => match state.globals.get(name) { - Some(SymbolDef::Dynamic { .. }) => true, - Some(SymbolDef::Defined { .. }) => false, - None => true, - }, - SymbolRef::Local(_, _) => false, - }; - got.insert(sym.clone(), data_cursor); - got_entries.push((sym.clone(), is_external)); - data_cursor += 8; - } - let got_sec_size = data_cursor - got_sec_vmaddr; - - // __thread_vars: TLV descriptors (file-backed) - let thread_vars_sec_vmaddr = align_up(data_cursor, 8); - let thread_vars_sec_offset = data_fileoff + (thread_vars_sec_vmaddr - data_vmaddr); - let mut tv_cursor = thread_vars_sec_vmaddr; - for &idx in &buckets.tls { - let sec = &mut state.sections[idx]; - if sec.kind != SectionKind::TlsVariables { continue; } - tv_cursor = align_up(tv_cursor, sec.align); - sec.vaddr = Some(tv_cursor); - tv_cursor += sec.size; - } - let thread_vars_sec_size = tv_cursor - thread_vars_sec_vmaddr; - data_cursor = tv_cursor; - - // __thread_data: initialized TLS data (file-backed) - let thread_data_sec_vmaddr = align_up(data_cursor, 8); - let thread_data_sec_offset = data_fileoff + (thread_data_sec_vmaddr - data_vmaddr); - let mut td_cursor = thread_data_sec_vmaddr; - for &idx in &buckets.tls { - let sec = &mut state.sections[idx]; - if sec.kind != SectionKind::Tls { continue; } - td_cursor = align_up(td_cursor, sec.align); - sec.vaddr = Some(td_cursor); - td_cursor += sec.size; - } - let thread_data_sec_size = td_cursor - thread_data_sec_vmaddr; - // Don't advance data_cursor yet — thread_bss must follow thread_data - // to keep the TLS template contiguous (dyld computes template as - // [thread_data_start .. thread_bss_end]). - - // __thread_bss: zero-init TLS (nobits) — must be adjacent to __thread_data - let thread_bss_sec_vmaddr = align_up(td_cursor, 8); - let mut tb_cursor = thread_bss_sec_vmaddr; - for &idx in &buckets.tls { - let sec = &mut state.sections[idx]; - if sec.kind != SectionKind::TlsBss { continue; } - tb_cursor = align_up(tb_cursor, sec.align); - sec.vaddr = Some(tb_cursor); - tb_cursor += sec.size; - } - let thread_bss_sec_size = tb_cursor - thread_bss_sec_vmaddr; - - // BSS (regular) — after all TLS sections - let bss_sec_vmaddr = align_up(tb_cursor, 8); - let bss_start = bss_sec_vmaddr; - let mut bss_cursor = bss_sec_vmaddr; - for &idx in &buckets.rw { - let sec = &mut state.sections[idx]; - if !sec.kind.is_nobits() { continue; } - bss_cursor = align_up(bss_cursor, sec.align); - sec.vaddr = Some(bss_cursor); - bss_cursor += sec.size; - } - let bss_sec_size = bss_cursor - bss_start; - - // Build sorted section lists for __TEXT and __DATA segments - let mut text_sections = vec![MachOSection { - sectname: pad16(b"__text"), segname: pad16(b"__TEXT"), - vmaddr: text_sec_vmaddr, size: text_sec_size, - offset: text_sec_offset as u32, - align: match arch { Arch::X86_64 => 4, Arch::Aarch64 => 2 }, - flags: S_REGULAR | S_ATTR_PURE_INSTRUCTIONS | S_ATTR_SOME_INSTRUCTIONS, reserved1: 0, - }]; - if has_const { - text_sections.push(MachOSection { - sectname: pad16(b"__const"), segname: pad16(b"__TEXT"), - vmaddr: const_sec_vmaddr, size: const_sec_size, - offset: const_sec_offset as u32, align: 0, flags: S_REGULAR, reserved1: 0, - }); - } - text_sections.sort_by_key(|s| s.vmaddr); - - let mut data_sections = Vec::new(); - let data_seg = pad16(b"__DATA"); - if has_data { - data_sections.push(MachOSection { - sectname: pad16(b"__data"), segname: data_seg, - vmaddr: data_sec_vmaddr, size: data_sec_size, - offset: data_sec_offset as u32, align: 3, flags: S_REGULAR, reserved1: 0, - }); - } - if has_got { - data_sections.push(MachOSection { - sectname: pad16(b"__got"), segname: data_seg, - vmaddr: got_sec_vmaddr, size: got_sec_size, - offset: got_sec_offset as u32, align: 3, flags: S_NON_LAZY_SYMBOL_POINTERS, reserved1: 0, - }); - } - if has_thread_vars { - data_sections.push(MachOSection { - sectname: pad16(b"__thread_vars"), segname: data_seg, - vmaddr: thread_vars_sec_vmaddr, size: thread_vars_sec_size, - offset: thread_vars_sec_offset as u32, align: 3, flags: S_THREAD_LOCAL_VARIABLES, reserved1: 0, - }); - } - if has_thread_data { - data_sections.push(MachOSection { - sectname: pad16(b"__thread_data"), segname: data_seg, - vmaddr: thread_data_sec_vmaddr, size: thread_data_sec_size, - offset: thread_data_sec_offset as u32, align: 3, flags: S_THREAD_LOCAL_REGULAR, reserved1: 0, - }); - } - if has_bss { - data_sections.push(MachOSection { - sectname: pad16(b"__bss"), segname: data_seg, - vmaddr: bss_sec_vmaddr, size: bss_sec_size, - offset: 0, align: 3, flags: S_ZEROFILL, reserved1: 0, - }); - } - if has_thread_bss { - data_sections.push(MachOSection { - sectname: pad16(b"__thread_bss"), segname: data_seg, - vmaddr: thread_bss_sec_vmaddr, size: thread_bss_sec_size, - offset: 0, align: 3, flags: S_THREAD_LOCAL_ZEROFILL, reserved1: 0, - }); - } - data_sections.sort_by_key(|s| s.vmaddr); - - // File-backed size: everything up to the end of the last non-nobits section, - // rounded up to page size. macOS dyld hangs on TLS segments with non-page-aligned filesize. - let data_filesize = data_sections.iter() - .filter(|s| s.offset > 0) - .map(|s| (s.vmaddr + s.size) - data_vmaddr) - .max() - .map(|sz| align_up(sz, page_size)) - .unwrap_or(0); - let data_memsz = data_sections.iter() - .map(|s| (s.vmaddr + s.size) - data_vmaddr) - .max() - .unwrap_or(0); - let data_vmsize = align_up(data_memsz, page_size); - - let linkedit_vmaddr = data_vmaddr + data_vmsize; - let linkedit_fileoff = data_fileoff + align_up(data_filesize, page_size); - - // Defense-in-depth: verify predicted section counts match actual vectors. - assert_eq!(text_sections.len() as u32, text_nsects, - "text section count: predicted {text_nsects}, actual {}", text_sections.len()); - assert_eq!(data_sections.len() as u32, data_nsects, - "data section count: predicted {data_nsects}, actual {}", data_sections.len()); - - MachOLayout { - text_vmaddr, - text_vmsize, - text_filesize, - text_sections, - data_vmaddr, - data_vmsize, - data_fileoff, - data_filesize, - data_sections, - linkedit_vmaddr, - linkedit_fileoff, - got, - got_entries, - sizeofcmds, - ncmds, - arch, - page_size, - tls_template_start: thread_data_sec_vmaddr, - has_tlv: has_thread_vars, - } -} - -// ── Emission ──────────────────────────────────────────────────────────── - -pub(crate) fn emit_macho_bytes( - state: &LinkState, - layout: &MachOLayout, - entry_name: &str, - rebase_entries: &[u64], - bind_entries: &[(String, u64)], -) -> Result, LinkError> { - let entry_addr = state - .globals - .get(entry_name) - .map(|def| match def { - SymbolDef::Defined { section, value, .. } => { - let sec = &state.sections[*section]; - sec.vaddr.unwrap_or_else(|| panic!( - "entry point in section {:?} ({:?}) has no vaddr", - sec.name, sec.kind, - )) + value - } - SymbolDef::Dynamic { .. } => panic!("entry point cannot be a dynamic symbol"), - }) - .ok_or_else(|| LinkError::MissingEntry(entry_name.to_string()))?; - - let entryoff = entry_addr - layout.text_vmaddr; - - // Build __LINKEDIT content first to know sizes - let rebase_data = build_rebase_opcodes(layout, rebase_entries); - let bind_data = build_bind_opcodes(layout, bind_entries); - let export_data = build_export_trie(entry_name, entryoff); - - let (symtab_data, strtab_data, nlocalsym, nextdefsym, nundefsym, undef_syms) = - build_symbol_table(state, layout, bind_entries); - - // Build indirect symbol table: one u32 entry per GOT slot - const INDIRECT_SYMBOL_LOCAL: u32 = 0x8000_0000; - let undef_base = nlocalsym + nextdefsym; - let mut indirect_symtab = Vec::new(); - for (sym_ref, is_external) in &layout.got_entries { - if *is_external { - let name = sym_ref.name(); - let idx = undef_syms.iter().position(|s| s == name) - .unwrap_or_else(|| panic!("GOT symbol {name:?} not in undef_syms")); - indirect_symtab.extend_from_slice(&(undef_base + idx as u32).to_le_bytes()); - } else { - indirect_symtab.extend_from_slice(&INDIRECT_SYMBOL_LOCAL.to_le_bytes()); - } - } - - // __LINKEDIT layout - let mut linkedit_cursor = layout.linkedit_fileoff; - - let rebase_size = rebase_data.len() as u32; - let rebase_off = if rebase_size > 0 { linkedit_cursor } else { 0 }; - linkedit_cursor += align_up(rebase_size as u64, 8); - - let bind_size = bind_data.len() as u32; - let bind_off = if bind_size > 0 { linkedit_cursor } else { 0 }; - linkedit_cursor += align_up(bind_size as u64, 8); - - let export_size = export_data.len() as u32; - let export_off = if export_size > 0 { linkedit_cursor } else { 0 }; - linkedit_cursor += align_up(export_size as u64, 8); - - let symtab_off = linkedit_cursor; - let nsyms = (symtab_data.len() / size_of::()) as u32; - linkedit_cursor += symtab_data.len() as u64; - linkedit_cursor = align_up(linkedit_cursor, 8); - - let strtab_off = linkedit_cursor; - let strtab_size = strtab_data.len() as u32; - linkedit_cursor += align_up(strtab_size as u64, 8); - - let nindirectsyms = (indirect_symtab.len() / 4) as u32; - let indirect_symtab_off = if nindirectsyms > 0 { linkedit_cursor } else { 0 }; - linkedit_cursor += align_up(indirect_symtab.len() as u64, 8); - - // Code signature (16-byte aligned, must be last in __LINKEDIT) - linkedit_cursor = align_up(linkedit_cursor, 16); - let codesig_off = linkedit_cursor; - let code_limit = codesig_off as u32; - let cs_block_size: u32 = 4096; - let n_code_slots = code_limit.div_ceil(cs_block_size); - let ident = b"_main\0"; - let all_headers = (size_of::() + ident.len() + 15) & !15; - let hash_offset = all_headers as u32; - let cd_size = hash_offset + n_code_slots * 32; - // SuperBlob(12) + BlobIndex(8) + CodeDirectory - let codesig_size = 12 + 8 + cd_size; - linkedit_cursor += codesig_size as u64; - - let linkedit_filesize = linkedit_cursor - layout.linkedit_fileoff; - let linkedit_vmsize = align_up(linkedit_filesize, layout.page_size); - - // Build the output buffer - let total_size = linkedit_cursor as usize; - let mut buf = vec![0u8; total_size]; - - // ── Mach-O header ── - let mut off = write_struct(&mut buf, 0, &MachHeader64 { - magic: U32Le::new(MH_MAGIC_64), - cputype: U32Le::new(match layout.arch { Arch::X86_64 => CPU_TYPE_X86_64, Arch::Aarch64 => CPU_TYPE_ARM64 }), - cpusubtype: U32Le::new(match layout.arch { Arch::X86_64 => CPU_SUBTYPE_X86_64_ALL, Arch::Aarch64 => CPU_SUBTYPE_ARM64_ALL }), - filetype: U32Le::new(MH_EXECUTE), - ncmds: U32Le::new(layout.ncmds), - sizeofcmds: U32Le::new(layout.sizeofcmds), - flags: U32Le::new(MH_PIE | MH_TWOLEVEL | MH_DYLDLINK - | if layout.has_tlv { MH_HAS_TLV_DESCRIPTORS } else { 0 }), - reserved: U32Le::new(0), - }); - - // ── Load commands ── - - // LC_SEGMENT_64 __PAGEZERO - off = write_struct(&mut buf, off, &SegmentCommand64 { - cmd: U32Le::new(LC_SEGMENT_64), - cmdsize: U32Le::new(SEGMENT_CMD_SIZE), - segname: pad16(b"__PAGEZERO"), - vmaddr: U64Le::new(0), - vmsize: U64Le::new(PAGEZERO_SIZE), - fileoff: U64Le::new(0), - filesize: U64Le::new(0), - maxprot: U32Le::new(0), - initprot: U32Le::new(0), - nsects: U32Le::new(0), - flags: U32Le::new(0), - }); - - // LC_SEGMENT_64 __TEXT - off = write_struct(&mut buf, off, &SegmentCommand64 { - cmd: U32Le::new(LC_SEGMENT_64), - cmdsize: U32Le::new(SEGMENT_CMD_SIZE + SECTION_HEADER_SIZE * layout.text_sections.len() as u32), - segname: pad16(b"__TEXT"), - vmaddr: U64Le::new(layout.text_vmaddr), - vmsize: U64Le::new(layout.text_vmsize), - fileoff: U64Le::new(0), - filesize: U64Le::new(layout.text_filesize), - maxprot: U32Le::new(VM_PROT_READ_EXECUTE), - initprot: U32Le::new(VM_PROT_READ_EXECUTE), - nsects: U32Le::new(layout.text_sections.len() as u32), - flags: U32Le::new(0), - }); - for sec in &layout.text_sections { - off = write_struct(&mut buf, off, &Section64 { - sectname: sec.sectname, segname: sec.segname, - addr: U64Le::new(sec.vmaddr), size: U64Le::new(sec.size), - offset: U32Le::new(sec.offset), align: U32Le::new(sec.align), - reloff: U32Le::new(0), nreloc: U32Le::new(0), - flags: U32Le::new(sec.flags), - reserved1: U32Le::new(sec.reserved1), reserved2: U32Le::new(0), reserved3: U32Le::new(0), - }); - } - - // LC_SEGMENT_64 __DATA - if !layout.data_sections.is_empty() { - off = write_struct(&mut buf, off, &SegmentCommand64 { - cmd: U32Le::new(LC_SEGMENT_64), - cmdsize: U32Le::new(SEGMENT_CMD_SIZE + SECTION_HEADER_SIZE * layout.data_sections.len() as u32), - segname: pad16(b"__DATA"), - vmaddr: U64Le::new(layout.data_vmaddr), - vmsize: U64Le::new(layout.data_vmsize), - fileoff: U64Le::new(layout.data_fileoff), - filesize: U64Le::new(layout.data_filesize), - maxprot: U32Le::new(VM_PROT_READ_WRITE), - initprot: U32Le::new(VM_PROT_READ_WRITE), - nsects: U32Le::new(layout.data_sections.len() as u32), - flags: U32Le::new(0), - }); - for sec in &layout.data_sections { - off = write_struct(&mut buf, off, &Section64 { - sectname: sec.sectname, segname: sec.segname, - addr: U64Le::new(sec.vmaddr), size: U64Le::new(sec.size), - offset: U32Le::new(sec.offset), align: U32Le::new(sec.align), - reloff: U32Le::new(0), nreloc: U32Le::new(0), - flags: U32Le::new(sec.flags), - reserved1: U32Le::new(sec.reserved1), reserved2: U32Le::new(0), reserved3: U32Le::new(0), - }); - } - } - - // LC_SEGMENT_64 __LINKEDIT - off = write_struct(&mut buf, off, &SegmentCommand64 { - cmd: U32Le::new(LC_SEGMENT_64), - cmdsize: U32Le::new(SEGMENT_CMD_SIZE), - segname: pad16(b"__LINKEDIT"), - vmaddr: U64Le::new(layout.linkedit_vmaddr), - vmsize: U64Le::new(linkedit_vmsize), - fileoff: U64Le::new(layout.linkedit_fileoff), - filesize: U64Le::new(linkedit_filesize), - maxprot: U32Le::new(VM_PROT_READ), - initprot: U32Le::new(VM_PROT_READ), - nsects: U32Le::new(0), - flags: U32Le::new(0), - }); - - // LC_LOAD_DYLINKER - let dylinker_name_bytes = b"/usr/lib/dyld\0"; - let dylinker_cmd_size = align_up( - (size_of::() + dylinker_name_bytes.len()) as u64, 8, - ) as u32; - off = write_struct(&mut buf, off, &LoadDylinkerCmd { - cmd: U32Le::new(LC_LOAD_DYLINKER), - cmdsize: U32Le::new(dylinker_cmd_size), - name_offset: U32Le::new(size_of::() as u32), - }); - buf[off..off + dylinker_name_bytes.len()].copy_from_slice(dylinker_name_bytes); - off = (off - size_of::()) + dylinker_cmd_size as usize; - - // LC_MAIN - off = write_struct(&mut buf, off, &EntryPointCmd { - cmd: U32Le::new(LC_MAIN), - cmdsize: U32Le::new(size_of::() as u32), - entryoff: U64Le::new(entryoff), - stacksize: U64Le::new(0), - }); - - // LC_LOAD_DYLIB - let dylib_name_bytes = b"/usr/lib/libSystem.B.dylib\0"; - let dylib_cmd_size = align_up( - (size_of::() + dylib_name_bytes.len()) as u64, 8, - ) as u32; - off = write_struct(&mut buf, off, &DylibCmd { - cmd: U32Le::new(LC_LOAD_DYLIB), - cmdsize: U32Le::new(dylib_cmd_size), - name_offset: U32Le::new(size_of::() as u32), - timestamp: U32Le::new(0), - current_version: U32Le::new(0x010000), - compat_version: U32Le::new(0x010000), - }); - buf[off..off + dylib_name_bytes.len()].copy_from_slice(dylib_name_bytes); - off = (off - size_of::()) + dylib_cmd_size as usize; - - // LC_DYLD_INFO_ONLY - off = write_struct(&mut buf, off, &DyldInfoCmd { - cmd: U32Le::new(LC_DYLD_INFO_ONLY), - cmdsize: U32Le::new(size_of::() as u32), - rebase_off: U32Le::new(rebase_off as u32), - rebase_size: U32Le::new(rebase_size), - bind_off: U32Le::new(bind_off as u32), - bind_size: U32Le::new(bind_size), - weak_bind_off: U32Le::new(0), - weak_bind_size: U32Le::new(0), - lazy_bind_off: U32Le::new(0), - lazy_bind_size: U32Le::new(0), - export_off: U32Le::new(export_off as u32), - export_size: U32Le::new(export_size), - }); - - // LC_SYMTAB - off = write_struct(&mut buf, off, &SymtabCmd { - cmd: U32Le::new(LC_SYMTAB), - cmdsize: U32Le::new(size_of::() as u32), - symoff: U32Le::new(symtab_off as u32), - nsyms: U32Le::new(nsyms), - stroff: U32Le::new(strtab_off as u32), - strsize: U32Le::new(strtab_size), - }); - - // LC_DYSYMTAB - let mut dysymtab = DysymtabCmd::new_zeroed(); - dysymtab.cmd = U32Le::new(LC_DYSYMTAB); - dysymtab.cmdsize = U32Le::new(size_of::() as u32); - dysymtab.nlocalsym = U32Le::new(nlocalsym); - dysymtab.iextdefsym = U32Le::new(nlocalsym); - dysymtab.nextdefsym = U32Le::new(nextdefsym); - dysymtab.iundefsym = U32Le::new(nlocalsym + nextdefsym); - dysymtab.nundefsym = U32Le::new(nundefsym); - dysymtab.indirectsymoff = U32Le::new(indirect_symtab_off as u32); - dysymtab.nindirectsyms = U32Le::new(nindirectsyms); - off = write_struct(&mut buf, off, &dysymtab); - - // LC_BUILD_VERSION (platform = macOS, minos = 14.0) - off = write_struct(&mut buf, off, &BuildVersionCmd { - cmd: U32Le::new(LC_BUILD_VERSION), - cmdsize: U32Le::new( - size_of::() as u32 + size_of::() as u32, - ), - platform: U32Le::new(PLATFORM_MACOS), - minos: U32Le::new(MACOS_14_0), - sdk: U32Le::new(MACOS_14_0), - ntools: U32Le::new(1), - }); - off = write_struct(&mut buf, off, &BuildToolVersion { - tool: U32Le::new(TOOL_LD), - version: U32Le::new(0x010000), - }); - - // LC_CODE_SIGNATURE - off = write_struct(&mut buf, off, &LinkeditDataCmd { - cmd: U32Le::new(LC_CODE_SIGNATURE), - cmdsize: U32Le::new(size_of::() as u32), - dataoff: U32Le::new(code_limit), - datasize: U32Le::new(codesig_size), - }); - - let _ = off; - - // ── Write section data ── - // Every input section with a vaddr gets written at its file offset. - // __TEXT sections: file_offset = vaddr - text_vmaddr - // __DATA sections: file_offset = data_fileoff + (vaddr - data_vmaddr) - for sec in &state.sections { - let Some(vaddr) = sec.vaddr else { continue }; - if sec.data.is_empty() || sec.kind.is_nobits() { continue } - let file_off = if vaddr >= layout.data_vmaddr { - (layout.data_fileoff + (vaddr - layout.data_vmaddr)) as usize - } else { - (vaddr - layout.text_vmaddr) as usize - }; - buf[file_off..file_off + sec.data.len()].copy_from_slice(&sec.data); - } - - // __DATA,__got: fill internal GOT entries - for (sym_ref, &got_vaddr) in &layout.got { - let is_external = layout.got_entries.iter() - .any(|(n, ext)| n == sym_ref && *ext); - if is_external { continue; } // filled by dyld - let sym_addr = resolve_symbol(state, sym_ref, None) - .ok_or_else(|| LinkError::UndefinedSymbols(vec![sym_ref.name().to_string()]))?; - let off = (layout.data_fileoff + (got_vaddr - layout.data_vmaddr)) as usize; - buf[off..off + 8].copy_from_slice(&sym_addr.to_le_bytes()); - } - - // __LINKEDIT content - write_at(&mut buf, rebase_off as usize, &rebase_data); - write_at(&mut buf, bind_off as usize, &bind_data); - write_at(&mut buf, export_off as usize, &export_data); - write_at(&mut buf, symtab_off as usize, &symtab_data); - write_at(&mut buf, strtab_off as usize, &strtab_data); - write_at(&mut buf, indirect_symtab_off as usize, &indirect_symtab); - - // Build and write ad-hoc code signature (matching lld's minimal approach: - // single CodeDirectory blob, no special slots, CS_ADHOC | CS_LINKER_SIGNED) - build_code_signature(&mut buf, codesig_off as usize, code_limit, n_code_slots, - ident, hash_offset, cd_size, codesig_size, layout); - - Ok(buf) -} - -// ── Rebase opcodes ────────────────────────────────────────────────────── - -/// Mach-O C symbol names have a leading `_` prefix. ELF names don't. -/// Prepend `_` unless the name already starts with `_` (from Mach-O input). -fn macho_mangle(name: &str) -> String { - format!("_{name}") -} - -fn build_rebase_opcodes(layout: &MachOLayout, entries: &[u64]) -> Vec { - let mut ops = Vec::new(); - if entries.is_empty() { - return ops; - } - - ops.push(REBASE_OPCODE_SET_TYPE_IMM | REBASE_TYPE_POINTER); - - // Group rebase entries by segment - let mut data_entries: Vec = entries.iter() - .filter(|&&vaddr| vaddr >= layout.data_vmaddr && vaddr < layout.data_vmaddr + layout.data_vmsize) - .copied() - .collect(); - data_entries.sort(); - - // __DATA segment ordinal: __PAGEZERO=0, __TEXT=1, __DATA=2. - // All absolute reloc sections are moved to __DATA by mark_abs_reloc_sections_writable, - // so rebase entries are always in __DATA. - let seg_ordinal = 2u8; - for &vaddr in &data_entries { - let offset = vaddr - layout.data_vmaddr; - ops.push(REBASE_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB | seg_ordinal); - encode_uleb128(&mut ops, offset); - ops.push(REBASE_OPCODE_DO_REBASE_IMM_TIMES | 1); - } - - ops.push(REBASE_OPCODE_DONE); - ops -} - -// ── Bind opcodes ──────────────────────────────────────────────────────── - -fn build_bind_opcodes(layout: &MachOLayout, entries: &[(String, u64)]) -> Vec { - let mut ops = Vec::new(); - if entries.is_empty() { - ops.push(BIND_OPCODE_DONE); - return ops; - } - - for (sym_name, got_vaddr) in entries { - let macho_name = macho_mangle(sym_name); - // Set dylib ordinal (1 = first LC_LOAD_DYLIB = libSystem) - ops.push(BIND_OPCODE_SET_DYLIB_ORDINAL_IMM | 1); - // Set symbol name. The immediate is the symbol flags nibble and it is - // zero here, written as the OR every other opcode below is written as. - #[allow(clippy::identity_op)] - ops.push(BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM | 0); - ops.extend_from_slice(macho_name.as_bytes()); - ops.push(0); // null terminator - // Set type - ops.push(BIND_OPCODE_SET_TYPE_IMM | BIND_TYPE_POINTER); - // Set segment and offset (__DATA segment = ordinal 2) - let seg_ordinal = 2u8; - let offset = got_vaddr - layout.data_vmaddr; - ops.push(BIND_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB | seg_ordinal); - encode_uleb128(&mut ops, offset); - // Bind - ops.push(BIND_OPCODE_DO_BIND); - } - - ops.push(BIND_OPCODE_DONE); - ops -} - -// ── Export trie ───────────────────────────────────────────────────────── - -fn build_export_trie(entry_name: &str, entry_offset: u64) -> Vec { - // Minimal export trie with just the entry symbol. - // Trie format: each node has terminal_size + [info] + num_children + [edges] - // Root node with one child edge for the entry symbol. - let mut trie = Vec::new(); - - // Root node - trie.push(0); // terminal size = 0 (root has no export) - trie.push(1); // 1 child - - // Edge: entry symbol name → child node - let macho_entry = macho_mangle(entry_name); - trie.extend_from_slice(macho_entry.as_bytes()); - trie.push(0); // null terminator - - // Child node info (needed to know terminal_size) - let mut info = Vec::new(); - info.push(0); // flags: EXPORT_SYMBOL_FLAGS_KIND_REGULAR - encode_uleb128(&mut info, entry_offset); - let terminal_size = info.len(); - - // Child node offset: iteratively solve offset = pos + uleb_size(offset) - // since the ULEB encoding of the offset may itself change the offset. - let pos = trie.len(); - let mut child_node_offset = pos + 1; - loop { - let new = pos + uleb_size(child_node_offset as u64); - if new == child_node_offset { break; } - child_node_offset = new; - } - encode_uleb128(&mut trie, child_node_offset as u64); - - // Child node (terminal) - let mut child = Vec::new(); - encode_uleb128(&mut child, terminal_size as u64); // terminal size - child.extend_from_slice(&info); - child.push(0); // 0 children - - trie.extend_from_slice(&child); - - // Pad to 8-byte alignment - while trie.len() % 8 != 0 { trie.push(0); } - - trie -} - -// ── Symbol table ──────────────────────────────────────────────────────── - -fn build_symbol_table( - state: &LinkState, - layout: &MachOLayout, - bind_entries: &[(String, u64)], -) -> (Vec, Vec, u32, u32, u32, Vec) { - let mut symtab = Vec::new(); - let mut strtab = vec![0u8]; // index 0 = empty string - - fn add_string(strtab: &mut Vec, s: &str) -> u32 { - let offset = strtab.len() as u32; - strtab.extend_from_slice(s.as_bytes()); - strtab.push(0); - offset - } - - let nlocalsym = 0u32; - - // Defined external symbols - let mut extdef_syms: Vec<(String, u64, u8)> = Vec::new(); - for (name, def) in &state.globals { - let SymbolDef::Defined { section, value, .. } = def else { continue; }; - let sec = &state.sections[*section]; - let value = sec.vaddr.unwrap_or_else(|| panic!( - "symbol {name:?} in section {:?} ({:?}) has no vaddr", - sec.name, sec.kind, - )) + value; - let sect = layout.section_for_addr(value).unwrap_or_else(|| panic!( - "symbol {name:?} at {value:#x} does not fall in any Mach-O section", - )); - extdef_syms.push((name.clone(), value, sect)); - } - extdef_syms.sort_by(|a, b| a.0.cmp(&b.0)); - let nextdefsym = extdef_syms.len() as u32; - - for (name, value, sect) in &extdef_syms { - let macho_name = macho_mangle(name); - let nlist = Nlist64 { - n_strx: U32Le::new(add_string(&mut strtab, &macho_name)), - n_type: N_SECT | N_EXT, - n_sect: *sect, - n_desc: U16Le::new(0), - n_value: U64Le::new(*value), - }; - symtab.extend_from_slice(nlist.as_bytes()); - } - - // Undefined symbols (external dylib references) - let mut undef_syms: Vec = bind_entries.iter().map(|(n, _)| n.clone()).collect(); - undef_syms.sort(); - undef_syms.dedup(); - let nundefsym = undef_syms.len() as u32; - - for name in &undef_syms { - let macho_name = macho_mangle(name); - let nlist = Nlist64 { - n_strx: U32Le::new(add_string(&mut strtab, &macho_name)), - n_type: N_EXT, // undefined - n_sect: 0, // NO_SECT - // REFERENCE_FLAG_UNDEFINED_NON_LAZY (0) | SET_LIBRARY_ORDINAL(1) - n_desc: U16Le::new(0x0100), // library ordinal 1 in high byte - n_value: U64Le::new(0), - }; - symtab.extend_from_slice(nlist.as_bytes()); - } - - (symtab, strtab, nlocalsym, nextdefsym, nundefsym, undef_syms) -} - -// ── Ad-hoc code signature (matching lld's minimal approach) ────────── - -const CSMAGIC_EMBEDDED_SIGNATURE: u32 = 0xFADE_0CC0; -const CSMAGIC_CODEDIRECTORY: u32 = 0xFADE_0C02; -const CS_SUPPORTSEXECSEG: u32 = 0x0002_0400; -const CS_ADHOC: u32 = 0x0000_0002; -const CS_LINKER_SIGNED: u32 = 0x0002_0000; -const CS_HASHTYPE_SHA256: u8 = 2; -const CS_EXECSEG_MAIN_BINARY: u64 = 1; - -#[allow(clippy::too_many_arguments)] -fn build_code_signature( - buf: &mut [u8], - codesig_off: usize, - code_limit: u32, - n_code_slots: u32, - ident: &[u8], - hash_offset: u32, - cd_size: u32, - codesig_size: u32, - layout: &MachOLayout, -) { - let blob_headers_size: u32 = 12 + 8; // SuperBlob + 1 BlobIndex - - // SuperBlob header - let mut off = codesig_off; - write_be32(buf, off, CSMAGIC_EMBEDDED_SIGNATURE); - write_be32(buf, off + 4, codesig_size); - write_be32(buf, off + 8, 1); // count = 1 blob - - // BlobIndex[0]: CodeDirectory - write_be32(buf, off + 12, 0); // CSSLOT_CODEDIRECTORY - write_be32(buf, off + 16, blob_headers_size); - - // CodeDirectory - let cd_off = codesig_off + blob_headers_size as usize; - off = write_struct(buf, cd_off, &CsCodeDirectory { - magic: U32Be::new(CSMAGIC_CODEDIRECTORY), - length: U32Be::new(cd_size), - version: U32Be::new(CS_SUPPORTSEXECSEG), - flags: U32Be::new(CS_ADHOC | CS_LINKER_SIGNED), - hash_offset: U32Be::new(hash_offset), - ident_offset: U32Be::new(size_of::() as u32), - n_special_slots: U32Be::new(0), - n_code_slots: U32Be::new(n_code_slots), - code_limit: U32Be::new(code_limit), - hash_size: 32, - hash_type: CS_HASHTYPE_SHA256, - platform: 0, - page_size: 12, // log2(4096) - spare2: U32Be::new(0), - scatter_offset: U32Be::new(0), - team_offset: U32Be::new(0), - spare3: U32Be::new(0), - code_limit_64: U64Be::new(0), - exec_seg_base: U64Be::new(0), - exec_seg_limit: U64Be::new(layout.text_filesize), - exec_seg_flags: U64Be::new(CS_EXECSEG_MAIN_BINARY), - }); - - // Identifier string + padding - buf[off..off + ident.len()].copy_from_slice(ident); - // padding bytes are already zero - - // Code page hashes (SHA-256 of each 4K block) - let hash_start = cd_off + hash_offset as usize; - for i in 0..n_code_slots { - let page_start = (i * 4096) as usize; - let page_end = ((i + 1) * 4096).min(code_limit) as usize; - let hash = Sha256::digest(&buf[page_start..page_end]); - let off = hash_start + (i as usize) * 32; - buf[off..off + 32].copy_from_slice(&hash); - } -} - -fn write_be32(buf: &mut [u8], off: usize, val: u32) { - buf[off..off + 4].copy_from_slice(&val.to_be_bytes()); -} - - diff --git a/toyos-ld/src/emit_pe.rs b/toyos-ld/src/emit_pe.rs deleted file mode 100644 index 87e964bfcea..00000000000 --- a/toyos-ld/src/emit_pe.rs +++ /dev/null @@ -1,207 +0,0 @@ -use crate::collect::{collect_unique_symbols, LinkState, RelocType, SymbolDef, SymbolRef}; -use crate::reloc::resolve_symbol; -use crate::{align_up, classify_sections, LinkError}; -use object::write::pe::{NtHeaders, Writer}; -use object::pe; -use std::collections::BTreeMap; - -const PE_FILE_ALIGNMENT: u32 = 0x200; -const PE_SECTION_ALIGNMENT: u32 = 0x1000; - -// ── Layout ─────────────────────────────────────────────────────────────── - -pub(crate) struct PeLayout { - pub(crate) text_rva: u32, - pub(crate) text_virt_size: u32, - pub(crate) data_rva: u32, - pub(crate) data_virt_size: u32, - pub(crate) has_data: bool, - pub(crate) got: BTreeMap, -} - -/// PE section layout: RVAs use PE_SECTION_ALIGNMENT, file uses PE_FILE_ALIGNMENT. -/// All section vaddrs in LinkState are set relative to text_rva. -pub(crate) fn layout_pe(state: &mut LinkState) -> PeLayout { - let buckets = classify_sections(state); - - // .text section - let text_rva = PE_SECTION_ALIGNMENT; // first section always at 0x1000 - let mut cursor = text_rva as u64; - for &idx in &buckets.rx { - let sec = &mut state.sections[idx]; - cursor = align_up(cursor, sec.align); - sec.vaddr = Some(cursor); - cursor += sec.size; - } - let text_virt_size = (cursor - text_rva as u64) as u32; - - // .data section (if any RW sections exist) - let data_rva = pe_align_up(text_rva + text_virt_size, PE_SECTION_ALIGNMENT); - let mut data_virt_size = 0u32; - let has_data = !buckets.rw.is_empty(); - if has_data { - cursor = data_rva as u64; - for &idx in &buckets.rw { - let sec = &mut state.sections[idx]; - cursor = align_up(cursor, sec.align); - sec.vaddr = Some(cursor); - cursor += sec.size; - } - data_virt_size = (cursor - data_rva as u64) as u32; - } - - // GOT entries needed - let got_symbols = collect_unique_symbols(state.relocs.iter(), |r| { - matches!(r.r_type, - RelocType::X86Gotpcrel | RelocType::X86Gotpcrelx - | RelocType::X86RexGotpcrelx) - }); - if !got_symbols.is_empty() { - cursor = if has_data { align_up(cursor, 8) } else { data_rva as u64 }; - } - let mut got = BTreeMap::new(); - for sym in &got_symbols { - got.insert(sym.clone(), cursor); - cursor += 8; - } - if !got_symbols.is_empty() { - data_virt_size = (cursor - data_rva as u64) as u32; - } - - PeLayout { - text_rva, - text_virt_size, - data_rva, - data_virt_size, - has_data: has_data || !got_symbols.is_empty(), - got, - } -} - -fn pe_align_up(value: u32, alignment: u32) -> u32 { - (value + alignment - 1) & !(alignment - 1) -} - -// ── PE output ──────────────────────────────────────────────────────────── - -pub(crate) fn emit_pe_bytes( - state: &LinkState, - layout: &PeLayout, - entry_name: &str, - subsystem: u16, - abs_fixups: &[u32], -) -> Result, LinkError> { - let entry_rva = state - .globals - .get(entry_name) - .map(|def| match def { - SymbolDef::Defined { section, value, .. } => { - (state.sections[*section].vaddr.unwrap() + value) as u32 - } - SymbolDef::Dynamic { .. } => panic!("entry point cannot be a dynamic symbol"), - }) - .ok_or_else(|| LinkError::MissingEntry(entry_name.to_string()))?; - - let num_sections: u16 = if layout.has_data { 3 } else { 2 }; // .text [.data] .reloc - - let mut buf = Vec::new(); - let mut w = Writer::new(true, PE_SECTION_ALIGNMENT, PE_FILE_ALIGNMENT, &mut buf); - - // ── Phase 1: Reserve ── - - w.reserve_dos_header(); - w.reserve_nt_headers(16); - w.reserve_section_headers(num_sections); - - // .text section - let text_range = w.reserve_text_section(layout.text_virt_size); - debug_assert_eq!(text_range.virtual_address, layout.text_rva); - - // .data section - let data_range = if layout.has_data { - let range = w.reserve_data_section(layout.data_virt_size, layout.data_virt_size); - debug_assert_eq!(range.virtual_address, layout.data_rva); - Some(range) - } else { - None - }; - - // Base relocations - for &rva in abs_fixups { - w.add_reloc(rva, pe::IMAGE_REL_BASED_DIR64); - } - w.reserve_reloc_section(); - - // Ensure size_of_image is section-aligned - let aligned_vlen = pe_align_up(w.virtual_len(), PE_SECTION_ALIGNMENT); - w.reserve_virtual_until(aligned_vlen); - - // ── Phase 2: Write ── - - w.write_empty_dos_header().unwrap(); - w.write_nt_headers(NtHeaders { - machine: pe::IMAGE_FILE_MACHINE_AMD64, - time_date_stamp: 0, - characteristics: pe::IMAGE_FILE_EXECUTABLE_IMAGE | pe::IMAGE_FILE_LARGE_ADDRESS_AWARE, - major_linker_version: 0, - minor_linker_version: 0, - address_of_entry_point: entry_rva, - image_base: 0, - major_operating_system_version: 0, - minor_operating_system_version: 0, - major_image_version: 0, - minor_image_version: 0, - major_subsystem_version: 0, - minor_subsystem_version: 0, - subsystem, - dll_characteristics: pe::IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE - | pe::IMAGE_DLLCHARACTERISTICS_HIGH_ENTROPY_VA - | pe::IMAGE_DLLCHARACTERISTICS_NX_COMPAT, - size_of_stack_reserve: 0x100000, - size_of_stack_commit: 0x1000, - size_of_heap_reserve: 0x100000, - size_of_heap_commit: 0x1000, - }); - w.write_section_headers(); - - // ── Write section data ── - - // .text: collect all RX section data - let mut text_data = vec![0u8; layout.text_virt_size as usize]; - for sec in &state.sections { - let Some(vaddr) = sec.vaddr else { continue; }; - if sec.data.is_empty() { continue; } - let rva = vaddr as u32; - if rva >= layout.text_rva && rva < layout.text_rva + layout.text_virt_size { - let off = (rva - layout.text_rva) as usize; - text_data[off..off + sec.data.len()].copy_from_slice(&sec.data); - } - } - w.write_section(text_range.file_offset, &text_data); - - // .data: collect all RW section data + GOT entries - if let Some(data_range) = data_range { - let mut data_data = vec![0u8; layout.data_virt_size as usize]; - for sec in &state.sections { - let Some(vaddr) = sec.vaddr else { continue; }; - if sec.data.is_empty() { continue; } - let rva = vaddr as u32; - if rva >= layout.data_rva && rva < layout.data_rva + layout.data_virt_size { - let off = (rva - layout.data_rva) as usize; - data_data[off..off + sec.data.len()].copy_from_slice(&sec.data); - } - } - for (sym_ref, &got_vaddr) in &layout.got { - let sym_addr = resolve_symbol(state, sym_ref, None) - .ok_or_else(|| LinkError::UndefinedSymbols(vec![sym_ref.name().to_string()]))?; - let off = (got_vaddr - layout.data_rva as u64) as usize; - data_data[off..off + 8].copy_from_slice(&sym_addr.to_le_bytes()); - } - w.write_section(data_range.file_offset, &data_data); - } - - // .reloc - w.write_reloc_section(); - - Ok(buf) -} diff --git a/toyos-ld/src/lib.rs b/toyos-ld/src/lib.rs deleted file mode 100644 index 2d073c08484..00000000000 --- a/toyos-ld/src/lib.rs +++ /dev/null @@ -1,707 +0,0 @@ -//! toyos-ld: Minimal linker for ToyOS. -//! -//! Reads ELF and COFF object files. Produces PIE ELF, static ELF, or PE32+. -//! Supports .o object files and .rlib/.a archives (ar format). - -mod collect; -mod reloc; -mod emit_elf; -mod emit_pe; -mod emit_macho; - -use std::collections::{BTreeMap, BTreeSet}; -use std::path::PathBuf; -use std::fs; - -pub use collect::RelocType; -use collect::{Arch, collect, synthesize_alloc_shims, gc_sections, merge_string_sections, is_archive, is_shared_lib, extract_archive, find_lib, scan_symbols, SectionIdx, SectionKind, SymbolDef, SymbolRef}; -use reloc::{ElfRelocParams, apply_relocs, apply_relocs_pe, MachORelocParams, apply_relocs_macho}; -use emit_elf::{layout_elf, build_eh_frame_hdr, ElfEmitMode, ElfLayout}; -use emit_pe::{layout_pe, emit_pe_bytes, PeLayout}; -use emit_macho::{layout_macho, emit_macho_bytes, MachOLayout}; - -pub(crate) const BASE_VADDR: u64 = 0; -pub(crate) const PAGE_SIZE: u64 = 0x1000; - -// ── Error type ────────────────────────────────────────────────────────── - -#[derive(Debug, thiserror::Error)] -pub enum LinkError { - #[error("undefined symbol: {}", .0.join(", "))] - UndefinedSymbols(Vec), - #[error("cannot parse {file}: {message}")] - Parse { file: String, message: String }, - #[error("unsupported relocation type {reloc_type} for symbol {symbol}")] - UnsupportedRelocation { reloc_type: RelocType, symbol: String }, - #[error("unsupported raw relocation type {raw_type} for symbol {symbol}")] - UnsupportedRawRelocation { raw_type: String, symbol: String }, - #[error("relocation overflow: type {reloc_type} for symbol {symbol} value {value:#x}")] - RelocationOverflow { reloc_type: RelocType, symbol: String, value: i64 }, - #[error("entry symbol '{0}' not found")] - MissingEntry(String), - /// Its local-exec offsets would be against the unrounded block size, and - /// the loader ends an executable's block at its size rounded to `p_align`. - #[error("an executable with thread-local storage: toyos-ld is frozen, and places it where the loader does not")] - TlsExecutable, -} - -// ── Pipeline typestate ────────────────────────────────────────────────── -// -// Collected → LaidOut → Vec, each step consuming the previous state. -// Format-specific prep (GC, Mach-O stubs) runs via `&mut self` on `Collected`, -// before layout. - -/// Linker state after object collection + preparation. Ready for layout. -pub(crate) struct Collected { - pub(crate) state: collect::LinkState, -} - -/// Linker state after layout. Ready for relocation and emission. -pub(crate) struct LaidOut { - pub(crate) state: collect::LinkState, - pub(crate) layout: L, -} - -impl Collected { - fn new(objects: &[(String, Vec)]) -> Result { - let mut state = collect(objects)?; - synthesize_alloc_shims(&mut state); - merge_string_sections(&mut state); - Ok(Collected { state }) - } - - fn gc_sections(&mut self, entry: &str) { - gc_sections(&mut self.state, entry); - } - - /// Mark undefined symbols as dynamic (dylib) imports for Mach-O linking. - /// Determines whether each symbol is a function or data by checking if it - /// has any call-type relocations (functions) vs only GOT/data relocations (data). - fn mark_dynamic_symbols(&mut self) { - let call_relocs: &[RelocType] = match self.state.arch { - Arch::Aarch64 => &[RelocType::Aarch64Call26, RelocType::Aarch64Jump26], - Arch::X86_64 => &[RelocType::X86Plt32, RelocType::X86Pc32], - }; - // Collect referenced symbols and whether they have call relocations - let mut referenced: BTreeMap = BTreeMap::new(); - for r in &self.state.relocs { - let name = r.target.name().to_string(); - let entry = referenced.entry(name).or_insert(false); - if call_relocs.contains(&r.r_type) { - *entry = true; - } - } - let local_names: std::collections::HashSet<&str> = self.state.locals.keys().map(|(_, n)| n.as_str()).collect(); - for (sym, has_call) in referenced { - if !self.state.globals.contains_key(&sym) - && !local_names.contains(sym.as_str()) - { - self.state.dynamic_imports.insert(sym.clone()); - self.state.globals.insert(sym, SymbolDef::Dynamic { is_func: has_call, is_tls: false }); - } - } - } - - /// Mark sections with absolute relocations as writable (needed for Mach-O rebasing). - fn mark_abs_reloc_sections_writable(&mut self) { - let abs_reloc_sections: BTreeSet = self.state.relocs.iter() - .filter(|r| matches!(r.r_type, - RelocType::Aarch64Abs64 | RelocType::Aarch64Abs32 - | RelocType::X86_64 | RelocType::X86_32)) - .map(|r| r.section) - .collect(); - for &idx in &abs_reloc_sections { - let kind = self.state.sections[idx].kind; - if !kind.is_writable() && !kind.is_tls() { - self.state.sections[idx].kind = SectionKind::Data; - } - } - } - - fn layout_elf(mut self, base_addr: u64, entry: Option<&str>, build_id: bool) -> LaidOut { - let layout = layout_elf(&mut self.state, base_addr, entry, build_id); - LaidOut { state: self.state, layout } - } - - fn layout_pe(mut self) -> LaidOut { - let layout = layout_pe(&mut self.state); - LaidOut { state: self.state, layout } - } - - fn layout_macho(mut self) -> LaidOut { - let layout = layout_macho(&mut self.state); - LaidOut { state: self.state, layout } - } -} - -impl LaidOut { - fn refuse_tls_executable(&self) -> Result<(), LinkError> { - if self.layout.tls_memsz > 0 { - return Err(LinkError::TlsExecutable); - } - Ok(()) - } - - fn relocate_and_emit_pie(mut self, entry: &str) -> Result, LinkError> { - self.refuse_tls_executable()?; - let params = ElfRelocParams { - got: &self.layout.got, - tls_start: self.layout.tls_start, - tls_memsz: self.layout.tls_memsz, - plt: Some(&self.layout.plt), - dyn_got: &self.layout.dyn_got, - record_relatives: true, - allow_undefined: false, - is_shared: false, - ld_got_pair: self.layout.ld_got_pair, - gd_got: &self.layout.gd_got, - }; - let relocs = apply_relocs(&mut self.state, ¶ms)?; - let eh_hdr = build_eh_frame_hdr(&self.state, &self.layout); - emit_elf::emit_elf(&self.state, &self.layout, ElfEmitMode::Pie { - entry_name: entry, - relocs: &relocs, - eh_frame_hdr: &eh_hdr, - }) - } - - fn relocate_and_emit_static(mut self, entry: &str) -> Result, LinkError> { - self.refuse_tls_executable()?; - let empty_dyn_got = BTreeMap::new(); - let empty_gd_got = BTreeMap::new(); - let params = ElfRelocParams { - got: &self.layout.got, - tls_start: self.layout.tls_start, - tls_memsz: self.layout.tls_memsz, - plt: None, - dyn_got: &empty_dyn_got, - record_relatives: false, - allow_undefined: false, - is_shared: false, - ld_got_pair: None, - gd_got: &empty_gd_got, - }; - apply_relocs(&mut self.state, ¶ms)?; - emit_elf::emit_elf(&self.state, &self.layout, ElfEmitMode::Static { entry_name: entry }) - } - - fn relocate_and_emit_shared(mut self) -> Result, LinkError> { - let params = ElfRelocParams { - got: &self.layout.got, - tls_start: self.layout.tls_start, - tls_memsz: self.layout.tls_memsz, - plt: Some(&self.layout.plt), - dyn_got: &self.layout.dyn_got, - record_relatives: true, - allow_undefined: true, - is_shared: true, - ld_got_pair: self.layout.ld_got_pair, - gd_got: &self.layout.gd_got, - }; - let relocs = apply_relocs(&mut self.state, ¶ms)?; - let eh_hdr = build_eh_frame_hdr(&self.state, &self.layout); - emit_elf::emit_elf(&self.state, &self.layout, ElfEmitMode::Shared { - relocs: &relocs, - eh_frame_hdr: &eh_hdr, - }) - } -} - -impl LaidOut { - fn relocate_and_emit(mut self, entry: &str, subsystem: u16) -> Result, LinkError> { - let abs_fixups = apply_relocs_pe(&mut self.state, &self.layout)?; - emit_pe_bytes(&self.state, &self.layout, entry, subsystem, &abs_fixups) - } -} - -impl LaidOut { - fn relocate_and_emit(mut self, entry: &str) -> Result, LinkError> { - let params = MachORelocParams { - got: &self.layout.got, - tls_template_start: self.layout.tls_template_start, - }; - let reloc_output = apply_relocs_macho(&mut self.state, ¶ms)?; - - let mut bind_entries: Vec<(String, u64)> = self.layout.got_entries.iter() - .filter(|(_, ext)| *ext) - .map(|(sym, _)| (sym.name().to_string(), self.layout.got[sym])) - .collect(); - // Add non-GOT dynamic binds (e.g. function pointers stored in data) - bind_entries.extend(reloc_output.bind_entries); - - let mut rebase_entries = reloc_output.rebase_entries; - for (sym, ext) in &self.layout.got_entries { - if !ext { - rebase_entries.push(self.layout.got[sym]); - } - } - - emit_macho_bytes(&self.state, &self.layout, entry, &rebase_entries, &bind_entries) - } -} - -// ── Public API ────────────────────────────────────────────────────────── - -/// Link object files and produce a PE32+ executable for UEFI. -/// Input is ELF .o files; output is PE/COFF. -/// `entry` is the entry point symbol name (e.g. "efi_main"). -/// `subsystem` is the PE subsystem (10 = EFI_APPLICATION). -/// Returns the raw PE bytes on success, or a list of undefined symbols on failure. -pub fn link_pe( - objects: &[(String, Vec)], - entry: &str, - subsystem: u16, -) -> Result, LinkError> { - link_pe_with(objects, entry, subsystem, false) -} - -pub fn link_pe_with( - objects: &[(String, Vec)], - entry: &str, - subsystem: u16, - gc: bool, -) -> Result, LinkError> { - let mut collected = Collected::new(objects)?; - if gc { collected.gc_sections(entry); } - collected.layout_pe().relocate_and_emit(entry, subsystem) -} - -/// Link object files and produce a static ELF executable (ET_EXEC). -/// Used for bare-metal targets like x86_64-unknown-none (kernel). -/// `base_addr` sets the load address (e.g. 0xFFFF800000000000 for kernel code model). -/// Returns the raw ELF bytes on success, or a list of undefined symbols on failure. -pub fn link_static( - objects: &[(String, Vec)], - entry: &str, - base_addr: u64, -) -> Result, LinkError> { - link_static_full(objects, entry, base_addr, false, false) -} - -pub fn link_static_with( - objects: &[(String, Vec)], - entry: &str, - base_addr: u64, - gc: bool, -) -> Result, LinkError> { - link_static_full(objects, entry, base_addr, gc, false) -} - -pub fn link_static_full( - objects: &[(String, Vec)], - entry: &str, - base_addr: u64, - gc: bool, - build_id: bool, -) -> Result, LinkError> { - let mut collected = Collected::new(objects)?; - if gc { collected.gc_sections(entry); } - collected.layout_elf(base_addr, None, build_id) - .relocate_and_emit_static(entry) -} - -/// Link object files and produce a PIE ELF executable. -/// Returns the raw ELF bytes on success, or a list of undefined symbols on failure. -pub fn link(objects: &[(String, Vec)], entry: &str) -> Result, LinkError> { - link_full(objects, entry, false, false) -} - -pub fn link_with( - objects: &[(String, Vec)], - entry: &str, - gc: bool, -) -> Result, LinkError> { - link_full(objects, entry, gc, false) -} - -pub fn link_full( - objects: &[(String, Vec)], - entry: &str, - gc: bool, - build_id: bool, -) -> Result, LinkError> { - let mut collected = Collected::new(objects)?; - create_call_stubs(&mut collected.state); - if gc { collected.gc_sections(entry); } - collected.layout_elf(BASE_VADDR, Some(entry), build_id) - .relocate_and_emit_pie(entry) -} - -/// Resolve library names (-l flags) against search paths (-L flags), -/// reading and extracting archives. Only includes archive members that -/// define symbols needed by already-included objects (transitive pull-in). -/// The entry point symbol is treated as an initial undefined, ensuring the -/// archive member that defines it (e.g. std's `_start`) gets pulled in. -pub fn resolve_libs( - inputs: &[PathBuf], - lib_paths: &[PathBuf], - libs: &[String], -) -> Result)>, LinkError> { - resolve_libs_with_entry(inputs, lib_paths, libs, None) -} - -/// Like `resolve_libs`, but seeds the undefined symbol set with `entry` so -/// that archive members providing the entry point are pulled in. -pub fn resolve_libs_with_entry( - inputs: &[PathBuf], - lib_paths: &[PathBuf], - libs: &[String], - entry: Option<&str>, -) -> Result)>, LinkError> { - let mut objects: Vec<(String, Vec)> = Vec::new(); - // Archive members available for pull-in: (archive_name, member_name, data) - let mut archive_members: Vec<(String, Vec)> = Vec::new(); - - // Collect direct inputs and archive members - for path in inputs { - let data = fs::read(path) - .map_err(|e| LinkError::Parse { file: path.display().to_string(), message: e.to_string() })?; - if is_archive(&data) { - extract_archive(&path.display().to_string(), &data, &mut archive_members)?; - } else { - objects.push((path.display().to_string(), data)); - } - } - - for lib in libs { - let (name, data) = find_lib(lib, lib_paths) - .ok_or_else(|| LinkError::Parse { file: format!("-l{lib}"), message: "library not found".to_string() })?; - if is_archive(&data) { - extract_archive(&name, &data, &mut archive_members)?; - } else { - objects.push((name, data)); - } - } - - // Scan direct objects for defined/referenced symbols. - // Symbols from shared libraries (.so) must NOT count as "defined" for archive pull-in — - // they're resolved at load time, not link time. Only .o file definitions prevent pull-in. - // Ordered because the pull-in worklist is seeded from `undefined` and grown - // from `member_refs`: where two archive members define one symbol, the one - // pulled in is whichever the worklist reaches while that symbol is still - // undefined, so the worklist's order decides which sections exist at all. - let mut defined = BTreeSet::new(); - let mut undefined = BTreeSet::new(); - // The entry point is an implicit undefined — ensure its archive member - // gets pulled in even if no direct object references it. - if let Some(entry) = entry { - undefined.insert(entry.to_string()); - } - for (_name, data) in &objects { - // Skip shared libraries for defined-symbol scanning - if is_shared_lib(data) { continue; } - let (defs, refs) = scan_symbols(data); - defined.extend(defs); - undefined.extend(refs); - } - // Only truly undefined: referenced but not yet defined by .o files - undefined.retain(|s| !defined.contains(s)); - - // Scan archive members and build a symbol → member index for O(1) lookup. - let mut member_defs: Vec> = Vec::with_capacity(archive_members.len()); - let mut member_refs: Vec> = Vec::with_capacity(archive_members.len()); - let mut sym_to_members: BTreeMap> = BTreeMap::new(); - for (i, (_, data)) in archive_members.iter().enumerate() { - let (defs, refs) = scan_symbols(data); - for sym in &defs { - sym_to_members.entry(sym.clone()).or_default().push(i); - } - member_defs.push(defs); - member_refs.push(refs); - } - - // Worklist-driven pull-in: process each undefined symbol once - let mut included = vec![false; archive_members.len()]; - let mut worklist: Vec = undefined.iter().cloned().collect(); - while let Some(sym) = worklist.pop() { - if let Some(members) = sym_to_members.get(&sym) { - for &i in members { - if included[i] { continue; } - included[i] = true; - defined.extend(member_defs[i].iter().cloned()); - for r in &member_refs[i] { - if !defined.contains(r) && undefined.insert(r.clone()) { - worklist.push(r.clone()); - } - } - } - } - } - - // Collect the selected archive members in order - for (i, (name, data)) in archive_members.into_iter().enumerate() { - if included[i] { - objects.push((name, data)); - } - } - - Ok(objects) -} - -/// Link object files and produce a shared library (.so) ELF with .dynsym/.dynstr. -pub fn link_shared(objects: &[(String, Vec)]) -> Result, LinkError> { - link_shared_full(objects, false) -} - -pub fn link_shared_full(objects: &[(String, Vec)], build_id: bool) -> Result, LinkError> { - let mut collected = Collected::new(objects)?; - collected.mark_dynamic_symbols(); - create_call_stubs(&mut collected.state); - collected.layout_elf(BASE_VADDR, None, build_id) - .relocate_and_emit_shared() -} - -/// Link object files and produce a Mach-O executable for macOS. -/// Undefined symbols are resolved against /usr/lib/libSystem.B.dylib at runtime. -pub fn link_macho( - objects: &[(String, Vec)], - entry: &str, - gc: bool, -) -> Result, LinkError> { - // Internally all symbol names use ELF convention (no `_` prefix). - // Accept Mach-O-style `_main` from CLI by stripping the prefix. - let entry = entry.strip_prefix('_').unwrap_or(entry); - let mut collected = Collected::new(objects)?; - collected.mark_dynamic_symbols(); - create_call_stubs(&mut collected.state); - collected.mark_abs_reloc_sections_writable(); - if gc { collected.gc_sections(entry); } - collected.layout_macho() - .relocate_and_emit(entry) -} - -/// Create call stubs for relocations targeting dynamic (undefined) symbols. -/// On Mach-O, direct call instructions can't reach dylib functions — the -/// linker must create stubs that load the address from a GOT slot and branch. -fn create_call_stubs(state: &mut collect::LinkState) { - use std::collections::BTreeSet; - - // Find all dynamic symbols with call relocations - let mut stub_syms = BTreeSet::new(); - let call_relocs: &[RelocType] = match state.arch { - Arch::Aarch64 => &[RelocType::Aarch64Call26, RelocType::Aarch64Jump26], - Arch::X86_64 => &[RelocType::X86Plt32, RelocType::X86Pc32], - }; - for reloc in &state.relocs { - if !call_relocs.contains(&reloc.r_type) { continue; } - if let Some(SymbolDef::Dynamic { .. }) = state.globals.get(reloc.target.name()) { - stub_syms.insert(reloc.target.name().to_string()); - } - } - - // On x86_64, Cranelift emits `call *sym@GOTPCREL(%rip)` (FF 15 disp32) for - // external calls instead of `call sym@PLT`. Detect these and redirect them - // through stubs so we can set %al for the x86_64 SysV variadic ABI. - if matches!(state.arch, Arch::X86_64) { - rewrite_x86_64_got_calls(state, &mut stub_syms); - } - - if stub_syms.is_empty() { return; } - - let stub_sec_idx = SectionIdx(state.sections.len()); - - match state.arch { - Arch::Aarch64 => create_aarch64_stubs(state, &stub_syms, stub_sec_idx), - Arch::X86_64 => create_x86_64_stubs(state, &stub_syms, stub_sec_idx), - } - - // Rewrite call relocations to target the stub symbols - let stub_syms_set: BTreeSet<&str> = stub_syms.iter().map(|s| s.as_str()).collect(); - for reloc in &mut state.relocs { - if call_relocs.contains(&reloc.r_type) - && stub_syms_set.contains(reloc.target.name()) - { - reloc.target = SymbolRef::Global(format!("{}.__stub", reloc.target.name())); - } - } -} - -/// Rewrite `movq sym@GOTPCREL(%rip), %reg` to `leaq sym.__stub(%rip), %reg` -/// for function symbols on x86_64. Cranelift loads function addresses from GOT -/// then does `call *%reg`. By making the register hold the stub address instead, -/// all calls go through stubs that set %al for the x86_64 SysV variadic ABI. -/// Non-variadic callees ignore %al, so this is safe for all function calls. -/// -/// Only function symbols (defined in Code sections or Dynamic imports) are -/// rewritten. Data/string GOT loads are left untouched. -fn rewrite_x86_64_got_calls( - state: &mut collect::LinkState, - stub_syms: &mut std::collections::BTreeSet, -) { - let is_function_symbol = |name: &str| -> bool { - match state.globals.get(name) { - Some(SymbolDef::Dynamic { is_func, .. }) => *is_func, - Some(SymbolDef::Defined { section, .. }) => { - state.sections[*section].kind == SectionKind::Code - } - None => false, - } - }; - - let rewrite_indices: Vec = state.relocs.iter().enumerate() - .filter(|(_, r)| { - if !matches!(r.r_type, RelocType::X86Gotpcrel | RelocType::X86Gotpcrelx | RelocType::X86RexGotpcrelx) { - return false; - } - // Only rewrite MOV instructions (opcode 8B at offset-2). - let sec = &state.sections[r.section]; - let off = r.offset as usize; - if off < 2 || sec.data.get(off - 2).copied() != Some(0x8B) { - return false; - } - is_function_symbol(r.target.name()) - }) - .map(|(i, _)| i) - .collect(); - - for &i in &rewrite_indices { - let reloc = &state.relocs[i]; - let sym_name = reloc.target.name().to_string(); - let sec_idx = reloc.section; - let off = reloc.offset as usize; - - // Change opcode from MOV (8B) to LEA (8D) — same encoding otherwise. - // This turns `movq *disp(%rip), %reg` (dereference GOT) into - // `leaq disp(%rip), %reg` (compute stub address). - state.sections[sec_idx].data[off - 2] = 0x8D; - - stub_syms.insert(sym_name.clone()); - - // Change relocation from GOTPCREL → PC32 targeting the stub - state.relocs[i].r_type = RelocType::X86Pc32; - state.relocs[i].target = SymbolRef::Global(format!("{sym_name}.__stub")); - state.relocs[i].addend = -4; - } -} - -fn create_aarch64_stubs( - state: &mut collect::LinkState, - stub_syms: &std::collections::BTreeSet, - stub_sec_idx: SectionIdx, -) { - let stub_count = stub_syms.len(); - let mut stub_data = Vec::with_capacity(stub_count * 12); - let mut stub_relocs = Vec::new(); - - for (i, sym_name) in stub_syms.iter().enumerate() { - let offset = (i * 12) as u64; - // adrp x16, sym@GOTPAGE - stub_data.extend_from_slice(&0x9000_0010u32.to_le_bytes()); - // ldr x16, [x16, sym@GOTLO12] - stub_data.extend_from_slice(&0xF940_0210u32.to_le_bytes()); - // br x16 - stub_data.extend_from_slice(&0xD61F_0200u32.to_le_bytes()); - - stub_relocs.push(collect::InputReloc { - section: stub_sec_idx, offset, - r_type: RelocType::Aarch64AdrGotPage, - target: SymbolRef::Global(sym_name.clone()), addend: 0, - subtrahend: None, - }); - stub_relocs.push(collect::InputReloc { - section: stub_sec_idx, offset: offset + 4, - r_type: RelocType::Aarch64Ld64GotLo12Nc, - target: SymbolRef::Global(sym_name.clone()), addend: 0, - subtrahend: None, - }); - - state.globals.insert(format!("{sym_name}.__stub"), SymbolDef::Defined { - section: stub_sec_idx, value: offset, size: 0, - }); - } - - state.sections.push(collect::InputSection { - name: ".text".to_string(), data: stub_data, align: 4, - size: (stub_count * 12) as u64, vaddr: None, - kind: SectionKind::Code, merge: false, strings: false, entsize: 0, - }); - state.relocs.extend(stub_relocs); -} - -fn create_x86_64_stubs( - state: &mut collect::LinkState, - stub_syms: &std::collections::BTreeSet, - stub_sec_idx: SectionIdx, -) { - // x86-64 stub: 8 bytes per stub - // Each stub: mov $8, %al [B0 08] - // jmpq *sym@GOTPCREL(%rip) [FF 25 xx xx xx xx] - // The mov sets %al = 8 (max XMM arg count) for x86_64 SysV variadic calls. - // Non-variadic callees ignore %al, so this is safe for all dynamic calls. - let stub_count = stub_syms.len(); - let mut stub_data = Vec::with_capacity(stub_count * 8); - let mut stub_relocs = Vec::new(); - - for (i, sym_name) in stub_syms.iter().enumerate() { - let offset = (i * 8) as u64; - // B0 08 = mov $8, %al — conservative upper bound on vector register args - stub_data.extend_from_slice(&[0xB0, 0x08]); - // FF 25 00000000 = jmpq *0(%rip) — placeholder, patched by GOTPCREL reloc - stub_data.extend_from_slice(&[0xFF, 0x25, 0x00, 0x00, 0x00, 0x00]); - - // GOTPCREL reloc at offset+4 (the 4-byte displacement after FF 25) - stub_relocs.push(collect::InputReloc { - section: stub_sec_idx, offset: offset + 4, - r_type: RelocType::X86Gotpcrel, - target: SymbolRef::Global(sym_name.clone()), addend: -4, - subtrahend: None, - }); - - state.globals.insert(format!("{sym_name}.__stub"), SymbolDef::Defined { - section: stub_sec_idx, value: offset, size: 0, - }); - } - - state.sections.push(collect::InputSection { - name: ".text".to_string(), data: stub_data, align: 8, - size: (stub_count * 8) as u64, vaddr: None, - kind: SectionKind::Code, merge: false, strings: false, entsize: 0, - }); - state.relocs.extend(stub_relocs); -} - -// ── Shared helpers ────────────────────────────────────────────────────── - -pub(crate) fn align_up(addr: u64, align: u64) -> u64 { - (addr + align - 1) & !(align - 1) -} - -pub(crate) struct SectionBuckets { - pub(crate) rx: Vec, - pub(crate) rw: Vec, - pub(crate) tls: Vec, -} - -pub(crate) fn classify_sections(state: &mut collect::LinkState) -> SectionBuckets { - let mut buckets = SectionBuckets { rx: Vec::new(), rw: Vec::new(), tls: Vec::new() }; - for (idx, sec) in state.sections.iter().enumerate() { - let idx = SectionIdx(idx); - if sec.kind.is_tls() { - buckets.tls.push(idx); - if !state.tls_sections.contains(&idx) { - state.tls_sections.push(idx); - } - } else if sec.kind.is_writable() { - buckets.rw.push(idx); - } else { - buckets.rx.push(idx); - } - } - // Sort RX: .eh_frame at end (grouped for .eh_frame_hdr generation) - buckets.rx.sort_by_key(|&idx| if state.sections[idx].name == ".eh_frame" { 1u8 } else { 0 }); - // Sort RW: .init_array first, .fini_array second, other PROGBITS, then NOBITS (.bss) - buckets.rw.sort_by_key(|&idx| { - let sec = &state.sections[idx]; - match sec.kind { - SectionKind::InitArray => 0u8, - SectionKind::FiniArray => 1, - SectionKind::Bss => 3, - SectionKind::Data => 2, - // classify_sections routes Code/ReadOnly to rx, TLS to tls - SectionKind::Code | SectionKind::ReadOnly - | SectionKind::Tls | SectionKind::TlsBss - | SectionKind::TlsVariables => unreachable!(), - } - }); - buckets -} diff --git a/toyos-ld/src/main.rs b/toyos-ld/src/main.rs deleted file mode 100644 index 040e170241f..00000000000 --- a/toyos-ld/src/main.rs +++ /dev/null @@ -1,280 +0,0 @@ -use std::fmt::Write as _; -use std::path::{Path, PathBuf}; -use std::time::Instant; -use std::{env, fs, process}; - -fn main() { - let total_start = Instant::now(); - let args = parse_args(); - - let t = Instant::now(); - let objects = match toyos_ld::resolve_libs_with_entry(&args.inputs, &args.lib_paths, &args.libs, Some(&args.entry)) { - Ok(o) => o, - Err(e) => { - eprint!("toyos-ld: {e}"); - process::exit(1); - } - }; - let resolve_time = t.elapsed(); - - if objects.is_empty() { - eprintln!("toyos-ld: no input files"); - process::exit(1); - } - - let t = Instant::now(); - let result = match args.format { - OutputFormat::Shared => toyos_ld::link_shared_full(&objects, args.build_id), - OutputFormat::Pe { subsystem } => toyos_ld::link_pe_with(&objects, &args.entry, subsystem.to_u16(), args.gc_sections), - OutputFormat::Macho => toyos_ld::link_macho(&objects, &args.entry, args.gc_sections), - OutputFormat::Static { image_base } => toyos_ld::link_static_full(&objects, &args.entry, image_base, args.gc_sections, args.build_id), - OutputFormat::Pie => toyos_ld::link_full(&objects, &args.entry, args.gc_sections, args.build_id), - }; - let link_time = t.elapsed(); - - match result { - Ok(output_bytes) => { - // Write to a temp file then atomically rename. On macOS, overwriting - // a signed binary in-place leaves a stale code-signature cache on the - // old inode, causing the kernel to hang the next launch in _dyld_start. - // Rename gives the output a fresh inode every time. - let tmp = args.output.with_extension("tmp"); - fs::write(&tmp, &output_bytes).unwrap_or_else(|e| { - eprintln!("toyos-ld: cannot write {}: {e}", tmp.display()); - process::exit(1); - }); - // The output is a program, and `fs::write` creates at the umask — - // 0644 under the usual one. The rename carries this inode. - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt as _; - fs::set_permissions(&tmp, fs::Permissions::from_mode(0o755)).unwrap_or_else(|e| { - eprintln!("toyos-ld: cannot make {} executable: {e}", tmp.display()); - process::exit(1); - }); - } - fs::rename(&tmp, &args.output).unwrap_or_else(|e| { - eprintln!("toyos-ld: cannot rename {} -> {}: {e}", tmp.display(), args.output.display()); - process::exit(1); - }); - if let Some(map_path) = &args.map_file { - let map = generate_map(&output_bytes, &args.output, &objects); - fs::write(map_path, map).unwrap_or_else(|e| { - eprintln!("toyos-ld: cannot write map {}: {e}", map_path.display()); - process::exit(1); - }); - } - } - Err(e) => { - eprint!("toyos-ld: {e}"); - process::exit(1); - } - } - - // rustc invokes this, and reports anything on our stderr as a warning, so - // stderr is the channel a real linker warning has to arrive on and nothing - // else may share it. An env var rather than a flag because rustc passes the - // environment down untouched, where a flag would need -C link-arg at every - // site the build system sets rustflags. - if env::var_os("TOYOS_LD_TIMING").is_some() { - let output_name = args.output.file_name().unwrap_or_default().to_string_lossy(); - eprintln!("[toyos-ld] {output_name}: {:.3}s (resolve {:.3}s, link {:.3}s, {} objects, {:.1} MB)", - total_start.elapsed().as_secs_f64(), - resolve_time.as_secs_f64(), - link_time.as_secs_f64(), - objects.len(), - objects.iter().map(|(_, d)| d.len()).sum::() as f64 / 1_048_576.0); - } -} - -fn generate_map(output: &[u8], output_path: &Path, inputs: &[(String, Vec)]) -> String { - use object::read::{Object, ObjectSection, ObjectSymbol}; - - let mut map = String::new(); - let _ = writeln!(map, "Linker Map: {}", output_path.display()); - let _ = writeln!(map); - - // Input files - let _ = writeln!(map, "Input files:"); - for (name, data) in inputs { - let _ = writeln!(map, " {name} ({} bytes)", data.len()); - } - let _ = writeln!(map); - - // Try parsing as ELF - if let Ok(elf) = object::read::elf::ElfFile64::::parse(output) { - // Sections - let _ = writeln!(map, "Sections:"); - let _ = writeln!(map, " {:>16} {:>16} {:>10} Name", "Address", "Offset", "Size"); - for section in elf.sections() { - let name = section.name().unwrap_or(""); - if name.is_empty() { continue; } - let _ = writeln!(map, " {:>16x} {:>16x} {:>10x} {}", - section.address(), section.file_range().map(|(o, _)| o).unwrap_or(0), - section.size(), name); - } - let _ = writeln!(map); - - // Symbols - let _ = writeln!(map, "Symbols:"); - let _ = writeln!(map, " {:>16} {:>8} Name", "Value", "Bind"); - let mut syms: Vec<_> = elf.symbols().collect(); - syms.sort_by_key(|s| s.address()); - for sym in syms { - let name = sym.name().unwrap_or(""); - if name.is_empty() { continue; } - let bind = if sym.is_global() { "GLOBAL" } else { "LOCAL" }; - let _ = writeln!(map, " {:>16x} {:>8} {}", sym.address(), bind, name); - } - } - - map -} - -// The prefix is the PE/COFF spec's own: these are `IMAGE_SUBSYSTEM_EFI_*`, and -// a name that drops it names nothing. -#[allow(clippy::enum_variant_names)] -#[derive(Clone, Copy)] -enum PeSubsystem { - EfiApplication, - EfiBootServiceDriver, - EfiRuntimeDriver, -} - -impl PeSubsystem { - fn to_u16(self) -> u16 { - match self { - PeSubsystem::EfiApplication => 10, - PeSubsystem::EfiBootServiceDriver => 11, - PeSubsystem::EfiRuntimeDriver => 12, - } - } -} - -enum OutputFormat { - Pie, - Static { image_base: u64 }, - Shared, - Pe { subsystem: PeSubsystem }, - Macho, -} - -struct Args { - output: PathBuf, - entry: String, - format: OutputFormat, - gc_sections: bool, - build_id: bool, - map_file: Option, - inputs: Vec, - lib_paths: Vec, - libs: Vec, -} - -fn parse_args() -> Args { - let argv: Vec = env::args().collect(); - let mut output = PathBuf::from("a.out"); - let mut entry = String::from("_start"); - let mut shared = false; - let mut is_static = false; - let mut pe = false; - let mut macho = false; - let mut gc_sections = false; - let mut build_id = false; - let mut map_file: Option = None; - let mut image_base = 0x200000u64; - let mut subsystem = PeSubsystem::EfiApplication; - let mut inputs = Vec::new(); - let mut lib_paths = Vec::new(); - let mut libs = Vec::new(); - let mut i = 1; - - while i < argv.len() { - match argv[i].as_str() { - "-o" => { i += 1; output = PathBuf::from(&argv[i]); } - "-e" | "--entry" => { i += 1; entry = argv[i].clone(); } - "-L" => { i += 1; lib_paths.push(PathBuf::from(&argv[i])); } - s if s.starts_with("-L") => { lib_paths.push(PathBuf::from(&s[2..])); } - s if s.starts_with("-l") => { libs.push(s[2..].to_string()); } - "--shared" | "-shared" => { shared = true; } - "--static" => { is_static = true; } - "--pe" => { pe = true; } - "--macho" => { macho = true; } - s if s.starts_with("--subsystem=") => { - subsystem = parse_pe_subsystem(&s["--subsystem=".len()..]); - } - s if s.starts_with("--image-base=") => { - let val = &s["--image-base=".len()..]; - image_base = if val.starts_with("0x") || val.starts_with("0X") { - u64::from_str_radix(&val[2..], 16).unwrap_or_else(|_| { - eprintln!("toyos-ld: invalid --image-base value: {val}"); - process::exit(1); - }) - } else { - val.parse().unwrap_or_else(|_| { - eprintln!("toyos-ld: invalid --image-base value: {val}"); - process::exit(1); - }) - }; - } - // MSVC-style flags (from rustc MSVC linker flavor) - s if s.starts_with("/OUT:") || s.starts_with("/out:") => { - output = PathBuf::from(&s[5..]); - } - s if s.to_ascii_uppercase().starts_with("/ENTRY:") => { - entry = s[7..].to_string(); - pe = true; - } - s if s.to_ascii_uppercase().starts_with("/SUBSYSTEM:") => { - subsystem = parse_pe_subsystem(&s[11..]); - pe = true; - } - s if s.to_ascii_uppercase().starts_with("/LIBPATH:") => { - lib_paths.push(PathBuf::from(&s[9..])); - } - s if s.starts_with('/') && !s[1..].contains('/') && s.as_bytes().get(1).is_some_and(|c| c.is_ascii_uppercase()) => { - // Ignore other MSVC flags (/NOLOGO, /DEBUG, /INCREMENTAL:NO, etc.) - } - // GNU-style flags - "--gc-sections" => { gc_sections = true; } - "--no-gc-sections" => { gc_sections = false; } - "-Map" => { i += 1; map_file = Some(PathBuf::from(&argv[i])); } - s if s.starts_with("-Map=") => { map_file = Some(PathBuf::from(&s[5..])); } - "--build-id" => { build_id = true; } - "-pie" | "--as-needed" | "--no-as-needed" | "--eh-frame-hdr" - | "--hash-style=gnu" | "-Bstatic" | "-static" - | "--no-dynamic-linker" => {} - s if s.starts_with("-z") => { if s == "-z" { i += 1; } } - s if s.starts_with("--") => {} - s if s.starts_with('-') && s.len() > 1 => {} - path => { inputs.push(PathBuf::from(path)); } - } - i += 1; - } - - let format = if shared { - OutputFormat::Shared - } else if pe { - OutputFormat::Pe { subsystem } - } else if macho { - OutputFormat::Macho - } else if is_static { - OutputFormat::Static { image_base } - } else { - OutputFormat::Pie - }; - - Args { output, entry, format, gc_sections, build_id, map_file, inputs, lib_paths, libs } -} - -fn parse_pe_subsystem(val: &str) -> PeSubsystem { - match val.to_ascii_lowercase().as_str() { - "efi_application" | "10" => PeSubsystem::EfiApplication, - "efi_boot_service_driver" | "11" => PeSubsystem::EfiBootServiceDriver, - "efi_runtime_driver" | "12" => PeSubsystem::EfiRuntimeDriver, - _ => { - eprintln!("toyos-ld: invalid subsystem value: {val}"); - process::exit(1); - } - } -} diff --git a/toyos-ld/src/reloc.rs b/toyos-ld/src/reloc.rs deleted file mode 100644 index e0e703a1908..00000000000 --- a/toyos-ld/src/reloc.rs +++ /dev/null @@ -1,1045 +0,0 @@ -use crate::collect::{InputReloc, LinkState, RelocType, SectionIdx, SectionKind, SymbolDef, SymbolRef}; -use crate::emit_pe::PeLayout; -use crate::LinkError; -use std::collections::{BTreeMap, HashSet}; - - -pub(crate) struct RelocOutput { - pub(crate) relatives: Vec<(u64, i64)>, - /// Dynamic GOT entries needing GLOB_DAT relocations: (GOT slot vaddr, symbol name). - pub(crate) glob_dats: Vec<(u64, String)>, - /// GOTTPOFF GOT entries: (GOT slot vaddr, raw tpoff value). Written directly into - /// the output (not as RELATIVE) because RELATIVE would incorrectly add the load base. - pub(crate) tpoff_fills: Vec<(u64, i64)>, - /// R_X86_64_TPOFF64 runtime relocations for shared libraries: (GOT slot vaddr, addend). - /// The addend is the symbol's offset within its module's TLS segment. At load time, - /// the kernel adds the module's TLS base tpoff to produce the final tpoff value. - pub(crate) tpoff64s: Vec<(u64, i64)>, - /// Named R_X86_64_TPOFF64 runtime relocations for cross-library TLS references: - /// (GOT slot vaddr, symbol name). The kernel looks up the symbol across all loaded - /// libraries and computes the correct tpoff. - pub(crate) named_tpoff64s: Vec<(u64, String)>, - /// R_X86_64_TPOFF32 runtime relocations for shared libraries: (vaddr of imm32, addend). - /// Used for LD→LE relaxation in shared mode where DTPOFF32 immediates need load-time patching. - pub(crate) tpoff32s: Vec<(u64, i64)>, - /// R_X86_64_DTPMOD64 runtime relocations for DTV-based TLS in shared libraries: - /// (GOT slot vaddr, addend). Kernel writes the module ID at load time. - pub(crate) dtpmod64s: Vec<(u64, i64)>, - /// R_X86_64_DTPOFF64 runtime relocations for DTV-based TLS in shared libraries: - /// (GOT slot vaddr, addend). Addend is the symbol's offset within its module's TLS segment. - pub(crate) dtpoff64s: Vec<(u64, i64)>, - /// Named R_X86_64_DTPMOD64 for cross-library TLS: (GOT slot vaddr, symbol name). - pub(crate) named_dtpmod64s: Vec<(u64, String)>, - /// Named R_X86_64_DTPOFF64 for cross-library TLS: (GOT slot vaddr, symbol name). - pub(crate) named_dtpoff64s: Vec<(u64, String)>, -} - -/// Resolve a symbol to its virtual address. -/// `plt` provides PLT stubs for dynamic symbols (PIE mode). Pass `None` for -/// static/PE modes where dynamic symbols are unsupported. -pub(crate) fn resolve_symbol( - state: &LinkState, - sym: &SymbolRef, - plt: Option<&BTreeMap>, -) -> Option { - match sym { - SymbolRef::Global(name) => { - match state.globals.get(name)? { - SymbolDef::Dynamic { .. } => plt.and_then(|p| p.get(&SymbolRef::Global(name.clone())).copied()), - SymbolDef::Defined { section, value, .. } => { - let sec = &state.sections[*section]; - Some(sec.vaddr.unwrap_or_else(|| panic!( - "symbol {name:?} in section {:?} ({:?}) has no vaddr", - sec.name, sec.kind, - )) + value) - } - } - } - SymbolRef::Local(obj_idx, name) => { - // Local (static) symbols resolve to the local definition first. - // Only fall back to global if no local exists (e.g., section symbols - // that were promoted to global during merging). - if let Some(SymbolDef::Defined { section, value, .. }) = state.locals.get(&(*obj_idx, name.clone())) { - let sec = &state.sections[*section]; - return Some(sec.vaddr.unwrap_or_else(|| panic!( - "local symbol {name:?} in section {:?} ({:?}) has no vaddr", - sec.name, sec.kind, - )) + value); - } - if let Some(def) = state.globals.get(name) { - return match def { - SymbolDef::Dynamic { .. } => plt.and_then(|p| p.get(&SymbolRef::Global(name.clone())).copied()), - SymbolDef::Defined { section, value, .. } => { - let sec = &state.sections[*section]; - Some(sec.vaddr.unwrap_or_else(|| panic!( - "symbol {name:?} in section {:?} ({:?}) has no vaddr", - sec.name, sec.kind, - )) + value) - } - }; - } - None - } - } -} - -/// x86-64 Variant II: TP points to end of TLS block. -/// TPOFF = symbol_vaddr - (tls_start + tls_memsz) -pub(crate) fn tpoff(sym_addr: u64, tls_start: u64, tls_memsz: u64) -> i64 { - sym_addr as i64 - (tls_start as i64 + tls_memsz as i64) -} - -fn check_i32(value: i64, reloc: &InputReloc) -> Result<(), LinkError> { - if value < i32::MIN as i64 || value > i32::MAX as i64 { - return Err(LinkError::RelocationOverflow { - reloc_type: reloc.r_type, - symbol: reloc.target.name().to_string(), - value, - }); - } - Ok(()) -} - -fn check_u32(value: i64, reloc: &InputReloc) -> Result<(), LinkError> { - if value < 0 || value > u32::MAX as i64 { - return Err(LinkError::RelocationOverflow { - reloc_type: reloc.r_type, - symbol: reloc.target.name().to_string(), - value, - }); - } - Ok(()) -} - -/// Apply a single relocation to section data, returning `true` if it's an absolute -/// reference (needs a runtime relocation / PE base fixup). -fn apply_one_reloc_x86( - data: &mut [u8], - reloc: &InputReloc, - sym_addr: u64, - reloc_vaddr: u64, - got: &BTreeMap, - dyn_got: &BTreeMap, -) -> Result { - match reloc.r_type { - RelocType::X86_64 => { - let value = (sym_addr as i64 + reloc.addend) as u64; - write_u64_data(data, reloc.offset, value); - Ok(true) - } - RelocType::X86Pc32 | RelocType::X86Plt32 => { - let value = sym_addr as i64 + reloc.addend - reloc_vaddr as i64; - check_i32(value, reloc)?; - write_i32_data(data, reloc.offset, value as i32); - Ok(false) - } - RelocType::X86_32 => { - let value = sym_addr as i64 + reloc.addend; - check_u32(value, reloc)?; - write_u32_data(data, reloc.offset, value as u32); - Ok(false) - } - RelocType::X86_32S => { - let value = sym_addr as i64 + reloc.addend; - check_i32(value, reloc)?; - write_i32_data(data, reloc.offset, value as i32); - Ok(false) - } - RelocType::X86Gotpcrel | RelocType::X86Gotpcrelx - | RelocType::X86RexGotpcrelx => { - let got_slot = got.get(&reloc.target) - .or_else(|| dyn_got.get(&reloc.target)) - .or_else(|| dyn_got.get(&SymbolRef::Global(reloc.target.name().to_string()))) - .ok_or_else(|| { - LinkError::UndefinedSymbols(vec![reloc.target.name().to_string()]) - })?; - let value = *got_slot as i64 + reloc.addend - reloc_vaddr as i64; - check_i32(value, reloc)?; - write_i32_data(data, reloc.offset, value as i32); - Ok(false) - } - RelocType::X86Tlv => { - // Mach-O X86_64_RELOC_TLV: rewrite `movq disp(%rip), %reg` to - // `leaq disp(%rip), %reg` so the register gets the ADDRESS of the - // TLV descriptor, not its contents. Change opcode 0x8B → 0x8D. - let op_off = reloc.offset as usize - 2; - if data[op_off] == 0x8b { - data[op_off] = 0x8d; // movq → leaq - } - let value = sym_addr as i64 + reloc.addend - reloc_vaddr as i64; - check_i32(value, reloc)?; - write_i32_data(data, reloc.offset, value as i32); - Ok(false) - } - other => Err(LinkError::UnsupportedRelocation { - reloc_type: other, - symbol: reloc.target.name().to_string(), - }), - } -} - -fn write_bytes_data(data: &mut [u8], offset: u64, bytes: &[u8]) { - let off = offset as usize; - data[off..off + bytes.len()].copy_from_slice(bytes); -} - -fn write_u64_data(data: &mut [u8], offset: u64, value: u64) { - write_bytes_data(data, offset, &value.to_le_bytes()); -} - -fn write_i32_data(data: &mut [u8], offset: u64, value: i32) { - write_bytes_data(data, offset, &value.to_le_bytes()); -} - -fn write_u32_data(data: &mut [u8], offset: u64, value: u32) { - write_bytes_data(data, offset, &value.to_le_bytes()); -} - -fn write_bytes(state: &mut LinkState, sec_idx: SectionIdx, offset: u64, bytes: &[u8]) { - write_bytes_data(&mut state.sections[sec_idx].data, offset, bytes); -} - -fn write_i32(state: &mut LinkState, sec_idx: SectionIdx, offset: u64, value: i32) { - write_i32_data(&mut state.sections[sec_idx].data, offset, value); -} - -fn write_u32(state: &mut LinkState, sec_idx: SectionIdx, offset: u64, value: u32) { - write_u32_data(&mut state.sections[sec_idx].data, offset, value); -} - -fn write_u64(state: &mut LinkState, sec_idx: SectionIdx, offset: u64, value: u64) { - write_u64_data(&mut state.sections[sec_idx].data, offset, value); -} - -/// Detect whether a TLS GD/LD relocation uses the 16-byte padded or 12-byte -/// unpadded instruction sequence by examining the byte before the leaq. -/// Padded: `data16; leaq ...` → byte at offset-4 is 0x66 -/// Unpadded: `leaq ...` → byte at offset-3 is 0x48 (REX.W) -fn is_padded_tls_sequence(sec_data: &[u8], reloc_offset: u64) -> bool { - let off = reloc_offset as usize; - off >= 4 && sec_data[off - 4] == 0x66 -} - -/// Parameters for ELF relocation application (shared between PIE and static modes). -pub(crate) struct ElfRelocParams<'a> { - pub(crate) got: &'a BTreeMap, - pub(crate) tls_start: u64, - pub(crate) tls_memsz: u64, - pub(crate) plt: Option<&'a BTreeMap>, - pub(crate) dyn_got: &'a BTreeMap, - /// PIE mode: record R_X86_64_RELATIVE entries for runtime relocation. - /// Static mode: addresses are fixed at link time, no RELATIVE needed. - pub(crate) record_relatives: bool, - pub(crate) allow_undefined: bool, - /// Shared library mode: TLS offsets are not known at link time. GD→IE relaxation - /// is used instead of GD→LE, and R_X86_64_TPOFF64 runtime relocs are emitted. - pub(crate) is_shared: bool, - /// Shared LD GOT pair for all TLSLD accesses (shared mode only). - pub(crate) ld_got_pair: Option, - /// Per-symbol GD GOT pairs for TLSGD accesses (shared mode only). - pub(crate) gd_got: &'a BTreeMap, -} - -pub(crate) fn apply_relocs( - state: &mut LinkState, - params: &ElfRelocParams, -) -> Result { - let mut relatives = Vec::new(); - let mut undefined = HashSet::new(); - let mut tpoff64_entries: Vec<(u64, i64)> = Vec::new(); - let mut named_tpoff64_entries: Vec<(u64, String)> = Vec::new(); - let mut tpoff32_entries: Vec<(u64, i64)> = Vec::new(); - let mut dtpmod64_entries: Vec<(u64, i64)> = Vec::new(); - let mut dtpoff64_entries: Vec<(u64, i64)> = Vec::new(); - let mut named_dtpmod64_entries: Vec<(u64, String)> = Vec::new(); - let mut named_dtpoff64_entries: Vec<(u64, String)> = Vec::new(); - - let relocs = std::mem::take(&mut state.relocs); - - // Pass 1: TLS GD/LD/DTPOFF relaxations. These rewrite instruction bytes - // and overwrite the companion `call __tls_get_addr` instruction, so we - // track which (section, offset) ranges were relaxed. - let mut relaxed_calls: HashSet<(SectionIdx, u64)> = HashSet::new(); - - for reloc in &relocs { - match reloc.r_type { - RelocType::X86Tlsgd => { - let is_dynamic = matches!( - state.globals.get(reloc.target.name()), - Some(SymbolDef::Dynamic { .. }) - ); - let padded = is_padded_tls_sequence( - &state.sections[reloc.section].data, - reloc.offset, - ); - if !padded { - return Err(LinkError::UnsupportedRelocation { - reloc_type: RelocType::X86Tlsgd, - symbol: reloc.target.name().to_string(), - }); - } - if params.is_shared { - // Shared mode: preserve GD sequence for DTV-based TLS. - // Keep `call __tls_get_addr` intact. Emit DTPMOD64+DTPOFF64 - // for the two consecutive GOT slots (TlsIndex pair). - // Use gd_got (separate from per-symbol got to avoid GOTTPOFF collisions). - let got_slot = *params.gd_got.get(&reloc.target).ok_or_else(|| { - LinkError::UndefinedSymbols(vec![reloc.target.name().to_string()]) - })?; - // Patch leaq's disp32 to point to GOT slot pair - let sec_vaddr = state.sections[reloc.section].vaddr.unwrap(); - let rip = sec_vaddr + reloc.offset + 4; // end of leaq instruction - let disp = got_slot as i64 - rip as i64; - check_i32(disp, reloc)?; - write_i32(state, reloc.section, reloc.offset, disp as i32); - // Don't add to relaxed_calls — let the call __tls_get_addr PLT32 reloc fire - if is_dynamic { - // Cross-library TLS: named relocations for kernel to resolve - named_dtpmod64_entries.push((got_slot, reloc.target.name().to_string())); - named_dtpoff64_entries.push((got_slot + 8, reloc.target.name().to_string())); - } else { - // Same-module TLS: kernel writes module ID, offset is known - let sym_addr = resolve_symbol(state, &reloc.target, params.plt) - .ok_or_else(|| LinkError::UndefinedSymbols(vec![reloc.target.name().to_string()]))?; - let sym_tls_offset = sym_addr as i64 - params.tls_start as i64; - dtpmod64_entries.push((got_slot, 0)); - dtpoff64_entries.push((got_slot + 8, sym_tls_offset)); - } - } else if is_dynamic { - // PIE with dynamic TLS: GD → IE relaxation - // `data16; leaq; data16*2; rex64; call` - // → `mov %fs:0,%rax; add sym@GOTTPOFF(%rip),%rax` - #[rustfmt::skip] - let inst: [u8; 16] = [ - 0x64, 0x48, 0x8b, 0x04, 0x25, 0x00, 0x00, 0x00, 0x00, // mov %fs:0,%rax - 0x48, 0x03, 0x05, 0x00, 0x00, 0x00, 0x00, // add 0(%rip),%rax - ]; - write_bytes(state, reloc.section, reloc.offset - 4, &inst); - let got_slot = *params.got.get(&reloc.target).ok_or_else(|| { - LinkError::UndefinedSymbols(vec![reloc.target.name().to_string()]) - })?; - let sec_vaddr = state.sections[reloc.section].vaddr.unwrap(); - let rip = sec_vaddr + reloc.offset + 12; // end of `add` instruction - let disp = got_slot as i64 - rip as i64; - check_i32(disp, reloc)?; - write_i32(state, reloc.section, reloc.offset + 8, disp as i32); - relaxed_calls.insert((reloc.section, reloc.offset + 8)); - // Cross-library TLS: emit named TPOFF64 for kernel to resolve - named_tpoff64_entries.push((got_slot, reloc.target.name().to_string())); - } else { - let sym_addr = resolve_symbol(state, &reloc.target, params.plt) - .ok_or_else(|| LinkError::UndefinedSymbols(vec![reloc.target.name().to_string()]))?; - // GD → LE (16-byte padded): `data16; leaq; data16*2; rex64; call` - // → `mov %fs:0,%rax; lea tpoff(%rax),%rax` - #[rustfmt::skip] - let inst: [u8; 16] = [ - 0x64, 0x48, 0x8b, 0x04, 0x25, 0x00, 0x00, 0x00, 0x00, // mov %fs:0,%rax - 0x48, 0x8d, 0x80, 0x00, 0x00, 0x00, 0x00, // lea 0(%rax),%rax - ]; - write_bytes(state, reloc.section, reloc.offset - 4, &inst); - let tp_value = tpoff(sym_addr, params.tls_start, params.tls_memsz); - check_i32(tp_value, reloc)?; - write_i32(state, reloc.section, reloc.offset + 8, tp_value as i32); - relaxed_calls.insert((reloc.section, reloc.offset + 8)); - } - } - RelocType::X86Tlsld => { - if params.is_shared { - // Shared mode: preserve LD sequence for DTV-based TLS. - // Keep `call __tls_get_addr` intact. Emit DTPMOD64 for the - // dedicated LD GOT pair (separate from per-symbol GOTTPOFF slots). - let got_slot = params.ld_got_pair - .expect("TLSLD in shared mode but no LD GOT pair allocated"); - let sec_vaddr = state.sections[reloc.section].vaddr.unwrap(); - let rip = sec_vaddr + reloc.offset + 4; // end of leaq instruction - let disp = got_slot as i64 - rip as i64; - check_i32(disp, reloc)?; - write_i32(state, reloc.section, reloc.offset, disp as i32); - // Don't add to relaxed_calls — keep the call __tls_get_addr - // Emit DTPMOD64 for module ID (offset slot is 0 for LD) - dtpmod64_entries.push((got_slot, 0)); - dtpoff64_entries.push((got_slot + 8, 0)); - } else { - // LD → LE: get thread pointer - let padded = is_padded_tls_sequence( - &state.sections[reloc.section].data, - reloc.offset, - ); - if padded { - // LD → LE (16-byte padded) - #[rustfmt::skip] - let inst: [u8; 16] = [ - 0x66, 0x66, 0x66, // 3x data16 - 0x64, 0x48, 0x8b, 0x04, 0x25, 0x00, 0x00, 0x00, 0x00, // mov %fs:0,%rax - 0x0f, 0x1f, 0x40, 0x00, // nopl 0(%rax) - ]; - write_bytes(state, reloc.section, reloc.offset - 4, &inst); - relaxed_calls.insert((reloc.section, reloc.offset + 8)); - } else { - // LD → LE (12-byte unpadded) - #[rustfmt::skip] - let inst: [u8; 12] = [ - 0x66, 0x66, 0x66, // 3x data16 - 0x64, 0x48, 0x8b, 0x04, 0x25, 0x00, 0x00, 0x00, 0x00, // mov %fs:0,%rax - ]; - write_bytes(state, reloc.section, reloc.offset - 3, &inst); - relaxed_calls.insert((reloc.section, reloc.offset + 5)); - } - } - } - RelocType::X86Dtpoff32 => { - let sym_addr = resolve_symbol(state, &reloc.target, params.plt) - .ok_or_else(|| LinkError::UndefinedSymbols(vec![reloc.target.name().to_string()]))?; - if params.is_shared { - // Shared mode: LD is not relaxed, so __tls_get_addr returned the - // module's TLS base. DTPOFF32 is just a literal offset from there. - let value = sym_addr as i64 - params.tls_start as i64 + reloc.addend; - check_i32(value, reloc)?; - write_i32(state, reloc.section, reloc.offset, value as i32); - } else { - let value = tpoff(sym_addr, params.tls_start, params.tls_memsz) + reloc.addend; - check_i32(value, reloc)?; - write_i32(state, reloc.section, reloc.offset, value as i32); - } - } - // Handled in pass 2 - RelocType::X86_64 | RelocType::X86Pc32 | RelocType::X86Plt32 - | RelocType::X86_32 | RelocType::X86_32S - | RelocType::X86Gotpcrel | RelocType::X86Gotpcrelx - | RelocType::X86RexGotpcrelx - | RelocType::X86Tpoff32 | RelocType::X86Gottpoff - | RelocType::X86Tlv - | RelocType::Aarch64Abs64 | RelocType::Aarch64Abs32 - | RelocType::Aarch64Prel32 - | RelocType::Aarch64Call26 | RelocType::Aarch64Jump26 - | RelocType::Aarch64AdrPrelPgHi21 | RelocType::Aarch64AddAbsLo12Nc - | RelocType::Aarch64Ldst8AbsLo12Nc | RelocType::Aarch64Ldst16AbsLo12Nc - | RelocType::Aarch64Ldst32AbsLo12Nc | RelocType::Aarch64Ldst64AbsLo12Nc - | RelocType::Aarch64Ldst128AbsLo12Nc - | RelocType::Aarch64MovwUabsG0Nc | RelocType::Aarch64MovwUabsG1Nc - | RelocType::Aarch64MovwUabsG2Nc | RelocType::Aarch64MovwUabsG3 - | RelocType::Aarch64AdrGotPage | RelocType::Aarch64Ld64GotLo12Nc - | RelocType::Aarch64GotPcrel32 - | RelocType::Aarch64TlvpLoadPage21 - | RelocType::Aarch64TlvpLoadPageoff12 => {} - } - } - - // Pass 2: all other relocations. - for reloc in &relocs { - if matches!(reloc.r_type, RelocType::X86Tlsgd | RelocType::X86Tlsld | RelocType::X86Dtpoff32) { - continue; - } - if relaxed_calls.contains(&(reloc.section, reloc.offset)) { - continue; - } - - let sec_vaddr = state.sections[reloc.section].vaddr.unwrap_or_else(|| panic!( - "reloc section {:?} ({:?}) has no vaddr", - state.sections[reloc.section].name, state.sections[reloc.section].kind, - )); - let reloc_vaddr = sec_vaddr + reloc.offset; - - // Gottpoff relocations only need the GOT slot, not the symbol address. - if reloc.r_type == RelocType::X86Gottpoff { - let got_slot = params.got.get(&reloc.target) - .ok_or_else(|| LinkError::UndefinedSymbols(vec![reloc.target.name().to_string()]))?; - let value = *got_slot as i64 + reloc.addend - reloc_vaddr as i64; - check_i32(value, reloc)?; - write_i32(state, reloc.section, reloc.offset, value as i32); - continue; - } - - let sym_addr = match resolve_symbol(state, &reloc.target, params.plt) { - Some(a) => a, - None => { - undefined.insert(reloc.target.name().to_string()); - continue; - } - }; - - let data = &mut state.sections[reloc.section].data; - match reloc.r_type { - RelocType::X86Tpoff32 => { - if params.is_shared { - let sym_tls_offset = sym_addr as i64 - params.tls_start as i64 + reloc.addend; - write_i32_data(data, reloc.offset, 0); - tpoff32_entries.push((reloc_vaddr, sym_tls_offset)); - } else { - let value = tpoff(sym_addr, params.tls_start, params.tls_memsz) + reloc.addend; - check_i32(value, reloc)?; - write_i32_data(data, reloc.offset, value as i32); - } - } - RelocType::X86Gottpoff => unreachable!("handled above"), - RelocType::X86Tlsgd | RelocType::X86Tlsld | RelocType::X86Dtpoff32 => { - unreachable!("handled in pass 1") - } - RelocType::X86_64 | RelocType::X86Pc32 | RelocType::X86Plt32 - | RelocType::X86_32 | RelocType::X86_32S - | RelocType::X86Gotpcrel | RelocType::X86Gotpcrelx - | RelocType::X86RexGotpcrelx - | RelocType::X86Tlv => { - let is_abs = - apply_one_reloc_x86(data, reloc, sym_addr, reloc_vaddr, params.got, params.dyn_got)?; - if is_abs && params.record_relatives { - relatives.push((reloc_vaddr, sym_addr as i64 + reloc.addend)); - } - } - RelocType::Aarch64Abs64 | RelocType::Aarch64Abs32 - | RelocType::Aarch64Prel32 - | RelocType::Aarch64Call26 | RelocType::Aarch64Jump26 - | RelocType::Aarch64AdrPrelPgHi21 | RelocType::Aarch64AddAbsLo12Nc - | RelocType::Aarch64Ldst8AbsLo12Nc | RelocType::Aarch64Ldst16AbsLo12Nc - | RelocType::Aarch64Ldst32AbsLo12Nc | RelocType::Aarch64Ldst64AbsLo12Nc - | RelocType::Aarch64Ldst128AbsLo12Nc - | RelocType::Aarch64MovwUabsG0Nc | RelocType::Aarch64MovwUabsG1Nc - | RelocType::Aarch64MovwUabsG2Nc | RelocType::Aarch64MovwUabsG3 - | RelocType::Aarch64AdrGotPage | RelocType::Aarch64Ld64GotLo12Nc - | RelocType::Aarch64GotPcrel32 - | RelocType::Aarch64TlvpLoadPage21 - | RelocType::Aarch64TlvpLoadPageoff12 => { - let is_abs = - apply_one_reloc_aarch64(data, reloc, sym_addr, reloc_vaddr, params.got)?; - if is_abs && params.record_relatives { - relatives.push((reloc_vaddr, sym_addr as i64 + reloc.addend)); - } - } - } - } - - // Fill GOT entries (PIE mode records as RELATIVE; static mode handles in emit) - let mut tpoff_fills = Vec::new(); - if params.record_relatives { - let gottpoff_syms: HashSet = relocs - .iter() - .filter(|r| r.r_type == RelocType::X86Gottpoff) - .map(|r| r.target.clone()) - .collect(); - - for (sym_ref, &got_vaddr) in params.got { - // Dynamic symbols are in dyn_got, resolved at load time via GLOB_DAT - if params.dyn_got.contains_key(sym_ref) { continue; } - // GD→IE GOT entries are handled via tpoff64/named_tpoff64 entries (collected in pass 1) - if tpoff64_entries.iter().any(|&(v, _)| v == got_vaddr) { continue; } - if named_tpoff64_entries.iter().any(|(v, _)| *v == got_vaddr) { continue; } - // DTV GOT entries are handled via dtpmod64/dtpoff64 entries (collected in pass 1) - if dtpmod64_entries.iter().any(|&(v, _)| v == got_vaddr) { continue; } - if named_dtpmod64_entries.iter().any(|(v, _)| *v == got_vaddr) { continue; } - // Dynamic TLS symbols: emit named TPOFF64 for kernel to resolve - let is_dynamic_tls = gottpoff_syms.contains(sym_ref) - && matches!(state.globals.get(sym_ref.name()), Some(SymbolDef::Dynamic { .. })); - if is_dynamic_tls { - named_tpoff64_entries.push((got_vaddr, sym_ref.name().to_string())); - continue; - } - let sym_addr = resolve_symbol(state, sym_ref, params.plt) - .ok_or_else(|| LinkError::UndefinedSymbols(vec![sym_ref.name().to_string()]))?; - if gottpoff_syms.contains(sym_ref) { - if params.is_shared { - // Shared mode: emit TPOFF64 runtime reloc instead of filling at link time - let sym_tls_offset = sym_addr as i64 - params.tls_start as i64; - tpoff64_entries.push((got_vaddr, sym_tls_offset)); - } else { - // PIE mode: write raw tpoff directly — NOT as RELATIVE (RELATIVE - // would add the load base, corrupting the TP-relative offset). - let tp = tpoff(sym_addr, params.tls_start, params.tls_memsz); - tpoff_fills.push((got_vaddr, tp)); - } - } else { - relatives.push((got_vaddr, sym_addr as i64)); - } - } - } - - // Collect dynamic GOT entries as GLOB_DAT relocations (resolved at load time) - let mut glob_dats = Vec::new(); - for (sym_ref, &got_vaddr) in params.dyn_got { - glob_dats.push((got_vaddr, sym_ref.name().to_string())); - } - - if !params.allow_undefined && !undefined.is_empty() { - let mut syms: Vec = undefined.into_iter().collect(); - syms.sort(); - return Err(LinkError::UndefinedSymbols(syms)); - } - - Ok(RelocOutput { - relatives, - glob_dats, - tpoff_fills, - tpoff64s: tpoff64_entries, - named_tpoff64s: named_tpoff64_entries, - tpoff32s: tpoff32_entries, - dtpmod64s: dtpmod64_entries, - dtpoff64s: dtpoff64_entries, - named_dtpmod64s: named_dtpmod64_entries, - named_dtpoff64s: named_dtpoff64_entries, - }) -} - -// ── AArch64 relocation helpers ─────────────────────────────────────────── - -/// Apply a single AArch64 relocation to section data. Returns `true` if it -/// produced an absolute reference that needs a Mach-O rebase entry. -fn apply_one_reloc_aarch64( - data: &mut [u8], - reloc: &InputReloc, - sym_addr: u64, - reloc_vaddr: u64, - got: &BTreeMap, -) -> Result { - match reloc.r_type { - RelocType::Aarch64Abs64 => { - let value = (sym_addr as i64 + reloc.addend) as u64; - write_u64_data(data, reloc.offset, value); - Ok(true) - } - RelocType::Aarch64Abs32 => { - let value = sym_addr as i64 + reloc.addend; - write_u32_data(data, reloc.offset, value as u32); - Ok(false) - } - RelocType::Aarch64Prel32 => { - let value = sym_addr as i64 + reloc.addend - reloc_vaddr as i64; - write_i32_data(data, reloc.offset, value as i32); - Ok(false) - } - RelocType::Aarch64Call26 | RelocType::Aarch64Jump26 => { - let value = sym_addr as i64 + reloc.addend - reloc_vaddr as i64; - let imm26 = value >> 2; - if !(-(1 << 25)..(1 << 25)).contains(&imm26) { - return Err(LinkError::RelocationOverflow { - reloc_type: reloc.r_type, symbol: reloc.target.name().to_string(), value, - }); - } - patch_aarch64_insn_imm26_data(data, reloc.offset, imm26 as u32); - Ok(false) - } - RelocType::Aarch64MovwUabsG0Nc => { - let value = ((sym_addr as i64 + reloc.addend) & 0xFFFF) as u32; - patch_aarch64_movw_data(data, reloc.offset, value); - Ok(false) - } - RelocType::Aarch64MovwUabsG1Nc => { - let value = (((sym_addr as i64 + reloc.addend) >> 16) & 0xFFFF) as u32; - patch_aarch64_movw_data(data, reloc.offset, value); - Ok(false) - } - RelocType::Aarch64MovwUabsG2Nc => { - let value = (((sym_addr as i64 + reloc.addend) >> 32) & 0xFFFF) as u32; - patch_aarch64_movw_data(data, reloc.offset, value); - Ok(false) - } - RelocType::Aarch64MovwUabsG3 => { - let value = (((sym_addr as i64 + reloc.addend) >> 48) & 0xFFFF) as u32; - patch_aarch64_movw_data(data, reloc.offset, value); - Ok(false) - } - RelocType::Aarch64AdrPrelPgHi21 => { - let sym_page = (sym_addr as i64 + reloc.addend) & !0xFFF; - let pc_page = reloc_vaddr as i64 & !0xFFF; - let page_delta = (sym_page - pc_page) >> 12; - if !(-(1 << 20)..(1 << 20)).contains(&page_delta) { - return Err(LinkError::RelocationOverflow { - reloc_type: reloc.r_type, symbol: reloc.target.name().to_string(), value: page_delta, - }); - } - patch_aarch64_adrp_data(data, reloc.offset, page_delta as i32); - Ok(false) - } - RelocType::Aarch64AddAbsLo12Nc => { - let value = ((sym_addr as i64 + reloc.addend) & 0xFFF) as u32; - patch_aarch64_add_imm12_data(data, reloc.offset, value); - Ok(false) - } - RelocType::Aarch64Ldst8AbsLo12Nc => { - let value = ((sym_addr as i64 + reloc.addend) & 0xFFF) as u32; - patch_aarch64_ldr_imm12_data(data, reloc.offset, value, 0); - Ok(false) - } - RelocType::Aarch64Ldst16AbsLo12Nc => { - let value = ((sym_addr as i64 + reloc.addend) & 0xFFF) as u32; - patch_aarch64_ldr_imm12_data(data, reloc.offset, value, 1); - Ok(false) - } - RelocType::Aarch64Ldst32AbsLo12Nc => { - let value = ((sym_addr as i64 + reloc.addend) & 0xFFF) as u32; - patch_aarch64_ldr_imm12_data(data, reloc.offset, value, 2); - Ok(false) - } - RelocType::Aarch64Ldst64AbsLo12Nc => { - let value = ((sym_addr as i64 + reloc.addend) & 0xFFF) as u32; - patch_aarch64_ldr_imm12_data(data, reloc.offset, value, 3); - Ok(false) - } - RelocType::Aarch64Ldst128AbsLo12Nc => { - let value = ((sym_addr as i64 + reloc.addend) & 0xFFF) as u32; - patch_aarch64_ldr_imm12_data(data, reloc.offset, value, 4); - Ok(false) - } - RelocType::Aarch64AdrGotPage => { - let got_slot = *got.get(&reloc.target).ok_or_else(|| { - LinkError::UndefinedSymbols(vec![reloc.target.name().to_string()]) - })?; - let sym_page = got_slot as i64 & !0xFFF; - let pc_page = reloc_vaddr as i64 & !0xFFF; - let page_delta = (sym_page - pc_page) >> 12; - if !(-(1 << 20)..(1 << 20)).contains(&page_delta) { - return Err(LinkError::RelocationOverflow { - reloc_type: reloc.r_type, symbol: reloc.target.name().to_string(), value: page_delta, - }); - } - patch_aarch64_adrp_data(data, reloc.offset, page_delta as i32); - Ok(false) - } - RelocType::Aarch64Ld64GotLo12Nc => { - let got_slot = *got.get(&reloc.target).ok_or_else(|| { - LinkError::UndefinedSymbols(vec![reloc.target.name().to_string()]) - })?; - let value = (got_slot & 0xFFF) as u32; - patch_aarch64_ldr_imm12_data(data, reloc.offset, value, 3); - Ok(false) - } - RelocType::Aarch64TlvpLoadPageoff12 => { - let value = ((sym_addr as i64 + reloc.addend) & 0xFFF) as u32; - let insn_off = reloc.offset as usize; - let old_insn = u32::from_le_bytes(data[insn_off..insn_off+4].try_into().unwrap()); - let rd = old_insn & 0x1F; - let rn = (old_insn >> 5) & 0x1F; - let new_insn: u32 = 0x91000000 | (value << 10) | (rn << 5) | rd; - data[insn_off..insn_off+4].copy_from_slice(&new_insn.to_le_bytes()); - Ok(false) - } - RelocType::Aarch64TlvpLoadPage21 => { - let sym_page = (sym_addr as i64 + reloc.addend) & !0xFFF; - let pc_page = reloc_vaddr as i64 & !0xFFF; - let page_delta = (sym_page - pc_page) >> 12; - if !(-(1 << 20)..(1 << 20)).contains(&page_delta) { - return Err(LinkError::RelocationOverflow { - reloc_type: reloc.r_type, symbol: reloc.target.name().to_string(), value: page_delta, - }); - } - patch_aarch64_adrp_data(data, reloc.offset, page_delta as i32); - Ok(false) - } - RelocType::Aarch64GotPcrel32 => { - let got_slot = *got.get(&reloc.target).ok_or_else(|| { - LinkError::UndefinedSymbols(vec![reloc.target.name().to_string()]) - })?; - let value = got_slot as i64 + reloc.addend - reloc_vaddr as i64; - check_i32(value, reloc)?; - write_i32_data(data, reloc.offset, value as i32); - Ok(false) - } - other => Err(LinkError::UnsupportedRelocation { - reloc_type: other, symbol: reloc.target.name().to_string(), - }), - } -} - -/// Read-modify-write a 32-bit LE instruction in section data. -fn modify_insn_data(data: &mut [u8], offset: u64, f: impl FnOnce(u32) -> u32) { - let off = offset as usize; - let insn = u32::from_le_bytes(data[off..off + 4].try_into().unwrap()); - data[off..off + 4].copy_from_slice(&f(insn).to_le_bytes()); -} - -fn patch_aarch64_adrp_data(data: &mut [u8], offset: u64, page_delta: i32) { - let val = page_delta as u32; - modify_insn_data(data, offset, |insn| - (insn & 0x9F00_001F) | ((val & 0x3) << 29) | (((val >> 2) & 0x7FFFF) << 5)); -} - -fn patch_aarch64_insn_imm26_data(data: &mut [u8], offset: u64, imm26: u32) { - modify_insn_data(data, offset, |insn| (insn & 0xFC00_0000) | (imm26 & 0x03FF_FFFF)); -} - -fn patch_aarch64_movw_data(data: &mut [u8], offset: u64, value: u32) { - modify_insn_data(data, offset, |insn| (insn & !(0xFFFF << 5)) | ((value & 0xFFFF) << 5)); -} - -fn patch_aarch64_add_imm12_data(data: &mut [u8], offset: u64, value: u32) { - modify_insn_data(data, offset, |insn| (insn & !(0xFFF << 10)) | ((value & 0xFFF) << 10)); -} - -fn patch_aarch64_ldr_imm12_data(data: &mut [u8], offset: u64, value: u32, scale: u32) { - modify_insn_data(data, offset, |insn| { - let scaled = (value >> scale) & 0xFFF; - (insn & !(0xFFF << 10)) | (scaled << 10) - }); -} - -/// Parameters for Mach-O relocation application. -pub(crate) struct MachORelocParams<'a> { - pub(crate) got: &'a BTreeMap, - /// Start of the TLS template (__thread_data vmaddr). Pointers from __thread_vars - /// into __thread_data/__thread_bss are stored as template-relative offsets. - pub(crate) tls_template_start: u64, -} - -/// Apply relocations for Mach-O output. Returns rebase entries (internal absolute -/// pointers) and bind entries (dynamic symbol references for dyld). -pub(crate) fn apply_relocs_macho( - state: &mut LinkState, - params: &MachORelocParams, -) -> Result { - let mut rebase_entries = Vec::new(); - let mut bind_entries = Vec::new(); - let mut undefined = std::collections::HashSet::new(); - - let relocs = std::mem::take(&mut state.relocs); - - for reloc in &relocs { - let sec = &state.sections[reloc.section]; - let reloc_vaddr = sec.vaddr.unwrap_or_else(|| panic!( - "reloc section {:?} ({:?}) has no vaddr", sec.name, sec.kind, - )) + reloc.offset; - - // GOT relocations don't need the symbol address — they use the GOT - // slot address, which is looked up by name inside the reloc handler. - let is_got_reloc = matches!(reloc.r_type, - RelocType::Aarch64AdrGotPage | RelocType::Aarch64Ld64GotLo12Nc - | RelocType::Aarch64GotPcrel32 - | RelocType::X86Gotpcrel | RelocType::X86Gotpcrelx - | RelocType::X86RexGotpcrelx); - - // SUBTRACTOR pair: resolve both symbols and compute the difference. - // The result is position-independent (no rebase entry needed). - if let Some(ref sub_sym) = reloc.subtrahend { - let minuend = match resolve_symbol(state, &reloc.target, None) { - Some(a) => a, - None => { - undefined.insert(reloc.target.name().to_string()); - continue; - } - }; - let subtrahend = match resolve_symbol(state, sub_sym, None) { - Some(a) => a, - None => { - undefined.insert(sub_sym.name().to_string()); - continue; - } - }; - let value = minuend as i64 - subtrahend as i64 + reloc.addend; - match reloc.r_type { - RelocType::Aarch64Abs64 | RelocType::X86_64 => - write_u64(state, reloc.section, reloc.offset, value as u64), - RelocType::Aarch64Abs32 | RelocType::X86_32 => - write_u32(state, reloc.section, reloc.offset, value as u32), - RelocType::X86_32S => - write_i32(state, reloc.section, reloc.offset, value as i32), - other => return Err(LinkError::UnsupportedRelocation { - reloc_type: other, - symbol: reloc.target.name().to_string(), - }), - } - continue; - } - - let sym_addr = match resolve_symbol(state, &reloc.target, None) { - Some(a) => a, - None if is_got_reloc && params.got.contains_key(&reloc.target) => 0, - None if matches!(reloc.r_type, RelocType::Aarch64Abs64 | RelocType::X86_64) => { - // Absolute pointer to an external symbol — dyld will bind it. - bind_entries.push((reloc.target.name().to_string(), reloc_vaddr)); - continue; - } - None if is_movw_to_dynamic(reloc, params) => { - // Non-PIC MOVW sequence targeting a dynamic symbol: rewrite - // to ADRP+LDR from GOT (G0→ADRP, G1→LDR, G2/G3→NOP). - rewrite_movw_to_got(state, reloc, reloc_vaddr, params.got)?; - continue; - } - None => { - undefined.insert(reloc.target.name().to_string()); - continue; - } - }; - - // TLV descriptor offset fixup: pointers from __thread_vars into - // __thread_data/__thread_bss must be template-relative offsets, - // not absolute virtual addresses. - let in_tlv_descriptors = sec.kind == SectionKind::TlsVariables; - let target_is_tls_data = match &reloc.target { - SymbolRef::Global(name) => matches!( - state.globals.get(name), - Some(SymbolDef::Defined { section, .. }) - if matches!(state.sections[section.0].kind, SectionKind::Tls | SectionKind::TlsBss) - ), - SymbolRef::Local(oi, name) => { - let def = state.globals.get(name) - .or_else(|| state.locals.get(&(*oi, name.clone()))); - matches!( - def, - Some(SymbolDef::Defined { section, .. }) - if matches!(state.sections[section.0].kind, SectionKind::Tls | SectionKind::TlsBss) - ) - } - }; - let sym_addr = if in_tlv_descriptors && target_is_tls_data { - // Convert absolute vaddr to TLS template offset - let offset = sym_addr - params.tls_template_start; - write_u64(state, reloc.section, reloc.offset, (offset as i64 + reloc.addend) as u64); - // No rebase — this is a template offset, not a pointer - continue; - } else { - sym_addr - }; - - let is_abs = match reloc.r_type { - // AArch64 relocations - RelocType::Aarch64Abs64 | RelocType::Aarch64Abs32 - | RelocType::Aarch64Prel32 - | RelocType::Aarch64Call26 | RelocType::Aarch64Jump26 - | RelocType::Aarch64AdrPrelPgHi21 | RelocType::Aarch64AddAbsLo12Nc - | RelocType::Aarch64Ldst8AbsLo12Nc | RelocType::Aarch64Ldst16AbsLo12Nc - | RelocType::Aarch64Ldst32AbsLo12Nc | RelocType::Aarch64Ldst64AbsLo12Nc - | RelocType::Aarch64Ldst128AbsLo12Nc - | RelocType::Aarch64MovwUabsG0Nc | RelocType::Aarch64MovwUabsG1Nc - | RelocType::Aarch64MovwUabsG2Nc | RelocType::Aarch64MovwUabsG3 - | RelocType::Aarch64AdrGotPage | RelocType::Aarch64Ld64GotLo12Nc - | RelocType::Aarch64GotPcrel32 - | RelocType::Aarch64TlvpLoadPage21 - | RelocType::Aarch64TlvpLoadPageoff12 => { - apply_one_reloc_aarch64(&mut state.sections[reloc.section].data, reloc, sym_addr, reloc_vaddr, params.got)? - } - // x86-64 relocations - RelocType::X86_64 - | RelocType::X86Pc32 - | RelocType::X86Plt32 - | RelocType::X86Gotpcrel - | RelocType::X86Gotpcrelx - | RelocType::X86RexGotpcrelx - | RelocType::X86_32 - | RelocType::X86_32S - | RelocType::X86Tlv => { - apply_one_reloc_x86(&mut state.sections[reloc.section].data, reloc, sym_addr, reloc_vaddr, params.got, &BTreeMap::new())? - } - other => return Err(LinkError::UnsupportedRelocation { - reloc_type: other, symbol: reloc.target.name().to_string(), - }), - }; - if is_abs { - rebase_entries.push(reloc_vaddr); - } - } - - if !undefined.is_empty() { - let mut syms: Vec = undefined.into_iter().collect(); - syms.sort(); - return Err(LinkError::UndefinedSymbols(syms)); - } - - Ok(MachORelocOutput { rebase_entries, bind_entries }) -} - -pub(crate) struct MachORelocOutput { - pub(crate) rebase_entries: Vec, - pub(crate) bind_entries: Vec<(String, u64)>, -} - -/// Check if a relocation is a MOVW type targeting a dynamic symbol with a GOT slot. -fn is_movw_to_dynamic(reloc: &InputReloc, params: &MachORelocParams) -> bool { - matches!(reloc.r_type, - RelocType::Aarch64MovwUabsG0Nc | RelocType::Aarch64MovwUabsG1Nc - | RelocType::Aarch64MovwUabsG2Nc | RelocType::Aarch64MovwUabsG3) - && params.got.contains_key(&reloc.target) -} - -/// Rewrite a MOVW instruction targeting a dynamic symbol to use the GOT slot. -/// G0 (MOVZ) → ADRP xN, got_page -/// G1 (MOVK) → LDR xN, [xN, #got_lo12] -/// G2/G3 → NOP -fn rewrite_movw_to_got( - state: &mut LinkState, - reloc: &InputReloc, - reloc_vaddr: u64, - got: &BTreeMap, -) -> Result<(), LinkError> { - let got_slot = *got.get(&reloc.target).unwrap(); - - // Read the current instruction to extract the destination register - let data = &mut state.sections[reloc.section].data; - let off = reloc.offset as usize; - let insn = u32::from_le_bytes(data[off..off + 4].try_into().unwrap()); - let rd = insn & 0x1F; // destination register in bits [4:0] - - match reloc.r_type { - RelocType::Aarch64MovwUabsG0Nc => { - // MOVZ → ADRP xN, got_page - let adrp = 0x9000_0000 | rd; // ADRP template - write_u32_data(data, reloc.offset, adrp); - let sym_page = got_slot as i64 & !0xFFF; - let pc_page = reloc_vaddr as i64 & !0xFFF; - let page_delta = (sym_page - pc_page) >> 12; - patch_aarch64_adrp_data(data, reloc.offset, page_delta as i32); - } - RelocType::Aarch64MovwUabsG1Nc => { - // MOVK → LDR xN, [xN, #got_lo12] (64-bit load, scale=3) - let ldr = 0xF940_0000 | (rd << 5) | rd; // LDR X template: Rt=Rd, Rn=Rd - write_u32_data(data, reloc.offset, ldr); - let value = (got_slot & 0xFFF) as u32; - patch_aarch64_ldr_imm12_data(data, reloc.offset, value, 3); - } - RelocType::Aarch64MovwUabsG2Nc | RelocType::Aarch64MovwUabsG3 => { - // NOP - write_u32_data(data, reloc.offset, 0xD503_201F); - } - _ => unreachable!(), - } - Ok(()) -} - -pub(crate) fn apply_relocs_pe( - state: &mut LinkState, - layout: &PeLayout, -) -> Result, LinkError> { - let mut undefined = HashSet::new(); - let mut abs_fixups: Vec = Vec::new(); // RVAs of absolute 64-bit fixups - let relocs = std::mem::take(&mut state.relocs); - - for reloc in &relocs { - if matches!(reloc.r_type, - RelocType::X86Tlsgd | RelocType::X86Tlsld | RelocType::X86Dtpoff32 - | RelocType::X86Tpoff32 | RelocType::X86Gottpoff) - { - panic!( - "TLS relocation {} for symbol {} not supported in PE output", - reloc.r_type, reloc.target.name() - ); - } - - let sec = &state.sections[reloc.section]; - let reloc_vaddr = sec.vaddr.unwrap_or_else(|| panic!( - "reloc section {:?} ({:?}) has no vaddr", sec.name, sec.kind, - )) + reloc.offset; - - let sym_addr = match resolve_symbol(state, &reloc.target, None) { - Some(a) => a, - None => { - undefined.insert(reloc.target.name().to_string()); - continue; - } - }; - - let is_abs = apply_one_reloc_x86(&mut state.sections[reloc.section].data, reloc, sym_addr, reloc_vaddr, &layout.got, &BTreeMap::new())?; - if is_abs { - abs_fixups.push(reloc_vaddr as u32); - } - } - - // Fill GOT entries - for &got_vaddr in layout.got.values() { - abs_fixups.push(got_vaddr as u32); - } - - if !undefined.is_empty() { - let mut syms: Vec = undefined.into_iter().collect(); - syms.sort(); - return Err(LinkError::UndefinedSymbols(syms)); - } - - abs_fixups.sort(); - Ok(abs_fixups) -} diff --git a/toyos-ld/tests/alloc_shims.rs b/toyos-ld/tests/alloc_shims.rs deleted file mode 100644 index 40dbe7b9c3e..00000000000 --- a/toyos-ld/tests/alloc_shims.rs +++ /dev/null @@ -1,137 +0,0 @@ -//! The allocator shims rustc normally synthesizes at final link time. -//! -//! A link that has rlibs and no leaf crate has to grow them itself. The names -//! carry a rustc crate disambiguator — a build hash that changes every time -//! `rust/` is rebuilt — and the table used to be nine string literals holding -//! one particular hash, which had already gone stale: it occurred zero times -//! across the thirty rlibs of the live `x86_64-unknown-toyos` sysroot, so the -//! synthesis produced nothing at all. -//! -//! **Every disambiguator here is invented**, which is the point: a test that -//! hardcodes today's hash reproduces the bug it is testing for. - -mod common; - -use common::{Case, ObjBuilder, RET}; -use object::read::{Object, ObjectSection, ObjectSymbol}; -use object::{SymbolKind, SymbolScope}; - -/// `_RNvCs_7___rustc_` — v0 mangling for an item of -/// the compiler-internal `___rustc` crate, written out rather than built by -/// the code under test. -fn rustc_item(disambiguator: &str, name: &str) -> String { - format!("_RNvCs{disambiguator}_7___rustc{}_{name}", name.len()) -} - -/// One object that references `__rust_*` and defines `__rdl_*`, which is the -/// shape of a link over rlibs with no leaf crate. -fn link_referencing(disambiguator: &str, referenced: &[&str], defined: &[&str]) -> Case { - let mut b = ObjBuilder::new(); - for name in defined { - b.text(&rustc_item(disambiguator, name), &[RET], SymbolScope::Dynamic); - } - let targets: Vec<_> = referenced - .iter() - .map(|name| b.undefined(&rustc_item(disambiguator, name), SymbolKind::Text)) - .collect(); - b.caller("_start", &targets, SymbolScope::Dynamic); - Case::new(&format!("shims-{disambiguator}")).input("a.o", b.finish()).arg("-static") -} - -fn defines(bytes: &[u8], name: &str) -> bool { - let obj = object::File::parse(bytes).unwrap(); - obj.symbols().any(|s| s.name() == Ok(name) && s.section_index().is_some()) -} - -/// The five bytes at `name`, which for a synthesized trampoline is -/// `jmp rel32` with the displacement filled in. -fn body(bytes: &[u8], name: &str) -> Vec { - let obj = object::File::parse(bytes).unwrap(); - let sym = obj - .symbols() - .find(|s| s.name() == Ok(name)) - .unwrap_or_else(|| panic!("no symbol {name:?} in the output")); - let section = obj - .sections() - .find(|s| { - let (lo, len) = (s.address(), s.size()); - sym.address() >= lo && sym.address() < lo + len - }) - .unwrap(); - let start = (sym.address() - section.address()) as usize; - section.data().unwrap()[start..start + 5].to_vec() -} - -#[test] -fn every_shim_is_synthesized_under_a_disambiguator_nobody_has_seen() { - for (shim, target) in [ - ("__rust_alloc", "__rdl_alloc"), - ("__rust_dealloc", "__rdl_dealloc"), - ("__rust_realloc", "__rdl_realloc"), - ("__rust_alloc_zeroed", "__rdl_alloc_zeroed"), - ("__rust_alloc_error_handler", "__rdl_alloc_error_handler"), - ] { - let disambiguator = "qQ7zZ0aA9bB1c"; - let out = link_referencing(disambiguator, &[shim], &[target]).link(); - let shim_symbol = rustc_item(disambiguator, shim); - assert!(defines(&out, &shim_symbol), "{shim} got no trampoline"); - let jump = body(&out, &shim_symbol); - assert_eq!(jump[0], 0xE9, "{shim}'s trampoline is not a jmp: {jump:02x?}"); - assert_ne!( - i32::from_le_bytes(jump[1..5].try_into().unwrap()), - 0, - "{shim}'s jmp displacement was never relocated to {target}", - ); - } -} - -/// The one shim that is not a trampoline: rustc emits a bare `ret`. -#[test] -fn the_unstable_marker_is_a_ret() { - let disambiguator = "Zz9Yy8Xx7Ww6V"; - let name = "__rust_no_alloc_shim_is_unstable_v2"; - let out = link_referencing(disambiguator, &[name], &[]).link(); - assert_eq!(body(&out, &rustc_item(disambiguator, name))[0], RET); -} - -/// The bound on the wildcard. The same sysroot carries six `___rustc` symbols -/// that are not allocator shims, and a match that loosened the *name* as well -/// as the hash would hand each of them a jump to a `__rdl_` symbol nothing -/// defines. Each must stay undefined and fail the link by its own name. -#[test] -fn a_rustc_symbol_that_is_not_a_shim_gets_no_trampoline() { - for name in [ - "__rust_start_panic", - "__rust_drop_panic", - "__rust_foreign_exception", - "__rust_panic_cleanup", - "__rust_abort", - "__rust_probestack", - ] { - let disambiguator = "aB3cD4eF5gH6i"; - let said = link_referencing(disambiguator, &[name], &[]).link_expecting_failure(); - assert!( - said.contains(&rustc_item(disambiguator, name)), - "the link failed without naming {name}: {said}", - ); - assert!( - !said.contains("__rdl_"), - "{name} was given a trampoline to a __rdl_ symbol: {said}", - ); - } -} - -/// A symbol shaped like one of the five but belonging to some other crate is -/// not the compiler's, and the `___rustc` path is what says so. -#[test] -fn a_lookalike_outside_the_rustc_crate_gets_no_trampoline() { - let mut b = ObjBuilder::new(); - let name = "_RNvCsqQ7zZ0aA9bB1c_8_notrustc12___rust_alloc"; - let target = b.undefined(name, SymbolKind::Text); - b.caller("_start", &[target], SymbolScope::Dynamic); - let said = Case::new("shims-lookalike") - .input("a.o", b.finish()) - .arg("-static") - .link_expecting_failure(); - assert!(said.contains(name), "the link failed without naming {name}: {said}"); -} diff --git a/toyos-ld/tests/archive_dialects.rs b/toyos-ld/tests/archive_dialects.rs deleted file mode 100644 index 2c51a789dbc..00000000000 --- a/toyos-ld/tests/archive_dialects.rs +++ /dev/null @@ -1,126 +0,0 @@ -//! `ar` has no specification, and the two dialects in circulation disagree -//! about where a member's name is kept. toyos-ld has to read both: the host -//! that builds ToyOS decides which one the C archive in `userland/doom` arrives -//! in, and nothing in the tree names that host. -//! -//! * **GNU/System V** — the name is in the header's 16-byte name field, -//! slash-terminated; a name too long for it goes in a `//` member and the -//! header holds `/`. The symbol table is a first member named `/`. -//! * **BSD/Darwin (cctools)** — `#1/` in the name field means the name is -//! the first `n` bytes of the member *data*, NUL-padded, and `n` is counted -//! in the member's size. The symbol table is a member named `__.SYMDEF` or -//! `__.SYMDEF SORTED`, which is itself long enough to arrive through `#1/`. -//! -//! What makes this worth a test rather than a comment is the failure mode. A -//! reader that takes the header's name field literally does not fault on a BSD -//! archive: it sees `#1/24`, decides that is not an object, drops the member, -//! and links an archive that defines nothing — and the linker then reports one -//! undefined symbol from a container it never said it could not read. Both -//! cases below therefore assert on the *link*, not on a member list. -//! -//! The dialects also differ in ways this file deliberately does not smooth -//! over: BSD pads names and members to 8 bytes where GNU pads to 2, so the -//! member data a BSD reader hands back is the same object under a different -//! byte offset and, for an odd-sized member, a different length. The output -//! must not be able to tell. - -mod common; - -use common::{archive, bsd_archive, Case, ObjBuilder, RET}; -use object::{SymbolKind, SymbolScope}; - -/// Members whose names are too long for a GNU header's 16 bytes — which is the -/// length `cc` gives them (`-.o`) and the length at which the two -/// dialects stop agreeing about anything. -fn members() -> Vec<(&'static str, Vec)> { - let mut out: Vec<(&'static str, Vec)> = Vec::new(); - for (i, name) in ["375a3c652c72663f-doomgeneric.o", "375a3c652c72663f-i_video.o", "short.o"] - .into_iter() - .enumerate() - { - let mut b = ObjBuilder::new(); - b.data(&format!("DATUM_{i}"), &[i as u8; 8], SymbolScope::Linkage); - b.text(&format!("member_fn_{i}"), &[RET], SymbolScope::Linkage); - // An odd-length member: GNU pads it to 2, BSD to 8, so the member after - // it sits at a different offset in the two containers. - b.text(&format!("member_odd_{i}"), &[RET, RET, RET], SymbolScope::Compilation); - out.push((name, b.finish())); - } - out -} - -/// A `_start` that both calls into the archive and takes the address of a datum -/// through the GOT — the second is how a Rust `extern static` reaches a C -/// definition, and it is the reference that reported `DG_ScreenBuffer` when the -/// archive turned out to hold nothing. -fn start_object() -> Vec { - let mut b = ObjBuilder::new(); - let data: Vec<_> = (0..3).map(|i| b.undefined(&format!("DATUM_{i}"), SymbolKind::Data)).collect(); - let calls: Vec<_> = - (0..3).map(|i| b.undefined(&format!("member_fn_{i}"), SymbolKind::Text)).collect(); - b.got_loader("_loads", &data, SymbolScope::Compilation); - b.caller("_start", &calls, SymbolScope::Linkage); - b.finish() -} - -fn linked(lib: Vec, tag: &str) -> Vec { - Case::new(tag).input("start.o", start_object()).input("libm.a", lib).link() -} - -/// The fixture is the claim, so it is checked before anything is concluded -/// from it: a `bsd_archive` that quietly grew a GNU header would make the case -/// below pass while testing nothing. -#[test] -fn the_bsd_fixture_is_bsd() { - let bytes = bsd_archive(&members()); - assert_eq!(&bytes[..8], b"!\n"); - - // The symbol table is the first member and is BSD's, not GNU's. - assert_eq!(&bytes[8..11], b"#1/", "the first member's name is not extended BSD-style"); - let symdef_name_len: usize = - std::str::from_utf8(&bytes[11..24]).unwrap().trim().parse().unwrap(); - let symdef_name = &bytes[68..68 + symdef_name_len]; - assert!( - symdef_name.starts_with(b"__.SYMDEF SORTED\0"), - "first member is {:?}, not a BSD symbol table", - String::from_utf8_lossy(symdef_name), - ); - - // Every member arrives through `#1/`, and none of GNU's own members exists. - let headers = bytes.windows(3).filter(|w| *w == b"#1/").count(); - assert_eq!(headers, members().len() + 1, "not every member carries a BSD extended name"); - assert!( - !bytes.windows(3).any(|w| w == b"//\x20"), - "the fixture carries GNU's long-name member", - ); - - // Names in the member data, not in the header — the whole difference. - for (name, _) in members() { - assert!( - bytes.windows(name.len()).any(|w| w == name.as_bytes()), - "{name} is not in the archive at all", - ); - } -} - -/// The negative control for the reader: with the BSD dialect unread, every -/// member is dropped and this link fails on the symbols the archive defines. -#[test] -fn a_bsd_archive_resolves_its_members() { - let out = linked(bsd_archive(&members()), "bsd"); - assert!(!out.is_empty()); -} - -/// The oracle: the same objects in the two dialects are the same program. This -/// is what says the reader recovered the member *data* and not merely a name — -/// a member handed back with its `#1/` name still on the front, or with BSD's -/// 8-byte tail padding counted as content, parses as an object and produces a -/// different binary. -#[test] -fn the_two_dialects_link_identically() { - assert_eq!( - linked(archive(&members()), "gnu-vs-bsd-gnu"), - linked(bsd_archive(&members()), "gnu-vs-bsd-bsd"), - "the same members in the two ar dialects linked to different binaries", - ); -} diff --git a/toyos-ld/tests/common/mod.rs b/toyos-ld/tests/common/mod.rs deleted file mode 100644 index 7ae5f3288f0..00000000000 --- a/toyos-ld/tests/common/mod.rs +++ /dev/null @@ -1,472 +0,0 @@ -//! Synthetic linker inputs and a run harness, shared by the test files here. -//! -//! One copy: two would drift, and both are asked to build the same shapes of -//! object. - -#![allow(dead_code)] - -use object::write::{Object, Relocation, StandardSection, Symbol, SymbolSection}; -use object::{ - elf, Architecture, BinaryFormat, Endianness, Object as _, ObjectSymbol as _, RelocationFlags, - SymbolFlags, SymbolKind, SymbolScope, -}; -use std::path::{Path, PathBuf}; -use std::process::Command; -use toyos_tmpdir::TempDir; - -// ── Input synthesis ────────────────────────────────────────────────────── - -/// How many samples a determinism case takes. Two suffices for a case whose -/// hazard is wide — each of them is, deliberately, so that a hash order and a -/// sorted order essentially never coincide. Eight is the margin for a narrow -/// case somebody adds later: a hazard of a single stack slot was caught by two -/// runs 39 times in 40 and by eight 40 times. -pub const RUNS: usize = 8; - -/// `mov rax, [rip + disp32]` — the byte sequence Cranelift emits for a GOT load. -pub const MOV_RAX_RIP: [u8; 3] = [0x48, 0x8B, 0x05]; -/// `call rel32` -pub const CALL_REL32: u8 = 0xE8; -pub const RET: u8 = 0xC3; - -pub struct ObjBuilder { - obj: Object<'static>, -} - -impl ObjBuilder { - pub fn new() -> Self { - ObjBuilder { - obj: Object::new(BinaryFormat::Elf, Architecture::X86_64, Endianness::Little), - } - } - - pub fn text(&mut self, name: &str, code: &[u8], scope: SymbolScope) -> object::write::SymbolId { - let section = self.obj.section_id(StandardSection::Text); - let offset = self.obj.append_section_data(section, code, 16); - self.obj.add_symbol(Symbol { - name: name.as_bytes().to_vec(), - value: offset, - size: code.len() as u64, - kind: SymbolKind::Text, - scope, - weak: false, - section: SymbolSection::Section(section), - flags: SymbolFlags::None, - }) - } - - pub fn data(&mut self, name: &str, bytes: &[u8], scope: SymbolScope) -> object::write::SymbolId { - let section = self.obj.section_id(StandardSection::Data); - let offset = self.obj.append_section_data(section, bytes, 8); - self.obj.add_symbol(Symbol { - name: name.as_bytes().to_vec(), - value: offset, - size: bytes.len() as u64, - kind: SymbolKind::Data, - scope, - weak: false, - section: SymbolSection::Section(section), - flags: SymbolFlags::None, - }) - } - - pub fn undefined(&mut self, name: &str, kind: SymbolKind) -> object::write::SymbolId { - self.obj.add_symbol(Symbol { - name: name.as_bytes().to_vec(), - value: 0, - size: 0, - kind, - scope: SymbolScope::Dynamic, - weak: false, - section: SymbolSection::Undefined, - flags: SymbolFlags::None, - }) - } - - /// Append a function that GOT-loads each of `targets`, then returns. The - /// GOT slot order is what `.rela.dyn` is built from. - pub fn got_loader( - &mut self, - name: &str, - targets: &[object::write::SymbolId], - scope: SymbolScope, - ) -> object::write::SymbolId { - let mut code = Vec::new(); - for _ in targets { - code.extend_from_slice(&MOV_RAX_RIP); - code.extend_from_slice(&0i32.to_le_bytes()); - } - code.push(RET); - let sym = self.text(name, &code, scope); - let section = self.obj.section_id(StandardSection::Text); - let base = self.symbol_offset(sym); - for (i, &target) in targets.iter().enumerate() { - self.obj.add_relocation( - section, - Relocation { - offset: base + (i * 7) as u64 + MOV_RAX_RIP.len() as u64, - symbol: target, - addend: -4, - flags: RelocationFlags::Elf { r_type: elf::R_X86_64_REX_GOTPCRELX }, - }, - ) - .unwrap(); - } - sym - } - - /// Append a function that calls each of `targets` through PLT32. - pub fn caller( - &mut self, - name: &str, - targets: &[object::write::SymbolId], - scope: SymbolScope, - ) -> object::write::SymbolId { - let mut code = Vec::new(); - for _ in targets { - code.push(CALL_REL32); - code.extend_from_slice(&0i32.to_le_bytes()); - } - code.push(RET); - let sym = self.text(name, &code, scope); - let section = self.obj.section_id(StandardSection::Text); - let base = self.symbol_offset(sym); - for (i, &target) in targets.iter().enumerate() { - self.obj.add_relocation( - section, - Relocation { - offset: base + (i * 5) as u64 + 1, - symbol: target, - addend: -4, - flags: RelocationFlags::Elf { r_type: elf::R_X86_64_PLT32 }, - }, - ) - .unwrap(); - } - sym - } - - pub fn symbol_offset(&self, id: object::write::SymbolId) -> u64 { - self.obj.symbol(id).value - } - - /// The object being built, for a section the helpers above do not make. - pub fn object(&mut self) -> &mut Object<'static> { - &mut self.obj - } - - pub fn finish(self) -> Vec { - self.obj.write().unwrap() - } -} - -/// A GNU (System V) `ar` archive with the given members. -/// -/// The member's name lives in the header's 16-byte name field, terminated by a -/// slash so it may contain spaces, and the data follows the header directly. -/// A name too long for the field goes in a `//` member instead and the header -/// carries `/`; the symbol table is a first member named -/// `/`, holding big-endian offsets of the member headers. Members are padded to -/// an even byte. -pub fn archive(members: &[(&str, Vec)]) -> Vec { - // GNU's long-name table: every name too long for the header field, each - // terminated by "/\n", and the offset each header will point at. - let mut long_names: Vec = Vec::new(); - let mut name_field: Vec = Vec::new(); - for (name, _) in members { - if name.len() < 16 { - name_field.push(format!("{name}/")); - } else { - name_field.push(format!("/{}", long_names.len())); - long_names.extend_from_slice(name.as_bytes()); - long_names.extend_from_slice(b"/\n"); - } - } - - let mut symbols: Vec<(String, usize)> = Vec::new(); - for (i, (_, data)) in members.iter().enumerate() { - for name in global_definitions(data) { - symbols.push((name, i)); - } - } - - let symtab_len = 4 + 4 * symbols.len() + symbols.iter().map(|(n, _)| n.len() + 1).sum::(); - let mut pos = 8 + 60 + symtab_len + symtab_len % 2; - if !long_names.is_empty() { - pos += 60 + long_names.len() + long_names.len() % 2; - } - let mut member_offsets = Vec::with_capacity(members.len()); - for (_, data) in members { - member_offsets.push(pos as u32); - pos += 60 + data.len() + data.len() % 2; - } - - // The GNU index is in member order and big-endian, where BSD's is sorted - // and little-endian. Neither is read by toyos-ld, which scans the members. - let mut symtab: Vec = (symbols.len() as u32).to_be_bytes().to_vec(); - for (_, member) in &symbols { - symtab.extend_from_slice(&member_offsets[*member].to_be_bytes()); - } - for (name, _) in &symbols { - symtab.extend_from_slice(name.as_bytes()); - symtab.push(0); - } - assert_eq!(symtab.len(), symtab_len); - - let mut out = b"!\n".to_vec(); - push_gnu_member(&mut out, "/", &symtab); - if !long_names.is_empty() { - push_gnu_member(&mut out, "//", &long_names); - } - for (i, ((_, data), field)) in members.iter().zip(&name_field).enumerate() { - assert_eq!( - out.len(), - member_offsets[i] as usize, - "member {i} is not where the symbol table says it is", - ); - push_gnu_member(&mut out, field, data); - } - out -} - -fn push_gnu_member(out: &mut Vec, name_field: &str, data: &[u8]) { - out.extend_from_slice(&member_header(name_field, data.len())); - out.extend_from_slice(data); - if data.len() % 2 == 1 { - out.push(b'\n'); - } -} - -/// Every global a member defines — what an archive's symbol table indexes. -fn global_definitions(data: &[u8]) -> Vec { - let obj = object::File::parse(data).unwrap(); - obj.symbols() - .filter(|s| s.is_global() && !s.is_undefined()) - .filter_map(|s| s.name().ok().filter(|n| !n.is_empty()).map(str::to_string)) - .collect() -} - -/// A BSD (cctools/Darwin) `ar` archive with the same members `archive` takes. -/// -/// The two dialects share `!\n` and the 60-byte member header and agree -/// on nothing else that a reader has to know: -/// -/// * **`#1/`** in the name field means the real name is the first `n` bytes -/// of the *member data*, NUL-padded, and `n` is counted inside the member's -/// size — so both the data pointer and the data length move. A reader that -/// takes the header field as the name sees `#1/24` and a member whose first -/// bytes are its own name. -/// * **`__.SYMDEF SORTED`** (or `__.SYMDEF`) is the symbol table, where GNU -/// writes a member named `/`. It is not an object and carries a name long -/// enough that it arrives through `#1/` as well. -/// * Padding is to 8 bytes, not 2: the name is padded so the data that follows -/// starts 8-aligned, and the member is padded so the next header does. -/// -/// The paddings are LLVM's `printBSDMemberHeader` -/// (`llvm/lib/Object/ArchiveWriter.cpp`): `#1/` carries -/// `name.len() + pad_to_8(pos + 60 + name.len())`. Apple's `ar` pads the name -/// differently — it rounds `60 + name.len()` up to 8 — and both land the data -/// on the same alignment; a reader that stops the name at its first NUL takes -/// either. Measured against `/usr/bin/ar` on macOS 26 (cctools, the same inode -/// as `/usr/bin/libtool` and `/usr/bin/ranlib`): `ar cr m.a f.o` over one -/// 512-byte Mach-O object wrote `#1/20` + `__.SYMDEF SORTED` at offset 8 and -/// `#1/12` + `f.o` at offset 0x70, member data at 0x58 and 0xb8 — both -/// 8-aligned, name lengths 20 and 12 for names of 16 and 3. -/// -/// The symbol table this writes is a real one — `__.SYMDEF SORTED`'s ranlib -/// array over every global the members define, sorted by name, each entry -/// naming the offset of its member's header. toyos-ld does not read it (it -/// scans the members itself), which is exactly why it is written correctly -/// here: a table nothing checks is a fixture that could rot into a shape no -/// archiver produces. -pub fn bsd_archive(members: &[(&str, Vec)]) -> Vec { - // (symbol, index of the member defining it), sorted — `SORTED` is a claim - // about this order and a reader may binary-search on it. - let mut symbols: Vec<(String, usize)> = Vec::new(); - for (i, (_, data)) in members.iter().enumerate() { - for name in global_definitions(data) { - symbols.push((name, i)); - } - } - symbols.sort(); - - // The string table, and each symbol's offset into it. - let mut strtab: Vec = Vec::new(); - let mut str_offsets: Vec = Vec::new(); - for (name, _) in &symbols { - str_offsets.push(strtab.len() as u32); - strtab.extend_from_slice(name.as_bytes()); - strtab.push(0); - } - while !strtab.len().is_multiple_of(8) { - strtab.push(0); - } - - // The table's own size is fixed before any member is placed, so the member - // offsets it has to carry can be computed ahead of writing it. - let ranlib_bytes = symbols.len() * 8; - let symdef_payload = 4 + ranlib_bytes + 4 + strtab.len(); - let symdef_name = b"__.SYMDEF SORTED"; - let symdef_name_len = bsd_name_len(8, symdef_name.len()); - let mut pos = 8 + 60 + symdef_name_len + symdef_payload; - pos += pad_to(pos, 8); - - let mut member_offsets = Vec::with_capacity(members.len()); - for (name, data) in members { - member_offsets.push(pos as u32); - let name_len = bsd_name_len(pos, name.len()); - pos += 60 + name_len + data.len(); - pos += pad_to(pos, 8); - } - - let mut symdef: Vec = Vec::new(); - symdef.extend_from_slice(&(ranlib_bytes as u32).to_le_bytes()); - for (str_off, (_, member)) in str_offsets.iter().zip(&symbols) { - symdef.extend_from_slice(&str_off.to_le_bytes()); - symdef.extend_from_slice(&member_offsets[*member].to_le_bytes()); - } - symdef.extend_from_slice(&(strtab.len() as u32).to_le_bytes()); - symdef.extend_from_slice(&strtab); - assert_eq!(symdef.len(), symdef_payload); - - let mut out = b"!\n".to_vec(); - push_bsd_member(&mut out, symdef_name, &symdef); - for (name, data) in members { - push_bsd_member(&mut out, name.as_bytes(), data); - } - for (i, offset) in member_offsets.iter().enumerate() { - assert_eq!( - &out[*offset as usize..*offset as usize + 3], - b"#1/", - "member {i} is not where __.SYMDEF says it is", - ); - } - out -} - -/// The 60-byte header every `ar` dialect shares: name, mtime, uid, gid, mode, -/// size, and the `` `\n `` terminator that lets a reader tell it found one. -fn member_header(name: &str, size: usize) -> [u8; 60] { - let header = - format!("{:<16}{:<12}{:<6}{:<6}{:<8}{:<10}`\n", name, 0, 0, 0, "100644", size); - header.as_bytes().try_into().expect("an ar member header is 60 bytes") -} - -fn pad_to(pos: usize, align: usize) -> usize { - (align - pos % align) % align -} - -/// `#1/`'s `n`: the name, padded so that the data behind it starts 8-aligned. -fn bsd_name_len(pos: usize, name_len: usize) -> usize { - name_len + pad_to(pos + 60 + name_len, 8) -} - -fn push_bsd_member(out: &mut Vec, name: &[u8], data: &[u8]) { - let name_len = bsd_name_len(out.len(), name.len()); - out.extend_from_slice(&member_header(&format!("#1/{name_len}"), name_len + data.len())); - out.extend_from_slice(name); - out.resize(out.len() + (name_len - name.len()), 0); - assert_eq!(out.len() % 8, 0, "BSD member data must start 8-aligned"); - out.extend_from_slice(data); - let pad = pad_to(out.len(), 8); - out.resize(out.len() + pad, 0); -} - -// ── Harness ────────────────────────────────────────────────────────────── - -pub struct Case { - dir: TempDir, - inputs: Vec, - args: Vec, -} - -impl Case { - pub fn new(name: &str) -> Self { - Case { dir: TempDir::new(&format!("ld-{name}")), inputs: Vec::new(), args: Vec::new() } - } - - /// The directory its inputs and outputs are in, gone with the case. - pub fn dir(&self) -> &Path { - &self.dir - } - - pub fn input(mut self, name: &str, bytes: Vec) -> Self { - let path = self.dir.join(name); - std::fs::write(&path, bytes).unwrap(); - self.inputs.push(path); - self - } - - pub fn arg(mut self, a: &str) -> Self { - self.args.push(a.to_string()); - self - } - - pub fn link_once(&self, out: &Path) { - let status = Command::new(env!("CARGO_BIN_EXE_toyos-ld")) - .args(&self.args) - .arg("-o") - .arg(out) - .args(&self.inputs) - .output() - .unwrap(); - assert!( - status.status.success(), - "link failed: {}", - String::from_utf8_lossy(&status.stderr) - ); - } - - /// Link once and return the output bytes. - pub fn link(&self) -> Vec { - let out = self.dir.join("out"); - self.link_once(&out); - std::fs::read(&out).unwrap() - } - - /// Link once, expecting it to fail, and return what the linker said. - pub fn link_expecting_failure(&self) -> String { - let out = self.dir.join("out"); - let result = Command::new(env!("CARGO_BIN_EXE_toyos-ld")) - .args(&self.args) - .arg("-o") - .arg(&out) - .args(&self.inputs) - .output() - .unwrap(); - assert!(!result.status.success(), "expected the link to fail, and it succeeded"); - String::from_utf8_lossy(&result.stderr).into_owned() - } - - /// Link `RUNS` times and return the index of the first run whose output - /// differs from run 0, with the number of differing bytes. - pub fn diff(&self) -> Option<(usize, usize)> { - let mut first: Option> = None; - for run in 0..RUNS { - let out = self.dir.join(format!("out.{run}")); - self.link_once(&out); - let bytes = std::fs::read(&out).unwrap(); - match &first { - None => first = Some(bytes), - Some(f) => { - if *f != bytes { - let differing = if f.len() == bytes.len() { - f.iter().zip(&bytes).filter(|(a, b)| a != b).count() - } else { - usize::MAX - }; - return Some((run, differing)); - } - } - } - } - None - } - - pub fn assert_identical(&self, what: &str) { - if let Some((run, bytes)) = self.diff() { - panic!("{what}: run {run} of {RUNS} differs from run 0 in {bytes} bytes"); - } - } -} - diff --git a/toyos-ld/tests/determinism.rs b/toyos-ld/tests/determinism.rs deleted file mode 100644 index a47a59508dd..00000000000 --- a/toyos-ld/tests/determinism.rs +++ /dev/null @@ -1,181 +0,0 @@ -//! The same linker binary, given byte-identical inputs, must emit a -//! byte-identical output. -//! -//! Every case drives the `toyos-ld` binary rather than the library: a build is -//! one process per output, and `RandomState` seeds each map from a per-process -//! key, so two links inside one process are not the two links a build performs. - -mod common; - -use common::{archive, Case, ObjBuilder, RET}; -use object::{SymbolKind, SymbolScope}; -use std::process::Command; - -/// A synthetic translation unit with `n` of each kind of symbol, dense enough -/// that a hash order and a sorted order almost never coincide. -fn wide_object(n: usize, tag: &str) -> Vec { - let mut b = ObjBuilder::new(); - for i in 0..n { - b.text(&format!("{tag}_local_fn_{i}"), &[RET], SymbolScope::Compilation); - b.data(&format!("{tag}_local_obj_{i}"), &[i as u8; 8], SymbolScope::Compilation); - } - let mut globals = Vec::new(); - for i in 0..n { - globals.push(b.data(&format!("{tag}_global_obj_{i}"), &[i as u8; 8], SymbolScope::Linkage)); - b.text(&format!("{tag}_global_fn_{i}"), &[RET], SymbolScope::Linkage); - } - b.got_loader(&format!("{tag}_loads"), &globals, SymbolScope::Linkage); - b.finish() -} - -fn start_object(calls: &[&str]) -> Vec { - let mut b = ObjBuilder::new(); - let targets: Vec<_> = calls.iter().map(|n| b.undefined(n, SymbolKind::Text)).collect(); - b.caller("_start", &targets, SymbolScope::Linkage); - b.finish() -} - -// ── Cases ──────────────────────────────────────────────────────────────── - -/// `.symtab`/`.strtab`: `LinkState::locals` and `::globals` are walked to build -/// the symbol entries, and the string table follows the entry order. -#[test] -fn symtab_order() { - Case::new("symtab") - .input("wide.o", wide_object(200, "w")) - .input("start.o", start_object(&["w_loads"])) - .assert_identical("symtab/strtab"); -} - -/// `.rela.dyn` RELATIVE entries: `ElfLayout::got` is walked to fill the GOT. -#[test] -fn rela_dyn_relatives() { - let mut b = ObjBuilder::new(); - let targets: Vec<_> = - (0..200).map(|i| b.data(&format!("g{i}"), &[i as u8; 8], SymbolScope::Linkage)).collect(); - b.got_loader("_start", &targets, SymbolScope::Linkage); - Case::new("relatives").input("got.o", b.finish()).assert_identical(".rela.dyn RELATIVE"); -} - -/// `.rela.dyn` GLOB_DAT and the `.dynsym`/`.dynstr` that name them: -/// `ElfLayout::dyn_got` is walked, and the import strings follow it. -#[test] -fn rela_dyn_glob_dat() { - let dir = toyos_tmpdir::TempDir::new("ld-solib"); - - let mut provider = ObjBuilder::new(); - for i in 0..150 { - provider.data(&format!("imp{i}"), &[i as u8; 8], SymbolScope::Dynamic); - } - let provider_path = dir.join("provider.o"); - std::fs::write(&provider_path, provider.finish()).unwrap(); - let so = dir.join("libprov.so"); - let out = Command::new(env!("CARGO_BIN_EXE_toyos-ld")) - .arg("--shared") - .arg("-o") - .arg(&so) - .arg(&provider_path) - .output() - .unwrap(); - assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr)); - - let mut b = ObjBuilder::new(); - let targets: Vec<_> = - (0..150).map(|i| b.undefined(&format!("imp{i}"), SymbolKind::Data)).collect(); - b.got_loader("_start", &targets, SymbolScope::Linkage); - - Case::new("globdat") - .input("main.o", b.finish()) - .input("libprov.so", std::fs::read(&so).unwrap()) - .assert_identical(".rela.dyn GLOB_DAT"); -} - -/// A shared-library output: `.dynsym` exports, `.gnu.hash` buckets and the -/// symbol table all come off the same two maps. -#[test] -fn shared_output() { - Case::new("shared") - .arg("--shared") - .input("wide.o", wide_object(150, "s")) - .assert_identical("shared library"); -} - -/// The PE writer emits the UEFI bootloader, and holds a `got` of the same -/// shape as the ELF one. -#[test] -fn pe_output() { - let mut b = ObjBuilder::new(); - let targets: Vec<_> = - (0..150).map(|i| b.data(&format!("p{i}"), &[i as u8; 8], SymbolScope::Linkage)).collect(); - b.got_loader("efi_main", &targets, SymbolScope::Linkage); - for i in 0..150 { - b.text(&format!("p_local_{i}"), &[RET], SymbolScope::Compilation); - } - Case::new("pe") - .arg("--pe") - .arg("-e") - .arg("efi_main") - .input("uefi.o", b.finish()) - .assert_identical("PE"); -} - -/// A static output: the kernel's shape. The GOT is filled in place rather than -/// through `.rela.dyn`, so this is the writer's own ordering. -#[test] -fn static_output() { - Case::new("static") - .arg("--static") - .input("wide.o", wide_object(150, "t")) - .input("start.o", start_object(&["t_loads"])) - .assert_identical("static executable"); -} - -/// Archive member selection: the pull-in worklist is seeded from the undefined -/// set, so its order decides which members a symbol with two definitions pulls -/// in — and therefore which sections exist at all. -#[test] -fn archive_member_selection() { - let mut members: Vec<(&'static str, Vec)> = Vec::new(); - let names: Vec = (0..40).map(|i| format!("m{i}.o")).collect(); - for (i, name) in names.iter().enumerate() { - let mut b = ObjBuilder::new(); - // Every member defines the same helper as well as its own symbol, so - // which member the worklist reaches first is observable. - b.text("shared_helper", &[RET], SymbolScope::Linkage); - b.text(&format!("member_fn_{i}"), &[RET], SymbolScope::Linkage); - let dep = b.undefined(&format!("member_fn_{}", (i + 1) % 40), SymbolKind::Text); - b.caller(&format!("member_entry_{i}"), &[dep], SymbolScope::Linkage); - members.push((Box::leak(name.clone().into_boxed_str()), b.finish())); - } - let wanted: Vec = (0..40).map(|i| format!("member_entry_{i}")).collect(); - let refs: Vec<&str> = wanted.iter().map(|s| s.as_str()).collect(); - - Case::new("archive") - .input("start.o", start_object(&refs)) - .input("libm.a", archive(&members)) - .assert_identical("archive member selection"); -} - -/// Everything at once, which is the shape of a real link. -#[test] -fn broad() { - let mut members: Vec<(&'static str, Vec)> = Vec::new(); - let names: Vec = (0..20).map(|i| format!("a{i}.o")).collect(); - for (i, name) in names.iter().enumerate() { - let mut b = ObjBuilder::new(); - for j in 0..10 { - b.text(&format!("arch_local_{i}_{j}"), &[RET], SymbolScope::Compilation); - } - b.text(&format!("arch_fn_{i}"), &[RET], SymbolScope::Linkage); - members.push((Box::leak(name.clone().into_boxed_str()), b.finish())); - } - let wanted: Vec = (0..20).map(|i| format!("arch_fn_{i}")).collect(); - let mut refs: Vec<&str> = wanted.iter().map(|s| s.as_str()).collect(); - refs.push("b_loads"); - - Case::new("broad") - .input("wide.o", wide_object(300, "b")) - .input("start.o", start_object(&refs)) - .input("liba.a", archive(&members)) - .assert_identical("broad"); -} diff --git a/toyos-ld/tests/output_mode.rs b/toyos-ld/tests/output_mode.rs deleted file mode 100644 index a3091b5ad2b..00000000000 --- a/toyos-ld/tests/output_mode.rs +++ /dev/null @@ -1,22 +0,0 @@ -//! What the linker writes is a program, and a program nothing may exec is not -//! one. Every caller in this repository feeds the output to a loader that does -//! not read the mode, so a consumer linking by hand is who finds out. - -mod common; - -use common::{Case, ObjBuilder, RET}; -use object::SymbolScope; -use std::os::unix::fs::PermissionsExt as _; - -#[test] -fn a_linked_program_is_executable() { - let mut b = ObjBuilder::new(); - b.text("_start", &[RET], SymbolScope::Dynamic); - let case = Case::new("mode").input("a.o", b.finish()).arg("-static"); - - let out = case.dir().join("a.out"); - case.link_once(&out); - - let mode = std::fs::metadata(&out).unwrap().permissions().mode() & 0o777; - assert_eq!(mode, 0o755, "{} was written 0{mode:o}", out.display()); -} diff --git a/toyos-ld/tests/tls.rs b/toyos-ld/tests/tls.rs deleted file mode 100644 index 0c5d9592360..00000000000 --- a/toyos-ld/tests/tls.rs +++ /dev/null @@ -1,55 +0,0 @@ -//! toyos-ld is frozen with a TLS layout the loader no longer shares: it -//! resolves an executable's local-exec accesses against the block's unrounded -//! size, and the loader ends that block at its size rounded to `p_align`. An -//! executable with thread-local storage is refused rather than linked wrong; a -//! shared object's accesses go through the loader and are linked as before. - -mod common; - -use common::{ObjBuilder, RET}; -use object::write::{Symbol, SymbolSection}; -use object::{SymbolFlags, SymbolKind, SymbolScope}; - -fn with_tls(entry: bool) -> Vec { - let mut b = ObjBuilder::new(); - if entry { - b.text("_start", &[RET], SymbolScope::Dynamic); - } - let obj = b.object(); - let tdata = obj.section_id(object::write::StandardSection::Tls); - // Five bytes, so the block's size is not its 64-byte alignment's multiple. - let offset = obj.append_section_data(tdata, &[1, 2, 3, 4, 5], 64); - obj.add_symbol(Symbol { - name: b"counter".to_vec(), - value: offset, - size: 5, - kind: SymbolKind::Tls, - scope: SymbolScope::Dynamic, - weak: false, - section: SymbolSection::Section(tdata), - flags: SymbolFlags::None, - }); - b.finish() -} - -#[test] -fn an_executable_with_thread_local_storage_is_refused() { - let refused = toyos_ld::link_full(&[("a.o".to_string(), with_tls(true))], "_start", false, false); - assert!( - matches!(refused, Err(toyos_ld::LinkError::TlsExecutable)), - "linked: {:?}", - refused.map(|out| out.len()) - ); -} - -#[test] -fn an_executable_without_it_still_links() { - let mut b = ObjBuilder::new(); - b.text("_start", &[RET], SymbolScope::Dynamic); - toyos_ld::link_full(&[("a.o".to_string(), b.finish())], "_start", false, false).expect("no TLS, linked"); -} - -#[test] -fn a_shared_object_with_thread_local_storage_still_links() { - toyos_ld::link_shared(&[("a.o".to_string(), with_tls(false))]).expect("a library's TLS, linked"); -} diff --git a/toyos-symbols/Cargo.toml b/toyos-symbols/Cargo.toml index fd4e89f45ca..5f6627dce48 100644 --- a/toyos-symbols/Cargo.toml +++ b/toyos-symbols/Cargo.toml @@ -22,4 +22,5 @@ toyos-elf = { path = "../toyos-elf" } toyos-elide = { path = "../toyos-elide" } [dev-dependencies] -object = { version = "0.38", default-features = false, features = ["read_core", "elf", "std"] } +# unaligned: an include_bytes! fixture is aligned to 1, and object reads a header in place. +object = { version = "0.38", default-features = false, features = ["read_core", "elf", "std", "unaligned"] }