split_info's Places::sym placed a symbol with no section of its own at its stub as Symbol::addr finds it, but looked the stubs up again, a lazily loaded dylib's call helper and a delay-init stub before the plain stub, the other way around from Symbol::addr; got_slot looked up the GOT and __lazy_load_got slots as Symbol::got_addr does. Both resolve through Symbol now: stub_entry, the chunk and address of the stub, call helper, delay-init stub or selector stub that stands in for a symbol, which addr returns the address of, and got_slot, the chunk and address of the slot, which got_addr returns the address of. The orders differ only for a dylib symbol with both a __stubs entry and one of the others, which a lazily loaded dylib's never has (the link refuses it) and a delay-init dylib's has only where the linker makes the stub itself (_dlopen's, _objc_msgSend's for small selector stubs, an entry point's); there the split info now records the stub the calls go to. Co-Authored-By: Claude Opus 5.5 <[email protected]>
mold-macho: mold for macOS
mold-macho is a Mach-O linker for macOS, written in Rust as a port of
the mold linker to Apple platforms.
It is a drop-in replacement for Apple's ld for common workloads: it
accepts ld64's command line, and links working, correctly code-signed
executables, dylibs and bundles for arm64 and x86-64 macOS.
C, C++, Objective-C, Swift and Rust programs link and run, including real-world projects (ripgrep builds and passes its full test suite; the C++ mold tree with vendored tbb/zstd/zlib builds at -O3; the linker links itself). Debugging works (stabs for lldb/dsymutil), and so do C++ exceptions, thread-locals, LTO, auto-linking, frameworks and Objective-C selector stubs.
Usage
Build with cargo build --release. The binary is target/release/mold;
./install.sh (PREFIX=/usr/local by default) installs it as
mold with an ld64.mold symlink, the same arrangement as mold's
ld.mold. Then point your compiler driver at the linker:
clang -o hello hello.c --ld-path=path/to/ld64.mold
swiftc -o hello hello.swift -use-ld=path/to/ld64.mold
RUSTFLAGS="-C link-arg=--ld-path=path/to/ld64.mold" cargo build
Feature highlights
- Classic dyld info and chained fixups (the default for deployment targets of macOS 13+), ad-hoc code signing, export tries, and content-hash UUIDs
- Subsections via symbols,
-dead_strip, identical code folding (on by default, like ld64's deduplication), literal merging __unwind_infosynthesis from compact unwind,__eh_framere-synthesis for DWARF-only unwind, range-extension thunks- Archives with mold's order-insensitive resolution model, .tbd stubs with umbrella reexports, frameworks, auto-linking (LC_LINKER_OPTION), LTO via libLTO
- Parallel input parsing, output writing and code-signature hashing, following the design described in the mold paper
Architecture
The code mirrors mold's layout: macho/driver.rs runs the passes in
order, macho/passes.rs implements them, macho/arch/ isolates the
target-dependent relocation handling (arm64 and x86-64 are each
instantiated in a crate under arch/ and dispatched by the
executable in cli/), and macho/chunks/ builds every piece of
the output file. Parsing is decoupled from resolution: all inputs,
including every archive member, are parsed in parallel, and symbol
resolution ranks competing definitions with a liveness walk deciding
which archive members join the link.
The commit history doubles as a reference on the Mach-O file format and dyld: each commit's message explains the structures and loader behavior involved in that change.
Tests
Tests are shell scripts under tests/, one feature per script,
driving the real toolchain through cc --ld-path=...; a harness runs
each for macOS on arm64 and (under Rosetta) x86-64, and then for the
arm64 iOS simulator if its runtime is installed, building the programs
with the simulator's SDK and running them on a simulator device it
boots. Logs and outputs go under target/<profile>/mold-test/out/test.
--all adds the x86-64 iOS simulator (which only the iOS 17 runtime
runs) and the tvOS and visionOS simulators; --host runs macOS's
alone, and --triple (or TRIPLE) one simulator. The harness is the
integration test of the cli crate; name that target to pass it
options, which the unit tests would otherwise reject:
cargo test
cargo test --test integration -- dead-strip --native --timeout 120
cargo test --test integration -- --all
cargo test --test integration -- --triple arm64-apple-tvos-simulator
cargo test --test integration -- --list
License
MIT, like mold.