Expand description
Build the linker invocation for a hot-patch dylib by editing
the captured fat-build linker call (see I4g-X1
whisker-linker-shim) as little as possible.
§Why we don’t construct linker args from scratch
cargo + rustc + the platform’s clang/gcc driver assemble a
large, fragile argv: sysroot, target triple, -arch, NDK
search paths, framework directories, OS-version-min flags,
-Wl,… directives, sometimes a custom -fuse-ld=…. These
shift across:
- macOS major versions (sysroot path,
-platform_version) - Xcode releases (
-isysroot, framework dirs) - Android NDK r24/r25/r26 (CRT layout, libc++.a, libunwind.a)
- glibc CSU layout (crt1.o vs Scrt1.o, libc_nonshared.a)
- rustc’s choice of linker driver (cc, clang, lld)
Re-deriving any of these is a long-tail of papercuts. So:
capture the fat-build linker invocation verbatim (X1) and edit
only the parts a hot-patch must change. Same principle as
build_obj_plan does for rustc.
§What we change
- Drop object/archive inputs (
.o,.rlib,.a,.so,.dylib). The fat build linked the entire workspace; the patch only needs the freshly-rebuilt object. - Drop
-o <path>and the existing-shared(we re-add both). - Drop
-undefined <action>(the separated macOS form) so we can deterministically setdynamic_lookup. - Drop
--version-script=<path>and--no-undefined-version. The fat build’s version-script enumerates thousands of Rust-mangled symbols (rustc auto-generates one for bothdylibandcdylib). Re-applying it to a patch dylib that only defines the one changed function makes the linker emitversion script assignment ... failed: symbol not definedfor every absent symbol — fatal under--no-undefined-version. The patch dylib’s default visibility (everything global) is the right behaviour:subsecond::apply_patchreads the patch’s.dynsymlooking for the changed function’s mangled name. - Append:
-shared- OS-specific “unresolved is fine” directive:
- macOS:
-Wl,-undefined,dynamic_lookup - Linux/Android:
-Wl,--unresolved-symbols=ignore-all
- macOS:
- any caller-supplied extra objects (typically the
stub.oproduced bycrate::hotpatch::create_undefined_symbol_stub). The stub defines every host symbol the patch refers to as a tiny ARM64 trampoline branching to that symbol’s runtime address, computed from the device’s reportedsubsecond::aslr_reference(). Linking it in this slot means the patch has noDT_NEEDEDback-edge to the host, and no dlopen-time symbol resolution to perform — which avoids the Android linker-namespace +RTLD_LOCALcorner cases the previous “back-edge to host dylib” scheme tripped over. - the new object path
-o <output>
Everything else — -isysroot, -arch, -target, -L,
-l, -rpath, -Wl,…, -F, -framework, -fuse-ld=…,
-mmacosx-version-min=… — is preserved verbatim.
The “unresolved is fine” directive is the load-bearing trick
that makes hot-patch dylibs small and fast: every symbol the
patch references but doesn’t define (e.g. an unmodified
whisker::println) is left as an undefined-symbol marker, and
subsecond::apply_patch resolves it against the original
binary’s symbol table at swap-in time. So the patch dylib
holds only the changed function bodies, not their entire
transitive call graph.
Structs§
- Link
Plan - Result of
build_link_plan.
Enums§
- Linker
Os - Which OS-specific “unresolved-is-fine” directive to emit. Android uses the same lld defaults as Linux for our purposes.
Functions§
- build_
link_ plan - Re-shape a captured linker invocation into the link step of a hot-patch build. See module docs for the rationale.
- linker_
os_ for_ host - Pick the
LinkerOsthat matches the host we’re building on.cfg!-based — at runtime we know which compiled-in branch ran. For cross-target hot-patch (e.g. macOS host → Android device), callers should pass the target OS explicitly rather than rely on this convenience.