[lld] [lld][MachO] Implement -bind_at_load (PR #224788)
via llvm-commits
llvm-commits at lists.llvm.org
Fri Sep 18 19:18:39 PDT 2026
https://github.com/b1scoito created https://github.com/llvm/llvm-project/pull/224788
Implements ld64's `-bind_at_load`: bind every symbol eagerly at load time rather than lazily on first use. lld previously parsed the flag, warned "not yet implemented", and ignored it.
**Change (classic, non-chained path):** the two lazy cases in `addBindingsForStub` — non-weak dylib symbols and interposable defined symbols — emit non-lazy bind opcodes targeting the stub's `__la_symbol_ptr` slot instead of entries in the lazy bind table. Consequences in the output:
- `__stub_helper` is omitted entirely (`dyld_stub_binder` is never referenced);
- `LC_DYLD_INFO`'s lazy bind table is empty;
- the eager bind table gains one bind per stub symbol, matching what ld64 emits for the same inputs.
With chained fixups the flag stays a no-op (chained fixups have no laziness), same as ld64.
**Motivation:** images that are *not* loaded by dyld cannot tolerate lazy binding — a manually mapped image whose lazy pointers call `dyld_stub_binder` crashes on the first cross-image call, because dyld has no registration for the image. This is the load-bearing flag for one such injection toolchain (a JVM agent mapped into a running Minecraft client), which is also the live test case.
**Test:** `lld/test/MachO/bind-at-load.s` — links with and without the flag, asserts `__stub_helper`'s absence, the absence of lazy bind opcodes, and the eager bind for the symbol.
Local verification: `lld/test/MachO` run before/after shows an identical failure set (12 environment-related failures on this host both with and without the change); the new test passes.
>From 01a4f6679bbf3d11ad34b33c8612dfd0af01db50 Mon Sep 17 00:00:00 2001
From: biscoito <biscoito at riseup.net>
Date: Fri, 18 Sep 2026 23:18:10 -0300
Subject: [PATCH] [lld][MachO] Implement -bind_at_load
ld64's -bind_at_load binds every symbol eagerly at load time instead
of lazily on first use. lld parsed the flag but only warned that it was
not yet implemented, and otherwise ignored it.
In the classic (non-chained) link, route the two lazy cases through the
non-lazy bind opcodes at the stub's __la_symbol_ptr slot: non-weak
dylib symbols and interposable defined symbols. __stub_helper and
dyld_stub_binder then disappear from the output and the LC_DYLD_INFO
lazy bind table is empty, matching what ld64 emits for the same inputs.
This matters for images that are not loaded by dyld: a manually mapped
image whose lazy pointers call dyld_stub_binder crashes on the first
cross-image call, because dyld has no registration for the image (a
real-world injection toolchain hit exactly this on macOS).
The flag also leaves the hidden unimplemented-option warning path, so
it is now documented in --help like its sibling -fixup_chains.
---
lld/MachO/Config.h | 1 +
lld/MachO/Driver.cpp | 1 +
lld/MachO/Options.td | 1 -
lld/MachO/SyntheticSections.cpp | 13 +++++++---
lld/test/MachO/bind-at-load.s | 44 +++++++++++++++++++++++++++++++++
5 files changed, 55 insertions(+), 5 deletions(-)
create mode 100644 lld/test/MachO/bind-at-load.s
diff --git a/lld/MachO/Config.h b/lld/MachO/Config.h
index a939d7789a758f..a0e4e9f9b94a61 100644
--- a/lld/MachO/Config.h
+++ b/lld/MachO/Config.h
@@ -137,6 +137,7 @@ struct Configuration {
bool emitEncryptionInfo = false;
bool emitInitOffsets = false;
bool emitChainedFixups = false;
+ bool bindAtLoad = false;
bool emitRelativeMethodLists = false;
bool thinLTOEmitImportsFiles;
bool thinLTOEmitIndexFiles;
diff --git a/lld/MachO/Driver.cpp b/lld/MachO/Driver.cpp
index 5603dcba5a38a5..c63443176acca1 100644
--- a/lld/MachO/Driver.cpp
+++ b/lld/MachO/Driver.cpp
@@ -2028,6 +2028,7 @@ bool link(ArrayRef<const char *> argsArr, llvm::raw_ostream &stdoutOS,
config->emitDataInCodeInfo =
args.hasFlag(OPT_data_in_code_info, OPT_no_data_in_code_info, true);
config->emitChainedFixups = shouldEmitChainedFixups(args);
+ config->bindAtLoad = args.hasArg(OPT_bind_at_load);
config->emitInitOffsets =
config->emitChainedFixups || args.hasArg(OPT_init_offsets);
config->emitRelativeMethodLists = shouldEmitRelativeMethodLists(args);
diff --git a/lld/MachO/Options.td b/lld/MachO/Options.td
index 32b6ce0a64a07d..2124ac06166488 100644
--- a/lld/MachO/Options.td
+++ b/lld/MachO/Options.td
@@ -937,7 +937,6 @@ def headerpad_max_install_names : Flag<["-"], "headerpad_max_install_names">,
Group<grp_rare>;
def bind_at_load : Flag<["-"], "bind_at_load">,
HelpText<"Tell dyld to bind all symbols at load time, rather than lazily">,
- Flags<[HelpHidden]>,
Group<grp_rare>;
def force_flat_namespace : Flag<["-"], "force_flat_namespace">,
HelpText<"Tell dyld to use a flat namespace on this executable and all its dependent dylibs & bundles">,
diff --git a/lld/MachO/SyntheticSections.cpp b/lld/MachO/SyntheticSections.cpp
index 1f388b329aedf4..2b9727fb862c82 100644
--- a/lld/MachO/SyntheticSections.cpp
+++ b/lld/MachO/SyntheticSections.cpp
@@ -731,11 +731,12 @@ void StubsSection::finalize() { isFinal = true; }
static void addBindingsForStub(Symbol *sym) {
assert(!config->emitChainedFixups);
if (auto *dysym = dyn_cast<DylibSymbol>(sym)) {
- if (sym->isWeakDef()) {
+ if (sym->isWeakDef() || config->bindAtLoad) {
in.binding->addEntry(dysym, in.lazyPointers->isec,
sym->stubsIndex * target->wordSize);
- in.weakBinding->addEntry(sym, in.lazyPointers->isec,
- sym->stubsIndex * target->wordSize);
+ if (sym->isWeakDef())
+ in.weakBinding->addEntry(sym, in.lazyPointers->isec,
+ sym->stubsIndex * target->wordSize);
} else {
in.lazyBinding->addEntry(dysym);
}
@@ -746,7 +747,11 @@ static void addBindingsForStub(Symbol *sym) {
in.weakBinding->addEntry(sym, in.lazyPointers->isec,
sym->stubsIndex * target->wordSize);
} else if (defined->interposable) {
- in.lazyBinding->addEntry(sym);
+ if (config->bindAtLoad)
+ in.binding->addEntry(sym, in.lazyPointers->isec,
+ sym->stubsIndex * target->wordSize);
+ else
+ in.lazyBinding->addEntry(sym);
} else {
llvm_unreachable("invalid stub target");
}
diff --git a/lld/test/MachO/bind-at-load.s b/lld/test/MachO/bind-at-load.s
new file mode 100644
index 00000000000000..1a78345bedea55
--- /dev/null
+++ b/lld/test/MachO/bind-at-load.s
@@ -0,0 +1,44 @@
+# REQUIRES: x86
+# RUN: rm -rf %t; split-file %s %t
+# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %t/dylib.s -o %t/dylib.o
+# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %t/main.s -o %t/main.o
+# RUN: %lld -lSystem -dylib %t/dylib.o -o %t/libfoo.dylib
+# RUN: %lld -e _main -lSystem %t/main.o %t/libfoo.dylib -o %t/default
+# RUN: %lld -e _main -lSystem -bind_at_load %t/main.o %t/libfoo.dylib -o %t/bound
+
+## By default, the stub's pointer is lazily bound: __stub_helper exists and
+## _foo appears in the lazy bind opcodes.
+# RUN: llvm-objdump --macho --section-headers %t/default | FileCheck %s --check-prefix=DEFAULT
+# RUN: obj2yaml %t/default | FileCheck %s --check-prefix=DEFAULT-LAZY
+
+## With -bind_at_load, every bind is eager: no __stub_helper, no
+## dyld_stub_binder, no lazy bind opcodes, and _foo is bound in the
+## non-lazy bind opcodes at the (formerly lazy) pointer's location.
+# RUN: llvm-objdump --macho --section-headers %t/bound | FileCheck %s --check-prefix=BIND-SECTIONS
+# RUN: obj2yaml %t/bound | FileCheck %s --check-prefix=BIND-BIND
+
+# DEFAULT: __stub_helper
+
+# DEFAULT-LAZY: LazyBindOpcodes:
+# DEFAULT-LAZY: Symbol: _foo
+
+# BIND-SECTIONS: __stubs
+# BIND-SECTIONS-NOT: __stub_helper
+# BIND-SECTIONS: __la_symbol_ptr
+
+# BIND-BIND: BindOpcodes:
+# BIND-BIND: Symbol: _foo
+# BIND-BIND-NOT: LazyBindOpcodes
+
+#--- dylib.s
+.section __TEXT,__text,regular,pure_instructions
+.globl _foo
+_foo:
+ ret
+
+#--- main.s
+.section __TEXT,__text,regular,pure_instructions
+.globl _main
+_main:
+ callq _foo
+ ret
More information about the llvm-commits
mailing list