Do not demote non-exported globals to local symbols in .symtab

We computed the binding of a symbol in the static symbol table from its
dynamic import/export status, so in an ordinary executable, every global
symbol was emitted as STB_LOCAL. That misleads all symbol table
consumers; nm prints "t main" instead of "T main", and gdb had to work
around it for a feature that depends on symbol bindings.

Only symbols hidden by visibility or localized by a version script
should be demoted. GNU ld keeps hidden symbols STB_GLOBAL with hidden
visibility while lld converts them to STB_LOCAL; we follow lld, which
also matches our previous output for such symbols.

One wrinkle: claim_unresolved_symbols() assigned version index 0
(VER_NDX_LOCAL) to imported symbols so that their .gnu.version entries
would be 0, which would make the new is_local() misclassify them as
version-script-localized. Leave ver_idx unassigned for imported symbols
and have the .gnu.version writer emit 0 for unversioned undefined
symbols directly, so that VER_NDX_LOCAL on a symbol means only
"localized by a version script".

Fixes https://github.com/rui314/mold/issues/906

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
Rui UeyamaandClaude Fable 5 committed 2026-07-26 14:43:12 +09:00
1 parent fba0a2185d
commit 35f4a334a0
4 files changed
+45 -5

No files matched your search

+6 -1
View File
@@ -3618,11 +3618,16 @@ inline bool Symbol<E>::is_absolute() const {
!get_output_section();
}
// Returns true if the symbol should be emitted as a local symbol in the
// output symbol table. Note that a symbol that is merely not exported to
// the dynamic symbol table is still a global symbol; only ones hidden by
// symbol visibility or localized by a version script are demoted.
template <typename E>
inline bool Symbol<E>::is_local(Context<E> &ctx) const {
if (ctx.arg.relocatable)
return esym().st_bind == STB_LOCAL;
return !is_imported && !is_exported;
return visibility == STV_HIDDEN || visibility == STV_INTERNAL ||
ver_idx == VER_NDX_LOCAL;
}
template <typename E>
+10 -3
View File
@@ -2953,7 +2953,7 @@ void VerdefSection<E>::construct(Context<E> &ctx) {
// Handle --default-symver
if (ctx.arg.default_symver)
for (Symbol<E> *sym : ctx.dynsym->symbols)
if (sym && !sym->file->is_dso)
if (sym && !sym->file->is_dso && !sym->esym().is_undef())
if (u16 ver = sym->ver_idx;
ver == VER_NDX_GLOBAL || ver == VER_NDX_UNSPECIFIED)
sym->ver_idx = VER_NDX_LAST_RESERVED + 1;
@@ -2962,9 +2962,16 @@ void VerdefSection<E>::construct(Context<E> &ctx) {
ctx.versym->contents.resize(ctx.dynsym->symbols.size(), VER_NDX_GLOBAL);
ctx.versym->contents[0] = VER_NDX_LOCAL;
for (Symbol<E> *sym : ctx.dynsym->symbols)
if (sym && !sym->file->is_dso && sym->ver_idx != VER_NDX_UNSPECIFIED)
for (Symbol<E> *sym : ctx.dynsym->symbols) {
if (!sym || sym->file->is_dso)
continue;
// An unversioned undefined symbol takes version index 0.
if (sym->ver_idx != VER_NDX_UNSPECIFIED)
ctx.versym->contents[sym->get_dynsym_idx(ctx)] = sym->ver_idx;
else if (sym->esym().is_undef())
ctx.versym->contents[sym->get_dynsym_idx(ctx)] = VER_NDX_LOCAL;
}
// Allocate a buffer for .gnu.version_d and write to it
contents.resize((sizeof(ElfVerdef<E>) + sizeof(ElfVerdaux<E>)) *
+1 -1
View File
@@ -1809,7 +1809,7 @@ void claim_unresolved_symbols(Context<E> &ctx) {
sym.is_weak = false;
sym.is_imported = is_imported;
sym.is_exported = false;
sym.ver_idx = is_imported ? 0 : ctx.default_version;
sym.ver_idx = is_imported ? VER_NDX_UNSPECIFIED : ctx.default_version;
};
if (esym.is_undef_weak()) {
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
. $(dirname $0)/common.inc
# A symbol that is merely not exported to the dynamic symbol table must
# remain global in .symtab. https://github.com/rui314/mold/issues/906
cat <<EOF | $CC -fPIC -c -o $t/a.o -xc -
__attribute__((visibility("hidden"))) void hidden_fn() {}
__attribute__((weak)) void weak_fn() {}
void global_fn() {}
int main() {}
EOF
$CC -B. -o $t/exe1 $t/a.o
readelf -sW $t/exe1 > $t/log1
grep -E 'FUNC GLOBAL DEFAULT .* main$' $t/log1
grep -E 'FUNC GLOBAL DEFAULT .* global_fn$' $t/log1
grep -E 'FUNC WEAK DEFAULT .* weak_fn$' $t/log1
grep -E 'FUNC LOCAL HIDDEN .* hidden_fn$' $t/log1
# A symbol localized by a version script becomes local.
echo '{ global: global_fn; local: *; };' > $t/b.ver
$CC -B. -shared -o $t/c.so $t/a.o -Wl,--version-script=$t/b.ver
readelf -sW $t/c.so > $t/log2
grep -E 'FUNC GLOBAL DEFAULT .* global_fn$' $t/log2
grep -E 'FUNC LOCAL DEFAULT .* weak_fn$' $t/log2