[lld] 7049627 - [ELF] --retain-symbols-file: keep listed symbols in .symtab, not .dynsym (#209063)
via llvm-commits
llvm-commits at lists.llvm.org
Tue Jul 14 00:08:34 PDT 2026
Author: Fangrui Song
Date: 2026-07-14T07:08:29Z
New Revision: 70496276d0f441999b61e84ca22e59d61fa43582
URL: https://github.com/llvm/llvm-project/commit/70496276d0f441999b61e84ca22e59d61fa43582
DIFF: https://github.com/llvm/llvm-project/commit/70496276d0f441999b61e84ca22e59d61fa43582.diff
LOG: [ELF] --retain-symbols-file: keep listed symbols in .symtab, not .dynsym (#209063)
Commit c0fc25307143dd06c12fb2f5ebdb1dade7290ccf (2017) implements
--retain-symbols-file with symbol version machinery
`{local: *; global: listed;}`, which removes unlisted symbols from
.dynsym and leaves .symtab untouched. GNU ld does the opposite: it keeps
only the listed symbols in .symtab and does not touch .dynsym.
Reuse the --discard-{locals,all} mechanism to reimplement the feature,
matching GNU ld.
Use --export-dynamic-symbol or a version script to control .dynsym.
Extend `markUsedSymbols` to mark referenced globals as well, so
--emit-relocs/-r keep a valid symbol index.
Fix #91055
Added:
Modified:
lld/ELF/Config.h
lld/ELF/Driver.cpp
lld/ELF/MarkLive.cpp
lld/ELF/SymbolTable.cpp
lld/ELF/Writer.cpp
lld/docs/ReleaseNotes.rst
lld/docs/ld.lld.1
lld/test/ELF/retain-symbols-file.s
lld/test/ELF/version-script-warn.s
Removed:
################################################################################
diff --git a/lld/ELF/Config.h b/lld/ELF/Config.h
index 12b16ded61fca..863f3223094bb 100644
--- a/lld/ELF/Config.h
+++ b/lld/ELF/Config.h
@@ -299,6 +299,7 @@ struct Config {
llvm::StringRef thinLTOPrefixReplaceNativeObject;
std::string rpath;
llvm::SmallVector<VersionDefinition, 0> versionDefinitions;
+ std::optional<llvm::DenseSet<llvm::StringRef>> retainSymbols;
llvm::SmallVector<llvm::StringRef, 0> auxiliaryList;
llvm::SmallVector<llvm::StringRef, 0> filterList;
llvm::SmallVector<llvm::StringRef, 0> passPlugins;
diff --git a/lld/ELF/Driver.cpp b/lld/ELF/Driver.cpp
index 7ec7dfcae6bca..77821e8c813ef 100644
--- a/lld/ELF/Driver.cpp
+++ b/lld/ELF/Driver.cpp
@@ -1961,15 +1961,12 @@ static void readConfigs(Ctx &ctx, opt::InputArgList &args) {
ctx.arg.versionDefinitions.push_back(
{"global", (uint16_t)VER_NDX_GLOBAL, {}, {}});
- // If --retain-symbol-file is used, we'll keep only the symbols listed in
- // the file and discard all others.
+ // Keep only these symbols in .symtab (not .dynsym), matching GNU ld.
if (auto *arg = args.getLastArg(OPT_retain_symbols_file)) {
- ctx.arg.versionDefinitions[VER_NDX_LOCAL].nonLocalPatterns.push_back(
- {"*", /*isExternCpp=*/false, /*hasWildcard=*/true});
+ ctx.arg.retainSymbols.emplace();
if (std::optional<MemoryBufferRef> buffer = readFile(ctx, arg->getValue()))
for (StringRef s : args::getLines(*buffer))
- ctx.arg.versionDefinitions[VER_NDX_GLOBAL].nonLocalPatterns.push_back(
- {s, /*isExternCpp=*/false, /*hasWildcard=*/false});
+ ctx.arg.retainSymbols->insert(s);
}
for (opt::Arg *arg : args.filtered(OPT_warn_backrefs_exclude)) {
diff --git a/lld/ELF/MarkLive.cpp b/lld/ELF/MarkLive.cpp
index d1e05b5eb9296..2820818133d5c 100644
--- a/lld/ELF/MarkLive.cpp
+++ b/lld/ELF/MarkLive.cpp
@@ -339,11 +339,13 @@ void MarkLive<ELFT, TrackWhyLive>::markSymbol(Symbol *sym, StringRef reason) {
enqueue(isec, d->value, sym, {std::nullopt, reason});
}
-template <class ELFT> static void markUsedLocalSymbols(InputSectionBase &sec) {
+// If -r or --emit-relocs, mark symbols referenced by relocations as used so
+// .symtab retains them and the relocations keep valid symbol indices. Callers
+// invoke this only when .symtab filtering is active (--discard-* or
+// --retain-symbols-file); otherwise .symtab keeps every symbol anyway.
+template <class ELFT> static void markUsedSymbols(InputSectionBase &sec) {
auto mark = [&](const auto &rel) {
- Symbol &sym = sec.file->getRelocTargetSym(rel);
- if (sym.isLocal())
- sym.setFlags(USED);
+ sec.file->getRelocTargetSym(rel).setFlags(USED);
};
const RelsOrRelas<ELFT> rels = sec.template relsOrRelas<ELFT>();
for (const typename ELFT::Rel &rel : rels.rels)
@@ -385,7 +387,10 @@ void MarkLive<ELFT, TrackWhyLive>::run() {
// referenced by .eh_frame sections, so we scan them for that here.
for (EhInputSection *eh : ctx.ehInputSections)
scanEhFrameSection(*eh);
- bool markUsed = ctx.arg.copyRelocs && ctx.arg.discard != DiscardPolicy::None;
+ // See markUsedSymbols.
+ bool markUsed =
+ ctx.arg.copyRelocs &&
+ (ctx.arg.discard != DiscardPolicy::None || ctx.arg.retainSymbols);
for (InputSectionBase *sec : ctx.inputSections) {
if (sec->flags & SHF_GNU_RETAIN) {
enqueue(sec, /*offset=*/0, /*sym=*/nullptr, {std::nullopt, "retained"});
@@ -421,10 +426,8 @@ void MarkLive<ELFT, TrackWhyLive>::run() {
sec->markLive();
for (InputSection *isec : sec->dependentSections)
isec->markLive();
- // If -r or --emit-relocs, ensure referenced local symbols are
- // preserved by --discard-{locals,all} (see shouldKeepInSymtab).
if (markUsed)
- markUsedLocalSymbols<ELFT>(*sec);
+ markUsedSymbols<ELFT>(*sec);
}
}
@@ -602,14 +605,13 @@ template <class ELFT> void elf::markLive(Ctx &ctx) {
if (auto *s = dyn_cast<SharedSymbol>(sym))
if (s->isUsedInRegularObj && !s->isWeak())
cast<SharedFile>(s->file)->isNeeded = true;
- // If -r or --emit-relocs, ensure referenced local symbols are preserved so
- // that they won't be discarded by --discard-{locals,all} (see
- // shouldKeepInSymtab).
- if (ctx.arg.copyRelocs && ctx.arg.discard != DiscardPolicy::None)
+ // See markUsedSymbols.
+ if (ctx.arg.copyRelocs &&
+ (ctx.arg.discard != DiscardPolicy::None || ctx.arg.retainSymbols))
parallelForEach(ctx.objectFiles, [](ELFFileBase *file) {
for (InputSectionBase *sec : file->getSections())
if (sec)
- markUsedLocalSymbols<ELFT>(*sec);
+ markUsedSymbols<ELFT>(*sec);
});
return;
}
diff --git a/lld/ELF/SymbolTable.cpp b/lld/ELF/SymbolTable.cpp
index a76f5d4434a0d..c5ea3ee04ff4c 100644
--- a/lld/ELF/SymbolTable.cpp
+++ b/lld/ELF/SymbolTable.cpp
@@ -312,15 +312,7 @@ void SymbolTable::scanVersionScript() {
bool asteriskReported = false;
auto assignAsterisk = [&](SymbolVersion &pat, VersionDefinition *ver,
bool isLocal) {
- // Avoid issuing a warning if both '--retain-symbol-file' and a version
- // script with `global: *` are used.
- //
- // '--retain-symbol-file' adds a "*" pattern to
- // 'versionDefinitions[VER_NDX_LOCAL].nonLocalPatterns', see
- // 'readConfigs()' in 'Driver.cpp'. Note that it is not '.localPatterns',
- // and may seem counterintuitive, but still works as expected. Here we can
- // exploit that and skip analyzing the pattern added for this option.
- if (!asteriskReported && (isLocal || ver->id > VER_NDX_LOCAL)) {
+ if (!asteriskReported) {
if ((isLocal && globalAsteriskFound) ||
(!isLocal && localAsteriskFound)) {
Warn(ctx)
diff --git a/lld/ELF/Writer.cpp b/lld/ELF/Writer.cpp
index 2da732c857be9..635bb47defb83 100644
--- a/lld/ELF/Writer.cpp
+++ b/lld/ELF/Writer.cpp
@@ -382,6 +382,12 @@ template <class ELFT> void Writer<ELFT>::run() {
}
}
+static bool retainKeepsInSymtab(Ctx &ctx, const Symbol &sym) {
+ if (sym.hasFlag(USED) && ctx.arg.copyRelocs)
+ return true;
+ return ctx.arg.retainSymbols->contains(sym.getName());
+}
+
static bool shouldKeepInSymtab(Ctx &ctx, const Defined &sym) {
if (sym.isSection())
return false;
@@ -414,6 +420,10 @@ static bool shouldKeepInSymtab(Ctx &ctx, const Defined &sym) {
(ctx.arg.discard == DiscardPolicy::Locals ||
(sym.section && (sym.section->flags & SHF_MERGE))))
return false;
+ // If --retain-symbols-file= is specified, keep in .symtab only listed symbols
+ // plus those referenced by emitted relocations.
+ if (LLVM_UNLIKELY(ctx.arg.retainSymbols))
+ return retainKeepsInSymtab(ctx, sym);
return true;
}
@@ -1941,7 +1951,8 @@ template <class ELFT> void Writer<ELFT>::finalizeSections() {
continue;
if (!ctx.arg.relocatable)
sym->binding = sym->computeBinding(ctx);
- if (ctx.in.symTab)
+ if (ctx.in.symTab &&
+ (!ctx.arg.retainSymbols || retainKeepsInSymtab(ctx, *sym)))
ctx.in.symTab->addSymbol(sym);
// computeBinding might localize a symbol that was considered exported
diff --git a/lld/docs/ReleaseNotes.rst b/lld/docs/ReleaseNotes.rst
index 9f3366c0d98b8..36f50db965f04 100644
--- a/lld/docs/ReleaseNotes.rst
+++ b/lld/docs/ReleaseNotes.rst
@@ -76,6 +76,9 @@ ELF Improvements
``.debug_info``), fixing DWARF corruption in the output.
(`#209035 <https://github.com/llvm/llvm-project/pull/209035>`_)
(`#209042 <https://github.com/llvm/llvm-project/pull/209042>`_)
+* ``--retain-symbols-file`` now filters ``.symtab`` instead of ``.dynsym``,
+ matching GNU ld.
+ (`#209063 <https://github.com/llvm/llvm-project/pull/209063>`_)
* ``INCLUDE`` in linker scripts now fully parses its own content instead of
sharing a lexer buffer stack with the includer, fixing spurious acceptance
of malformed scripts.
diff --git a/lld/docs/ld.lld.1 b/lld/docs/ld.lld.1
index 70541cf786f64..7dedf69881b3f 100644
--- a/lld/docs/ld.lld.1
+++ b/lld/docs/ld.lld.1
@@ -582,7 +582,10 @@ version.txt containing the output of ld.lld --version.
The archive when
unpacked can be used to re-run the linker with the same options and input files.
.It Fl -retain-symbols-file Ns = Ns Ar file
-Retain only the symbols listed in the file.
+Keep only the symbols listed in
+.Ar file
+in the static symbol table, along with symbols referenced by emitted relocations.
+This does not affect the dynamic symbol table.
.It Fl -rpath Ns = Ns Ar value , Fl R Ar value
Add a
.Dv DT_RUNPATH
diff --git a/lld/test/ELF/retain-symbols-file.s b/lld/test/ELF/retain-symbols-file.s
index 2af0792ec6106..8a9bc9b40a69b 100644
--- a/lld/test/ELF/retain-symbols-file.s
+++ b/lld/test/ELF/retain-symbols-file.s
@@ -1,9 +1,11 @@
# REQUIRES: x86
-## --retain-symbols-file removes unlisted symbols from .dynsym
+## --retain-symbols-file filters .symtab, not .dynsym, matching GNU ld.
# RUN: rm -rf %t && split-file %s %t && cd %t
# RUN: llvm-mc -filetype=obj -triple=x86_64 a.s -o a.o
+## --gc-sections marks referenced symbols used; unlisted ones are still dropped
+## from .symtab, testing that the used flag does not override the filter.
# RUN: ld.lld -shared --gc-sections --retain-symbols-file=retain a.o -o sym.so
# RUN: llvm-readelf --dyn-syms -s sym.so | FileCheck %s --check-prefix=SYM
@@ -11,30 +13,43 @@
# RUN: ld.lld -shared --gc-sections --retain-symbols-file retain a.o -o sym2.so
# RUN: cmp sym.so sym2.so
-## .dynsym keeps only the listed retain1 and retain2, plus the undefined und.
-# SYM: Symbol table '.dynsym' contains 4 entries:
+## .dynsym keeps every exported symbol, including unlisted ones.
+# SYM: Symbol table '.dynsym' contains 7 entries:
# SYM: GLOBAL DEFAULT UND und
+# SYM-NEXT: GLOBAL DEFAULT {{.*}} _start
+# SYM-NEXT: GLOBAL DEFAULT {{.*}} used
# SYM-NEXT: GLOBAL DEFAULT {{.*}} retain1
# SYM-NEXT: GLOBAL DEFAULT {{.*}} retain2
-## .symtab is unaffected.
-# SYM: Symbol table '.symtab' contains 9 entries:
-
-## --emit-relocs keeps symbols referenced by emitted relocations.
-# RUN: ld.lld -shared --emit-relocs --retain-symbols-file=retain a.o -o emit-dn.so
-# RUN: llvm-readelf -rs emit-dn.so | FileCheck %s --check-prefix=EMIT
-# EMIT: R_X86_64_PLT32 {{.*}} used - 4
-# EMIT-NEXT: R_X86_64_PLT32 {{.*}} und - 4
-# EMIT: Symbol table '.symtab' contains 12 entries:
-# EMIT: [[#]] local
+# SYM-NEXT: GLOBAL DEFAULT {{.*}} nonalloc_referenced
+## .symtab keeps only the listed retain1 and retain2.
+# SYM: Symbol table '.symtab' contains 3 entries:
+# SYM: GLOBAL DEFAULT {{.*}} retain1
+# SYM-NEXT: GLOBAL DEFAULT {{.*}} retain2
-## --discard-locals behaves like the default here.
+## --emit-relocs additionally keeps symbols referenced by emitted relocations.
+## --discard-locals/--discard-all match the default.
+# RUN: ld.lld -shared --emit-relocs --retain-symbols-file=retain a.o -o emit.so
+# RUN: llvm-readelf -rs emit.so | FileCheck %s --check-prefix=EMIT
# RUN: ld.lld -shared --emit-relocs --discard-locals --retain-symbols-file=retain a.o -o emit-dl.so
# RUN: llvm-readelf -rs emit-dl.so | FileCheck %s --check-prefix=EMIT
-
# RUN: ld.lld -shared --emit-relocs --discard-all --retain-symbols-file=retain a.o -o emit-da.so
-# RUN: llvm-readelf -rs emit-da.so | FileCheck %s --check-prefix=EMIT-DA
-# EMIT-DA: Symbol table '.symtab' contains 11 entries:
-# EMIT-DA-NOT: local
+# RUN: llvm-readelf -rs emit-da.so | FileCheck %s --check-prefix=EMIT
+# EMIT: R_X86_64_PLT32 {{.*}} used - 4
+# EMIT-NEXT: R_X86_64_PLT32 {{.*}} und - 4
+# EMIT: Symbol table '.symtab' contains 9 entries:
+# EMIT: GLOBAL DEFAULT {{.*}} used
+# EMIT-NEXT: GLOBAL DEFAULT UND und
+# EMIT-NEXT: GLOBAL DEFAULT {{.*}} retain1
+# EMIT-NEXT: GLOBAL DEFAULT {{.*}} retain2
+# EMIT-NEXT: GLOBAL DEFAULT {{.*}} nonalloc_referenced
+
+## --discard-none additionally keeps the unlisted `local`.
+# RUN: ld.lld -shared --emit-relocs --discard-none --retain-symbols-file=retain a.o -o emit-dn.so
+# RUN: llvm-readelf -rs emit-dn.so | FileCheck %s --check-prefix=EMIT-DN
+# EMIT-DN: R_X86_64_PLT32 {{.*}} used - 4
+# EMIT-DN-NEXT: R_X86_64_PLT32 {{.*}} und - 4
+# EMIT-DN: Symbol table '.symtab' contains 10 entries:
+# EMIT-DN: LOCAL DEFAULT {{.*}} local
## nonalloc_referenced is referenced only by the non-alloc .nonalloc section, and
## the emitted relocation keeps a valid symbol index.
@@ -42,12 +57,12 @@
# RUN: llvm-readelf -r na.so | FileCheck %s --check-prefix=NA
# NA: R_X86_64_64 {{.*}} nonalloc_referenced + 0
-## An empty file localizes every defined symbol out of .dynsym.
+## An empty file drops every symbol from .symtab; .dynsym is unaffected.
# RUN: ld.lld -shared --retain-symbols-file=/dev/null a.o -o empty.so
# RUN: llvm-readelf --dyn-syms -s empty.so | FileCheck %s --check-prefix=EMPTY
-# EMPTY: Symbol table '.dynsym' contains 2 entries:
+# EMPTY: Symbol table '.dynsym' contains 7 entries:
# EMPTY: GLOBAL DEFAULT UND und
-# EMPTY: Symbol table '.symtab' contains 9 entries:
+# EMPTY: Symbol table '.symtab' contains 1 entries:
#--- retain
retain1
diff --git a/lld/test/ELF/version-script-warn.s b/lld/test/ELF/version-script-warn.s
index 9aba596165796..13bb734b2887b 100644
--- a/lld/test/ELF/version-script-warn.s
+++ b/lld/test/ELF/version-script-warn.s
@@ -20,8 +20,7 @@
# RUN: echo 'V1 { local: *; }; V2 { local: *;};' > %t.ver
# RUN: ld.lld --version-script %t.ver %t.o -shared -o %t.so --fatal-warnings
-## --retain-symbols-file uses the same internal infrastructure as the support
-## for version scripts. Do not show the warings if they both are used.
+## --retain-symbols-file with a version-script '*' must not warn.
# RUN: echo 'foo' > %t_retain.txt
# RUN: echo '{ local: *; };' > %t_local.ver
# RUN: echo '{ global: *; };' > %t_global.ver
More information about the llvm-commits
mailing list