Preserve LTO symbols only for references from native code

A bitcode function that only other bitcode calls need not survive the
internalizer: libLTO resolves that call itself. ld-prime lets such a
function go local: linking a bitcode main that calls a bitcode
times2(), it exports _main and not _times2. So in an executable only
references from non-bitcode objects, the entry point, -u names and the
export list pin a symbol defined in bitcode.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Chris AllenandClaude Opus 5.5 committed 2026-09-23 11:58:40 -05:00
1 parent b0d6f41094
commit 97f4c07863
2 files changed
+24 -3

No files matched your search

+19 -3
View File
@@ -987,10 +987,26 @@ pub fn do_lto<E: Target>(ctx: &mut Context<E>) -> bool {
// code references (plus the entry point, and everything under
// -export_dynamic, which exists exactly to let executables
// keep their globals for dlsym) must survive; the rest can be
// internalized and dead-stripped inside the module.
// internalized and dead-stripped inside the module. A reference
// from another bitcode module does not count: libLTO resolves
// those itself, and ld-prime lets such a function go local
// (_times2, called only from a bitcode main, is not exported).
let executable = ctx.args.output_type == MH_EXECUTE;
let mut native_refs: hashbrown::HashSet<crate::symbol::SymbolId> =
hashbrown::HashSet::new();
for obj in &ctx.objs {
if !obj.is_alive || obj.lto_module.is_some() {
continue;
}
let r = obj.global_range();
for (nlist, &sym_id) in obj.nlists[r.clone()].iter().zip(&obj.symbols[r]) {
if !nlist.is_stab() && nlist.n_type() == N_UNDF && !nlist.is_common() {
native_refs.insert(sym_id);
}
}
}
let mut preserve: Vec<std::ffi::CString> = Vec::new();
for sym in &ctx.symbols.syms {
for (i, sym) in ctx.symbols.syms.iter().enumerate() {
if let Some(FileId::Obj(idx)) = sym.file()
&& ctx.objs[idx as usize].is_alive
&& ctx.objs[idx as usize].lto_module.is_some()
@@ -998,7 +1014,7 @@ pub fn do_lto<E: Target>(ctx: &mut Context<E>) -> bool {
{
if executable
&& !ctx.args.export_dynamic
&& !sym.is_used()
&& (!sym.is_used() || !native_refs.contains(&(i as u32)))
&& sym.name() != ctx.args.entry
&& !ctx.args.forced_undefined.iter().any(|n| n == sym.name())
&& !ctx
+5
View File
@@ -15,6 +15,11 @@ EOF2
$CC -flto --ld-path=$mold -o $t/exe $t/a.o $t/b.o
$t/exe | grep '^42$'
# A function only other bitcode calls is not kept external: libLTO
# resolves that call itself, and ld-prime exports only _main.
dyld_info -exports $t/exe > $t/exports
grep -q _main $t/exports
not grep -q _times2 $t/exports
# Mixed bitcode and Mach-O, with bitcode in an archive
cat <<EOF2 | $CC -flto -o $t/c.o -c -xc -