Report duplicate symbols as ld-prime does, and accept -allow_dead_duplicates

ld-prime reports a symbol live objects define strongly more than once
as "duplicate symbol '<name>' in:" followed by the real path of each
file defining it (an archive member as "lib.a[N](member.o)"), then
fails with "<N> duplicate symbols". Under -dead_strip it lists only the
symbols whose kept definition is live but counts them all, and
-allow_dead_duplicates lets one stay whose other definitions are dead.
-duplicate_symbols warning|error is read and has nothing to act on.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Rui UeyamaandClaude Opus 5.5 committed 2026-10-01 16:20:19 +09:00
1 parent fdb4247cd8
commit a9c2e988e2
4 files changed
+113 -36

No files matched your search

+13 -1
View File
@@ -425,6 +425,9 @@ pub struct Args {
/// -keep_duplicate / -keep_duplicates_list: the functions (local
/// ones too) function deduplication leaves alone.
pub keep_duplicates: Glob,
/// -allow_dead_duplicates: a symbol defined more than once is fine
/// if -dead_strip removes all its definitions but the one kept.
pub allow_dead_duplicates: bool,
/// -w: suppress warnings.
pub suppress_warnings: bool,
pub fatal_warnings: bool,
@@ -722,6 +725,7 @@ impl Default for Args {
force_weak: Glob::new(),
force_not_weak: Glob::new(),
keep_duplicates: Glob::new(),
allow_dead_duplicates: false,
suppress_warnings: false,
fatal_warnings: false,
demangle: false,
@@ -1572,7 +1576,9 @@ pub(crate) fn missing_argument(opt: &str) -> String {
"-force_symbols_not_weak_list" => {
return "-force_symbols_weak_list missing <path>".to_string();
}
"-read_only_relocs" | "-arch_variant_lto_cache_mismatch" => "missing <option>",
"-read_only_relocs" | "-arch_variant_lto_cache_mismatch" | "-duplicate_symbols" => {
"missing <option>"
}
"-target" => "missing <target-triple>",
"-alias" => "missing <real-name> <alias-name>",
"-dylib_file" => "missing <path:path>",
@@ -2407,6 +2413,12 @@ pub fn parse_args(target: &TargetTraits, cmdline: &[Cow<'_, OsStr>]) -> Args {
keep_duplicates.add(pat.as_bytes(), 0);
}
}
b"-allow_dead_duplicates" => args.allow_dead_duplicates = true,
// For duplicate symbols ld-prime would only warn of, which
// it has no more: it takes the treatment and does nothing.
b"-duplicate_symbols" => {
parse_treatment(name, next_arg(&mut i, name), false);
}
b"-dyld_env" => {
let arg = next_arg(&mut i, name).as_bytes();
+52 -33
View File
@@ -2831,16 +2831,18 @@ pub fn coalesce_weak_defs<E: Target>(ctx: &mut Context<E>) {
}
}
/// Reports two live strong definitions of one name. Resolution keeps
/// the first strong definition it meets; a strong definition in any
/// other live object that lost to it is an error (a weak or common one
/// yields quietly). Reported after resolution settles, sorted by name,
/// so the messages are deterministic: mold's
/// check_duplicate_symbols. As in ld-prime, a final link reports them
/// only if no symbol is undefined, and only of the symbols -dead_strip
/// leaves live.
/// Reports the symbols live objects define strongly more than once, as
/// ld-prime does once resolution settles (and, in a final link, no
/// symbol is undefined): each with the files that define it, then their
/// number. Resolution keeps the first strong definition it meets; a weak
/// or common one yields quietly. Under -dead_strip only a symbol whose
/// kept definition is live is reported, and -allow_dead_duplicates lets
/// one stay whose other definitions are all dead; ld-prime counts the
/// symbols it doesn't report in the number all the same. It lists them
/// in no stable order; mold sorts them by name.
pub fn check_duplicate_symbols<E: Target>(ctx: &Context<E>) {
let mut duplicates: Vec<(crate::symbol::SymbolId, usize)> = ctx
// Each losing definition, and whether it is live.
let mut losers: Vec<(SymbolId, usize, bool)> = ctx
.objs
.par_iter()
.enumerate()
@@ -2852,37 +2854,54 @@ pub fn check_duplicate_symbols<E: Target>(ctx: &Context<E>) {
|| !nlist.is_extern()
|| !matches!(nlist.n_type(), N_SECT | N_ABS)
|| nlist.n_desc & N_WEAK_DEF != 0
|| !matches!(ctx.symbols[sym_id].file(), Some(FileId::Obj(owner)) if owner as usize != obj_idx)
{
return None;
}
let sym = &ctx.symbols[sym_id];
match sym.file() {
Some(FileId::Obj(owner))
if owner as usize != obj_idx
&& sym
.input_section()
.is_none_or(|i| ctx.isecs[i as usize].is_alive()) =>
{
Some((sym_id, obj_idx))
}
let isec = match nlist.n_type() {
N_SECT => input_files::find_symbol_subsec(
&ctx.isecs,
&obj.subsecs,
nlist.n_sect,
nlist.n_value,
),
_ => None,
}
};
let live = isec.is_none_or(|(isec, _)| ctx.isecs[isec].is_alive());
Some((sym_id, obj_idx, live))
})
})
.collect();
duplicates.sort_by_key(|&(sym_id, obj_idx)| (ctx.symbols[sym_id].name(), obj_idx));
duplicates.dedup();
for (sym_id, obj_idx) in duplicates {
let prev = match ctx.symbols[sym_id].file() {
Some(FileId::Obj(idx)) => file_display(&ctx.objs[idx as usize]),
_ => "?".into(),
};
error!(
"duplicate symbol: {}: {}: {}",
file_display(&ctx.objs[obj_idx]),
prev,
ctx.symbols[sym_id]
);
if losers.is_empty() {
return;
}
losers.sort_by_key(|&(sym_id, obj_idx, _)| (ctx.symbols[sym_id].name(), obj_idx));
losers.dedup();
let mut count = 0;
let mut reported = false;
for group in losers.chunk_by(|a, b| a.0 == b.0) {
let id = group[0].0;
let sym = &ctx.symbols[id];
if ctx.args.allow_dead_duplicates && group.iter().all(|&(_, _, live)| !live) {
continue;
}
count += 1;
if !sym.input_section().is_none_or(|isec| ctx.isecs[isec as usize].is_alive()) {
continue;
}
reported = true;
let Some(FileId::Obj(winner)) = sym.file() else { continue };
let mut files: Vec<usize> = group.iter().map(|&(_, obj, _)| obj).collect();
files.push(winner as usize);
files.sort_unstable();
crate::error::notice(format_args!("duplicate symbol '{sym}' in:"));
for obj in files {
crate::error::notice(format_args!(" {}", resolved_file_name(ctx.objs[obj].mf)));
}
}
if reported {
error!("{count} duplicate symbols");
}
}
+38
View File
@@ -0,0 +1,38 @@
#!/bin/bash
source "$(dirname "$0")"/common.inc
# Under -dead_strip ld-prime reports only the duplicate symbols whose
# kept definition is live, though it counts all, and
# -allow_dead_duplicates lets one stay whose other definitions are all
# dead.
cat <<EOF | $CC -o $t/a.o -c -xc -
int dup1() { return 1; }
int dup2() { return 2; }
int main() { return dup1(); }
EOF
cat <<EOF | $CC -o $t/b.o -c -xc -
int dup1() { return 3; }
int dup2() { return 4; }
EOF
not $CC --ld-path=$mold -o $t/exe $t/a.o $t/b.o -Wl,-dead_strip 2> $t/log
grep "duplicate symbol '_dup1' in:" $t/log
not grep -q "duplicate symbol '_dup2'" $t/log
grep -q '2 duplicate symbols' $t/log
$CC --ld-path=$mold -o $t/exe $t/a.o $t/b.o -Wl,-dead_strip -Wl,-allow_dead_duplicates
nm $t/exe | grep -q _dup1
# Without -dead_strip nothing is dead.
not $CC --ld-path=$mold -o $t/exe $t/a.o $t/b.o -Wl,-allow_dead_duplicates 2> $t/log
grep -q '2 duplicate symbols' $t/log
# -duplicate_symbols is taken, and changes nothing.
not $CC --ld-path=$mold -o $t/exe $t/a.o $t/b.o -Wl,-dead_strip \
-Wl,-duplicate_symbols,warning 2> $t/log
grep -q '2 duplicate symbols' $t/log
not $mold -o $t/exe $t/a.o -duplicate_symbols suppress 2> $t/log
grep -q -- '-duplicate_symbols invalid option (warning | error)' $t/log
not $mold -o $t/exe $t/a.o -duplicate_symbols 2> $t/log
grep -q -- '-duplicate_symbols missing <option>' $t/log
+10 -2
View File
@@ -11,7 +11,15 @@ int main() {}
EOF
! $CC --ld-path=$mold -o $t/exe $t/a.o $t/b.o 2> $t/log || false
grep -q 'duplicate symbol: .*/b.o: .*/a.o: _hello' $t/log
# (ld-prime lists the files in no stable order.)
grep -v '^+' $t/log | sed -E 's|^(ld: \|mold: error: )||; s|/.*/||' > $t/msgs
cat > $t/expected <<EOF
duplicate symbol '_hello' in:
a.o
b.o
1 duplicate symbols
EOF
grep -v 'linker command failed' $t/msgs | sort | diff - <(sort $t/expected)
# A relocatable link must fail before publishing an erroneous object.
# Exercise both the default parent/child protocol and the debugger mode.
@@ -22,7 +30,7 @@ for no_fork in no yes; do
else
! $mold -r -arch $ARCH -o $t/merged.o $t/a.o $t/b.o 2> $t/log || false
fi
grep -q 'duplicate symbol: .*_hello' $t/log
grep -q "duplicate symbol '_hello' in:" $t/log
test ! -e $t/merged.o
done