[lld] [ELF] Synthesize STT_FILE if necessary (PR #209087)

Fangrui Song via llvm-commits llvm-commits at lists.llvm.org
Mon Jul 13 22:45:56 PDT 2026


https://github.com/MaskRay updated https://github.com/llvm/llvm-project/pull/209087

>From 67ae5ab8a650f692d23bb294ac5f30c66d924bb1 Mon Sep 17 00:00:00 2001
From: Fangrui Song <i at maskray.me>
Date: Sun, 12 Jul 2026 18:44:13 -0700
Subject: [PATCH 1/2] [ELF] Synthesize STT_FILE if necessary

lld groups local symbols in .symtab by input file, appending a symbol
converted to STB_LOCAL (hidden visibility, version script `local:`, or
--exclude-libs) to its file's group. The nearest preceding STT_FILE then
claims the symbol, but the attribution can be wrong: the file may
contain multiple STT_FILE symbols (-r output), or lack an STT_FILE for
the symbol's unit.

```
FILE    LOCAL  DEFAULT   ABS a.c
NOTYPE  LOCAL  DEFAULT     7 a_local
FILE    LOCAL  DEFAULT   ABS b.c
NOTYPE  LOCAL  DEFAULT     7 b_local
NOTYPE  LOCAL  DEFAULT     7 a_localized
```

Follow GNU ld:

- If the output contains an STT_FILE symbol, place all converted symbols
  (including linker-synthesized ones such as `_DYNAMIC`) at the end of
  the local part, after a synthetic STT_FILE with an empty name.
- For -r output, synthesize an STT_FILE named after the input file for
  an input that outputs local symbols but no STT_FILE, so that its locals
  are not attributed to a preceding file. We use the basename instead of
  the full name for local determinism (#47367).

GNU ld synthesizes this per-input STT_FILE for executable and
shared-object output as well. Restrict it to -r for now to minimize test
updates.

Close #47367 and #191478
---
 lld/ELF/SyntheticSections.cpp              |  63 ++++---
 lld/ELF/SyntheticSections.h                |  12 ++
 lld/ELF/Writer.cpp                         |  23 ++-
 lld/test/ELF/local-symbols-order.s         | 186 +++++++++++++++++----
 lld/test/ELF/lto/parallel-internalize.ll   |   9 +
 lld/test/ELF/ppc64-local-entry.s           |  13 +-
 lld/test/ELF/relocatable-comdat-multiple.s |   4 +-
 lld/test/ELF/relocatable-comdat2.s         |   4 +-
 lld/test/ELF/relocatable-discard-locals.s  |   6 +-
 lld/test/ELF/relocatable-gc.s              |  27 +--
 lld/test/ELF/relocatable.s                 |   6 +-
 lld/test/ELF/strtab-nodedup.s              |  12 +-
 12 files changed, 274 insertions(+), 91 deletions(-)

diff --git a/lld/ELF/SyntheticSections.cpp b/lld/ELF/SyntheticSections.cpp
index 06d5129844e5c..34f77e751e216 100644
--- a/lld/ELF/SyntheticSections.cpp
+++ b/lld/ELF/SyntheticSections.cpp
@@ -2003,34 +2003,57 @@ void SymbolTableBaseSection::finalizeContents() {
     s.sym->dynsymIndex = ++i;
 }
 
-// The ELF spec requires that all local symbols precede global symbols, so we
-// sort symbol entries in this function. (For .dynsym, we don't do that because
-// symbols for dynamic linking are inherently all globals.)
+// The ELF spec requires local symbols to precede globals. We additionally group
+// the locals by file, each led by its first STT_FILE.
 //
-// Aside from above, we put local symbols in groups starting with the STT_FILE
-// symbol. That is convenient for purpose of identifying where are local symbols
-// coming from.
+// symbols[firstGlobalIdx, sttFileIdx) can be converted local: move them after
+// the per-file groups, behind the synthetic STT_FILE sttFileSym. Locals added
+// later (e.g. thunks) fall outside that range and stay in their file's group.
 void SymbolTableBaseSection::sortSymTabSymbols() {
-  // Move all local symbols before global symbols.
-  auto e = std::stable_partition(
-      symbols.begin(), symbols.end(),
-      [](const SymbolTableEntry &s) { return s.sym->isLocal(); });
-  size_t numLocals = e - symbols.begin();
-  getParent()->info = numLocals + 1;
-
-  // We want to group the local symbols by file. For that we rebuild the local
-  // part of the symbols vector. We do not need to care about the STT_FILE
-  // symbols, they are already naturally placed first in each group. That
-  // happens because STT_FILE is always the first symbol in the object and hence
-  // precede all other local symbols we add for a file.
   MapVector<InputFile *, SmallVector<SymbolTableEntry, 0>> arr;
-  for (const SymbolTableEntry &s : llvm::make_range(symbols.begin(), e))
-    arr[s.sym->file].push_back(s);
+  SmallVector<SymbolTableEntry, 0> localized, globals;
+  SymbolTableEntry fileEntry{};
+  for (size_t i = 0, e = symbols.size(); i != e; ++i) {
+    const SymbolTableEntry &s = symbols[i];
+    if (!s.sym->isLocal())
+      globals.push_back(s);
+    else if (sttFileSym && i >= firstGlobalIdx && i < sttFileIdx)
+      localized.push_back(s);
+    else if (s.sym != sttFileSym)
+      arr[s.sym->file].push_back(s);
+    else
+      fileEntry = s;
+  }
 
   auto i = symbols.begin();
   for (auto &p : arr)
     for (SymbolTableEntry &entry : p.second)
       *i++ = entry;
+  if (sttFileSym) {
+    *i++ = fileEntry;
+    i = std::copy(localized.begin(), localized.end(), i);
+  }
+  getParent()->info = i - symbols.begin() + 1;
+  std::copy(globals.begin(), globals.end(), i);
+}
+
+// A symbol converted to STB_LOCAL cannot be reliably attributed to a file:
+// within a file's group the wrong STT_FILE would claim it, as a file may hold
+// several STT_FILE symbols (relocatable output) or none. Like GNU ld, when the
+// output has an STT_FILE, add a synthetic empty-name STT_FILE.
+void SymbolTableBaseSection::maybeAddSttFile() {
+  ArrayRef<SymbolTableEntry> syms = symbols;
+  if (llvm::none_of(syms.take_front(firstGlobalIdx),
+                    [](const SymbolTableEntry &s) { return s.sym->isFile(); }))
+    return;
+  if (llvm::any_of(
+          syms.drop_front(firstGlobalIdx),
+          [](const SymbolTableEntry &s) { return s.sym->isLocal(); })) {
+    sttFileIdx = symbols.size();
+    sttFileSym = makeDefined(ctx, ctx.internalFile, "", STB_LOCAL, STV_DEFAULT,
+                             STT_FILE, /*value=*/0, /*size=*/0, nullptr);
+    addSymbol(sttFileSym);
+  }
 }
 
 void SymbolTableBaseSection::addSymbol(Symbol *b) {
diff --git a/lld/ELF/SyntheticSections.h b/lld/ELF/SyntheticSections.h
index 523f6587899fe..1a29a96013723 100644
--- a/lld/ELF/SyntheticSections.h
+++ b/lld/ELF/SyntheticSections.h
@@ -659,6 +659,8 @@ class SymbolTableBaseSection : public SyntheticSection {
   void finalizeContents() override;
   size_t getSize() const override { return getNumSymbols() * entsize; }
   void addSymbol(Symbol *sym);
+  void maybeAddSttFile();
+  void markGlobalPart() { firstGlobalIdx = symbols.size(); }
   unsigned getNumSymbols() const { return symbols.size() + 1; }
   size_t getSymbolIndex(const Symbol &sym);
   ArrayRef<SymbolTableEntry> getSymbols() const { return symbols; }
@@ -669,6 +671,16 @@ class SymbolTableBaseSection : public SyntheticSection {
   // A vector of symbols and their string table offsets.
   SmallVector<SymbolTableEntry, 0> symbols;
 
+  // Synthetic STT_FILE with an empty name, added by maybeAddSttFile and placed
+  // by sortSymTabSymbols before all symbols demoted to STB_LOCAL.
+  Defined *sttFileSym = nullptr;
+
+  // symbols[firstGlobalIdx, sttFileIdx) were originally non-local and may be
+  // converted to local. sttFileIdx is where sttFileSym lands. Locals added
+  // later (thunks via addSyntheticLocal) fall outside and stay grouped with
+  // their file.
+  size_t firstGlobalIdx = 0, sttFileIdx = 0;
+
   StringTableSection &strTabSec;
 
   llvm::once_flag onceFlag;
diff --git a/lld/ELF/Writer.cpp b/lld/ELF/Writer.cpp
index 2da732c857be9..5766c1c2bf6d7 100644
--- a/lld/ELF/Writer.cpp
+++ b/lld/ELF/Writer.cpp
@@ -30,6 +30,7 @@
 #include "llvm/ADT/StringMap.h"
 #include "llvm/Support/BLAKE3.h"
 #include "llvm/Support/Parallel.h"
+#include "llvm/Support/Path.h"
 #include "llvm/Support/RandomNumberGenerator.h"
 #include "llvm/Support/TimeProfiler.h"
 #include "llvm/Support/xxhash.h"
@@ -456,9 +457,24 @@ static void demoteAndCopyLocalSymbols(Ctx &ctx) {
         symsVec[i].push_back(b);
     }
   });
-  for (auto &syms : ArrayRef(symsVec.get(), ctx.objectFiles.size()))
+  for (size_t i = 0, e = ctx.objectFiles.size(); i != e; ++i) {
+    // For -r, synthesize an STT_FILE named after the input file for an input
+    // that contributes local symbols but no STT_FILE, so that its symbols are
+    // not attributed to another file's STT_FILE (matching GNU ld).
+    // --discard-all discards STT_FILE symbols.
+    auto &syms = symsVec[i];
+    if (ctx.arg.relocatable && ctx.arg.discard != DiscardPolicy::All &&
+        !syms.empty() &&
+        llvm::none_of(syms, [](Symbol *s) { return s->isFile(); })) {
+      InputFile *file = ctx.objectFiles[i];
+      ctx.in.symTab->addSymbol(
+          makeDefined(ctx, file, sys::path::filename(file->getName()),
+                      STB_LOCAL, /*stOther=*/0, STT_FILE, /*value=*/0,
+                      /*size=*/0, nullptr));
+    }
     for (Symbol *sym : syms)
       ctx.in.symTab->addSymbol(sym);
+  }
 }
 
 // Create a section symbol for each output section so that we can represent
@@ -1934,6 +1950,8 @@ template <class ELFT> void Writer<ELFT>::finalizeSections() {
 
   {
     llvm::TimeTraceScope timeScope("Add symbols to symtabs");
+    if (ctx.in.symTab)
+      ctx.in.symTab->markGlobalPart();
     // Now that we have defined all possible global symbols including linker-
     // synthesized ones. Visit all symbols to give the finishing touches.
     for (Symbol *sym : ctx.symtab->getSymbols()) {
@@ -1953,7 +1971,8 @@ template <class ELFT> void Writer<ELFT>::finalizeSections() {
             addVerneed(ctx, *sym);
       }
     }
-
+    if (ctx.in.symTab && !ctx.arg.relocatable)
+      ctx.in.symTab->maybeAddSttFile();
   }
 
   if (ctx.in.mipsGot)
diff --git a/lld/test/ELF/local-symbols-order.s b/lld/test/ELF/local-symbols-order.s
index 46530a4faa8b6..0e3abbce19daa 100644
--- a/lld/test/ELF/local-symbols-order.s
+++ b/lld/test/ELF/local-symbols-order.s
@@ -1,37 +1,149 @@
-# REQUIRES: x86
-
-# RUN: echo '.data; .file "file2"; foo2:; .global bar2; .hidden bar2; bar2:' > %t2.s
-# RUN: llvm-mc -filetype=obj -triple=x86_64-unknown-linux %t2.s -o %t2.o
-# RUN: llvm-mc -filetype=obj -triple=x86_64-unknown-linux %s -o %t1.o
-
-# RUN: ld.lld -o %t %t1.o %t2.o --emit-relocs
-# RUN: llvm-readelf --symbols --sections %t | FileCheck %s
-
-## Check we sort local symbols to match the following order: 
-## file1, local1, section1, hidden1, file2, local2, section2, hidden2 ...
-
-# CHECK: Section Headers:
-# CHECK:   [Nr] Name
-# CHECK:   [ [[ST:.*]]] .text
-# CHECK:   [ [[SD:.*]]] .data
-# CHECK:   [ [[SC:.*]]] .comment
-
-# CHECK:      Size Type    Bind   Vis      Ndx Name
-# CHECK-NEXT:    0 NOTYPE  LOCAL  DEFAULT  UND
-# CHECK-NEXT:    0 FILE    LOCAL  DEFAULT  ABS file1
-# CHECK-NEXT:    0 NOTYPE  LOCAL  DEFAULT    1 foo1
-# CHECK-NEXT:    0 SECTION LOCAL  DEFAULT    [[ST]]
-# CHECK-NEXT:    0 NOTYPE  LOCAL  HIDDEN     1 bar1
-# CHECK-NEXT:    0 FILE    LOCAL  DEFAULT  ABS file2
-# CHECK-NEXT:    0 NOTYPE  LOCAL  DEFAULT    2 foo2
-# CHECK-NEXT:    0 SECTION LOCAL  DEFAULT    [[SD]]
-# CHECK-NEXT:    0 NOTYPE  LOCAL  HIDDEN     2 bar2
-# CHECK-NEXT:    0 SECTION LOCAL  DEFAULT    [[SC]]
-
-foo1:
-
-.global bar1
-.hidden bar1
-bar1:
-
-.file "file1"
+# REQUIRES: aarch64
+## Check the order of local symbols: grouped by file, each group led by the file's STT symbol.
+## Symbols converted to STB_LOCAL are placed at the end of the local part after a synthetic
+## STT_FILE with an empty name, omitted if the output has no STT_FILE.
+
+# RUN: rm -rf %t && split-file %s %t && cd %t
+# RUN: llvm-mc -filetype=obj -triple=aarch64 a.s -o a.o
+# RUN: llvm-mc -filetype=obj -triple=aarch64 b.s -o b.o
+# RUN: mkdir c && llvm-mc -filetype=obj -triple=aarch64 c.s -o c/a.o
+
+## Only b.c has an STT_FILE. a.o's a_local has no leading STT_FILE; the hidden
+## a_hidden follows the synthetic STT_FILE. --emit-relocs adds STT_SECTION
+## symbols to their file's group.
+# RUN: ld.lld --emit-relocs a.o b.o -o ab
+# RUN: llvm-readelf -s ab | FileCheck %s
+
+# CHECK:      NOTYPE  LOCAL  DEFAULT [[#]] a_local
+# CHECK-NEXT: NOTYPE  LOCAL  DEFAULT [[#]] $x
+# CHECK-NEXT: SECTION LOCAL  DEFAULT [[#]] .text
+# CHECK-NEXT: FILE    LOCAL  DEFAULT   ABS b.c
+# CHECK-NEXT: NOTYPE  LOCAL  DEFAULT [[#]] b_local
+# CHECK-NEXT: NOTYPE  LOCAL  DEFAULT [[#]] $d
+# CHECK-NEXT: SECTION LOCAL  DEFAULT [[#]] .data
+# CHECK-NEXT: SECTION LOCAL  DEFAULT [[#]] .comment
+# CHECK-NEXT: FILE    LOCAL  DEFAULT   ABS{{ $}}
+# CHECK-NEXT: NOTYPE  LOCAL  HIDDEN  [[#]] a_hidden
+# CHECK-NEXT: NOTYPE  GLOBAL DEFAULT [[#]] a_localized
+# CHECK-NEXT: NOTYPE  GLOBAL DEFAULT [[#]] a_exported
+# CHECK-NEXT: NOTYPE  GLOBAL DEFAULT [[#]] b_localized
+# CHECK-NEXT: NOTYPE  GLOBAL DEFAULT [[#]] b_exported
+
+## For -r, synthesize STT_FILE for a.o, which lacks one.
+# RUN: ld.lld -r a.o b.o -o ab.o
+# RUN: llvm-readelf -s ab.o | FileCheck %s --check-prefix=RO
+
+# RO:      FILE    LOCAL  DEFAULT   ABS a.o
+# RO-NEXT: NOTYPE  LOCAL  DEFAULT [[#]] a_local
+# RO-NEXT: NOTYPE  LOCAL  DEFAULT [[#]] $x
+# RO-NEXT: SECTION LOCAL  DEFAULT [[#]] .text
+# RO-NEXT: FILE    LOCAL  DEFAULT   ABS b.c
+# RO-NEXT: NOTYPE  LOCAL  DEFAULT [[#]] b_local
+# RO-NEXT: NOTYPE  LOCAL  DEFAULT [[#]] $d
+# RO-NEXT: SECTION LOCAL  DEFAULT [[#]] .data
+# RO-NEXT: NOTYPE  GLOBAL DEFAULT [[#]] a_localized
+# RO-NEXT: NOTYPE  GLOBAL HIDDEN  [[#]] a_hidden
+# RO-NEXT: NOTYPE  GLOBAL DEFAULT [[#]] a_exported
+# RO-NEXT: NOTYPE  GLOBAL DEFAULT [[#]] b_localized
+# RO-NEXT: NOTYPE  GLOBAL DEFAULT [[#]] b_exported
+
+## Synthesize STT_FILE named after its basename, not its path, for local determinism (#47367).
+# RUN: ld.lld -r ab.o c/a.o -o abc.o
+# RUN: llvm-readelf -s abc.o | FileCheck %s --check-prefix=MULTI
+
+# MULTI:      FILE    LOCAL  DEFAULT   ABS a.o
+# MULTI-NEXT: NOTYPE  LOCAL  DEFAULT [[#]] a_local
+# MULTI-NEXT: NOTYPE  LOCAL  DEFAULT [[#]] $x
+# MULTI-NEXT: FILE    LOCAL  DEFAULT   ABS b.c
+# MULTI-NEXT: NOTYPE  LOCAL  DEFAULT [[#]] b_local
+# MULTI-NEXT: NOTYPE  LOCAL  DEFAULT [[#]] $d
+# MULTI-NEXT: SECTION LOCAL  DEFAULT [[#]] .text
+# MULTI-NEXT: SECTION LOCAL  DEFAULT [[#]] .data
+# MULTI-NEXT: FILE    LOCAL  DEFAULT   ABS a.o
+# MULTI-NEXT: NOTYPE  LOCAL  DEFAULT [[#]] $x
+# MULTI-NEXT: NOTYPE  LOCAL  DEFAULT [[#]] far
+
+## Symbols localized by a version script follow a synthetic STT_FILE with empty name.
+# RUN: ld.lld -shared --version-script=ver ab.o -o ab.so
+# RUN: llvm-readelf -s ab.so | FileCheck %s --check-prefix=VER
+
+# VER:      FILE    LOCAL  DEFAULT   ABS a.o
+# VER-NEXT: NOTYPE  LOCAL  DEFAULT [[#]] a_local
+# VER-NEXT: NOTYPE  LOCAL  DEFAULT [[#]] $x
+# VER-NEXT: FILE    LOCAL  DEFAULT   ABS b.c
+# VER-NEXT: NOTYPE  LOCAL  DEFAULT [[#]] b_local
+# VER-NEXT: NOTYPE  LOCAL  DEFAULT [[#]] $d
+# VER-NEXT: FILE    LOCAL  DEFAULT   ABS{{ $}}
+# VER-NEXT: NOTYPE  LOCAL  DEFAULT [[#]] a_localized
+# VER-NEXT: NOTYPE  LOCAL  HIDDEN  [[#]] a_hidden
+# VER-NEXT: NOTYPE  LOCAL  DEFAULT [[#]] b_localized
+# VER-NEXT: NOTYPE  LOCAL  HIDDEN  [[#]] _DYNAMIC
+# VER-NEXT: NOTYPE  GLOBAL DEFAULT [[#]] a_exported
+# VER-NEXT: NOTYPE  GLOBAL DEFAULT [[#]] b_exported
+
+## A range-extension thunk is added to .symtab by finalizeAddressDependentContent,
+## after the global part. It must stay in the local part before the synthetic
+## STT_FILE that groups the demoted c_hidden, not be moved behind it.
+# RUN: ld.lld c/a.o b.o -T lds -o cb
+# RUN: llvm-readelf -s cb | FileCheck %s --check-prefix=THUNK
+
+# THUNK:      NOTYPE  LOCAL  DEFAULT [[#]] far
+# THUNK:      FUNC    LOCAL  DEFAULT [[#]] __AArch64AbsLongThunk_{{.*}}
+# THUNK-NEXT: NOTYPE  LOCAL  DEFAULT [[#]] $x
+# THUNK-NEXT: NOTYPE  LOCAL  DEFAULT [[#]] $d
+# THUNK-NEXT: FILE    LOCAL  DEFAULT   ABS{{ $}}
+# THUNK-NEXT: NOTYPE  LOCAL  HIDDEN  [[#]] c_hidden
+# THUNK-NEXT: NOTYPE  GLOBAL DEFAULT [[#]] _start
+
+## --discard-all discards STT_FILE symbols as well. With no STT_FILE in the
+## output, no synthetic STT_FILE is added.
+# RUN: ld.lld -shared --discard-all --version-script=ver ab.o -o ab2.so
+# RUN: llvm-readelf -s ab2.so | FileCheck %s --check-prefix=DISCARD --implicit-check-not=FILE
+
+# DISCARD:      NOTYPE  LOCAL  DEFAULT [[#]] a_localized
+# DISCARD-NEXT: NOTYPE  LOCAL  HIDDEN  [[#]] a_hidden
+# DISCARD-NEXT: NOTYPE  LOCAL  DEFAULT [[#]] b_localized
+# DISCARD-NEXT: NOTYPE  LOCAL  HIDDEN  [[#]] _DYNAMIC
+# DISCARD-NEXT: NOTYPE  GLOBAL DEFAULT [[#]] a_exported
+# DISCARD-NEXT: NOTYPE  GLOBAL DEFAULT [[#]] b_exported
+
+#--- a.s
+a_local:
+.globl a_localized
+a_localized:
+.globl a_hidden
+.hidden a_hidden
+a_hidden:
+.globl a_exported
+a_exported:
+  nop
+
+#--- b.s
+.file "b.c"
+.data
+b_local:
+.globl b_localized
+b_localized:
+.globl b_exported
+b_exported:
+  .byte 0
+
+#--- c.s
+.globl _start, c_hidden
+.hidden c_hidden
+_start:
+  bl far
+c_hidden:
+  ret
+.section .far,"ax"
+far:
+  ret
+
+#--- lds
+SECTIONS {
+  .text 0x10000    : { *(.text) }
+  .far  0x10000000 : { *(.far) }
+}
+
+#--- ver
+v1 { global: *_exported; local: *; };
diff --git a/lld/test/ELF/lto/parallel-internalize.ll b/lld/test/ELF/lto/parallel-internalize.ll
index a1511d6f6867d..d85096263f9f9 100644
--- a/lld/test/ELF/lto/parallel-internalize.ll
+++ b/lld/test/ELF/lto/parallel-internalize.ll
@@ -35,6 +35,15 @@
 ; CHECK-NEXT:     Section: Absolute
 ; CHECK-NEXT:   }
 ; CHECK-NEXT:   Symbol {
+; CHECK-NEXT:     Name:  (0)
+; CHECK-NEXT:     Value: 0x0
+; CHECK-NEXT:     Size: 0
+; CHECK-NEXT:     Binding: Local
+; CHECK-NEXT:     Type: File
+; CHECK-NEXT:     Other: 0
+; CHECK-NEXT:     Section: Absolute
+; CHECK-NEXT:   }
+; CHECK-NEXT:   Symbol {
 ; CHECK-NEXT:     Name: bar
 ; CHECK-NEXT:     Value:
 ; CHECK-NEXT:     Size: 8
diff --git a/lld/test/ELF/ppc64-local-entry.s b/lld/test/ELF/ppc64-local-entry.s
index 00940b7b27f76..ce2c361d8dff6 100644
--- a/lld/test/ELF/ppc64-local-entry.s
+++ b/lld/test/ELF/ppc64-local-entry.s
@@ -2,7 +2,7 @@
 
 # RUN: llvm-mc -filetype=obj -triple=powerpc64 %s -o %t
 # RUN: ld.lld -r %t -o %t2
-# RUN: llvm-objdump -s --section=.symtab %t2 | FileCheck %s
+# RUN: llvm-readelf -s %t2 | FileCheck %s
 
 .text
 .abiversion 2
@@ -37,11 +37,6 @@ _start:
 	.type	g, at object               # @g
 	.lcomm	g,4,4
 
-// We expect the st_other byte to be 0x60:
-// localentry = 011 (gep + 2 instructions), reserved = 000,
-// visibility = 00 (STV_DEFAULT)
-// Currently, llvm-objdump does not support displaying
-// st_other's PPC64 specific flags, thus we check the
-// result of the hexdump of .symtab section.
-
-// CHECK: 0060 00000003 12600001 00000000 00000000
+## We expect st_other to be 0x60: localentry = 011 (gep + 2 instructions),
+## reserved = 000, visibility = 00 (STV_DEFAULT).
+# CHECK: FUNC    GLOBAL DEFAULT [<other: 0x60>] [[#]] _start
diff --git a/lld/test/ELF/relocatable-comdat-multiple.s b/lld/test/ELF/relocatable-comdat-multiple.s
index 7a4eddd837a9b..6935e0a0fd79a 100644
--- a/lld/test/ELF/relocatable-comdat-multiple.s
+++ b/lld/test/ELF/relocatable-comdat-multiple.s
@@ -9,7 +9,7 @@
 # CHECK-NEXT:     Name: .group
 # CHECK-NEXT:     Index: 2
 # CHECK-NEXT:     Link: 9
-# CHECK-NEXT:     Info: 1
+# CHECK-NEXT:     Info: 2
 # CHECK-NEXT:     Type: COMDAT
 # CHECK-NEXT:     Signature: aaa
 # CHECK-NEXT:     Section(s) in group [
@@ -21,7 +21,7 @@
 # CHECK-NEXT:     Name: .group
 # CHECK-NEXT:     Index: 5
 # CHECK-NEXT:     Link: 9
-# CHECK-NEXT:     Info: 6
+# CHECK-NEXT:     Info: 8
 # CHECK-NEXT:     Type: COMDAT
 # CHECK-NEXT:     Signature: bbb
 # CHECK-NEXT:     Section(s) in group [
diff --git a/lld/test/ELF/relocatable-comdat2.s b/lld/test/ELF/relocatable-comdat2.s
index 8e3161d49f8fc..cb2074bf9abb1 100644
--- a/lld/test/ELF/relocatable-comdat2.s
+++ b/lld/test/ELF/relocatable-comdat2.s
@@ -14,7 +14,7 @@
 # CHECK-NEXT:     Name: .group
 # CHECK-NEXT:     Index: 2
 # CHECK-NEXT:     Link: 8
-# CHECK-NEXT:     Info: 1
+# CHECK-NEXT:     Info: 2
 # CHECK-NEXT:     Type: COMDAT
 # CHECK-NEXT:     Signature: bar
 # CHECK-NEXT:     Section(s) in group [
@@ -25,7 +25,7 @@
 # CHECK-NEXT:     Name: .group
 # CHECK-NEXT:     Index: 4
 # CHECK-NEXT:     Link: 8
-# CHECK-NEXT:     Info: 2
+# CHECK-NEXT:     Info: 3
 # CHECK-NEXT:     Type: COMDAT
 # CHECK-NEXT:     Signature: zed
 # CHECK-NEXT:     Section(s) in group [
diff --git a/lld/test/ELF/relocatable-discard-locals.s b/lld/test/ELF/relocatable-discard-locals.s
index 8941e8de7abc8..be8752467f4a8 100644
--- a/lld/test/ELF/relocatable-discard-locals.s
+++ b/lld/test/ELF/relocatable-discard-locals.s
@@ -9,11 +9,12 @@
 # RUN: llvm-readobj -r %tlocal.ro | FileCheck --check-prefix=REL %s
 
 # RUN: ld.lld -r --discard-all %t.o -o %tall.ro
-# RUN: llvm-readelf -s %tall.ro | FileCheck --check-prefix=DISCARD-ALL %s
+# RUN: llvm-readelf -s %tall.ro | FileCheck --check-prefix=DISCARD-ALL --implicit-check-not=FILE %s
 # RUN: llvm-readobj -r %tall.ro | FileCheck --check-prefix=REL %s
 
 ## --discard-locals removes unused local symbols which start with ".L"
 # DISCARD-LOCALS:    0: {{0+}} 0 NOTYPE  LOCAL  DEFAULT UND
+# DISCARD-LOCALS-NEXT:           FILE    LOCAL  DEFAULT ABS {{.*}}.o
 # DISCARD-LOCALS-NEXT:           NOTYPE  LOCAL  DEFAULT {{.*}} .Lused
 # DISCARD-LOCALS-NEXT:           NOTYPE  LOCAL  DEFAULT {{.*}} used
 # DISCARD-LOCALS-NEXT:           NOTYPE  LOCAL  DEFAULT {{.*}} .L.str
@@ -25,7 +26,8 @@
 # DISCARD-LOCALS-NEXT:           SECTION LOCAL  DEFAULT {{.*}} .rodata.str1.1
 # DISCARD-LOCALS-NEXT:           NOTYPE  GLOBAL DEFAULT {{.*}} _start
 
-## --discard-all removes all unused regular local symbols.
+## --discard-all removes all unused regular local symbols and STT_FILE
+## symbols; no STT_FILE is synthesized.
 # DISCARD-ALL:    0: {{0+}} 0 NOTYPE  LOCAL  DEFAULT UND
 # DISCARD-ALL-NEXT:           NOTYPE  LOCAL  DEFAULT {{.*}} .Lused
 # DISCARD-ALL-NEXT:           NOTYPE  LOCAL  DEFAULT {{.*}} used
diff --git a/lld/test/ELF/relocatable-gc.s b/lld/test/ELF/relocatable-gc.s
index 7d367ba57ec46..3304ed807ad72 100644
--- a/lld/test/ELF/relocatable-gc.s
+++ b/lld/test/ELF/relocatable-gc.s
@@ -12,11 +12,12 @@
 # CHECK:      [ 1] .group
 # CHECK-NEXT: [ 2] .note.GNU-stack
 
-# CHECK:      Symbol table '.symtab' contains 3 entries:
+# CHECK:      Symbol table '.symtab' contains 4 entries:
 # CHECK-NEXT: Num:
 # CHECK-NEXT: 0:
-# CHECK-NEXT: 1: {{.*}} NOTYPE  LOCAL DEFAULT  1 group
-# CHECK-NEXT: 2: {{.*}} SECTION LOCAL DEFAULT  1
+# CHECK-NEXT: 1: {{.*}} FILE    LOCAL DEFAULT ABS {{.*}}.o
+# CHECK-NEXT: 2: {{.*}} NOTYPE  LOCAL DEFAULT  1 group
+# CHECK-NEXT: 3: {{.*}} SECTION LOCAL DEFAULT  1
 
 ## -u keeps .text.bar alive. Other group members are kept alive as well.
 # RUN: ld.lld -r --gc-sections -u bar %t.o -o - | llvm-readelf -Ss - | \
@@ -34,10 +35,10 @@
 # KEEP_GROUP-NEXT: [ 3] .text.foo
 # KEEP_GROUP-NEXT: [ 4] .note.GNU-stack
 
-# KEEP_GROUP:      Symbol table '.symtab' contains 7 entries:
-# KEEP_GROUP:      4: {{.*}} SECTION
-# KEEP_GROUP-NEXT: 5: {{.*}}   2 bar
-# KEEP_GROUP-NEXT: 6: {{.*}}   3 foo
+# KEEP_GROUP:      Symbol table '.symtab' contains 8 entries:
+# KEEP_GROUP:      5: {{.*}} SECTION
+# KEEP_GROUP-NEXT: 6: {{.*}}   2 bar
+# KEEP_GROUP-NEXT: 7: {{.*}}   3 foo
 
 ## If .text is retained, its referenced qux and .fred are retained as well.
 ## fred_und is used (by .fred) and thus emitted.
@@ -53,12 +54,12 @@
 # KEEP_START-NEXT: [ 6] .rela.fred
 # KEEP_START-NEXT: [ 7] .note.GNU-stack
 
-# KEEP_START:      Symbol table '.symtab' contains 10 entries:
-# KEEP_START:      5: {{.*}} SECTION
-# KEEP_START-NEXT: 6: {{.*}}   1 _start
-# KEEP_START-NEXT: 7: {{.*}}   5 fred
-# KEEP_START-NEXT: 8: {{.*}} UND __start_qux
-# KEEP_START-NEXT: 9: {{.*}} UND fred_und
+# KEEP_START:      Symbol table '.symtab' contains 11 entries:
+# KEEP_START:      6: {{.*}} SECTION
+# KEEP_START-NEXT: 7: {{.*}}   1 _start
+# KEEP_START-NEXT: 8: {{.*}}   5 fred
+# KEEP_START-NEXT: 9: {{.*}} UND __start_qux
+# KEEP_START-NEXT: 10: {{.*}} UND fred_und
 
 .section qux,"a", at progbits
   .byte 0
diff --git a/lld/test/ELF/relocatable.s b/lld/test/ELF/relocatable.s
index 5af01c82cd08f..3be3ca9ac2e12 100644
--- a/lld/test/ELF/relocatable.s
+++ b/lld/test/ELF/relocatable.s
@@ -59,9 +59,9 @@
 # SECTION:   2 .rela.text    00000090 0000000000000000
 # SECTION:   3 .bss          00000018 0000000000000000 BSS
 # SECTION:   4 .note.GNU-stack 00000000 0000000000000000
-# SECTION:   5 .symtab       00000168 0000000000000000
+# SECTION:   5 .symtab       00000180 0000000000000000
 # SECTION:   6 .shstrtab     00000041 0000000000000000
-# SECTION:   7 .strtab       0000002d 0000000000000000
+# SECTION:   7 .strtab       {{[0-9a-f]+}} 0000000000000000
 
 # CHECKTEXT:      Disassembly of section .text:
 # CHECKTEXT-EMPTY:
@@ -94,7 +94,7 @@
 # CHECKEXE-NEXT:    Version: 1
 # CHECKEXE-NEXT:    Entry: 0x201160
 # CHECKEXE-NEXT:    ProgramHeaderOffset: 0x40
-# CHECKEXE-NEXT:    SectionHeaderOffset: 0x358
+# CHECKEXE-NEXT:    SectionHeaderOffset:
 # CHECKEXE-NEXT:    Flags [
 # CHECKEXE-NEXT:    ]
 # CHECKEXE-NEXT:    HeaderSize: 64
diff --git a/lld/test/ELF/strtab-nodedup.s b/lld/test/ELF/strtab-nodedup.s
index b20e738de0679..4deae29ce2da1 100644
--- a/lld/test/ELF/strtab-nodedup.s
+++ b/lld/test/ELF/strtab-nodedup.s
@@ -8,18 +8,28 @@
 # RUN: ld.lld %t/a.o %t/b.o -o %t/a
 # RUN: llvm-readelf -p .strtab %t/a | FileCheck %s --check-prefix=NODEDUP
 # RUN: ld.lld -r -O2 %t/a.o %t/b.o -o %t/a.ro
-# RUN: llvm-readelf -p .strtab %t/a.ro | FileCheck %s --check-prefix=NODEDUP
+# RUN: llvm-readelf -p .strtab %t/a.ro | FileCheck %s --check-prefix=RO
 
 # NODEDUP:        [     1]  local
 # NODEDUP-NEXT:   [     7]  local
 # NODEDUP-NEXT:   [     d]  foo
 # NODEDUP-EMPTY:
 
+## For -r, a.o and b.o get a synthesized STT_FILE.
+# RO:        [     1]  a.o
+# RO-NEXT:   [     5]  local
+# RO-NEXT:   [     b]  b.o
+# RO-NEXT:   [     f]  local
+# RO-NEXT:   [    15]  foo
+# RO-EMPTY:
+
 # RUN: llvm-readelf -s %t/a.ro | FileCheck %s --check-prefix=SYMTAB
 
 # SYMTAB:    0: {{0+}} 0 NOTYPE  LOCAL  DEFAULT UND
+# SYMTAB-NEXT:           FILE    LOCAL  DEFAULT ABS a.o
 # SYMTAB-NEXT:           NOTYPE  LOCAL  DEFAULT [[#]] local
 # SYMTAB-NEXT:           SECTION LOCAL  DEFAULT [[#]] .text
+# SYMTAB-NEXT:           FILE    LOCAL  DEFAULT ABS b.o
 # SYMTAB-NEXT:           NOTYPE  LOCAL  DEFAULT [[#]] local
 # SYMTAB-NEXT:           NOTYPE  GLOBAL DEFAULT [[#]] foo
 

>From 1ffc53571d3e42f0fd3ad57eb421d79c8b7d1dd3 Mon Sep 17 00:00:00 2001
From: Fangrui Song <i at maskray.me>
Date: Mon, 13 Jul 2026 22:45:44 -0700
Subject: [PATCH 2/2] rename to synthSttFileIdx

---
 lld/ELF/SyntheticSections.cpp | 22 ++++++++++++----------
 lld/ELF/SyntheticSections.h   | 12 ++++++------
 2 files changed, 18 insertions(+), 16 deletions(-)

diff --git a/lld/ELF/SyntheticSections.cpp b/lld/ELF/SyntheticSections.cpp
index 34f77e751e216..599184bef3fe0 100644
--- a/lld/ELF/SyntheticSections.cpp
+++ b/lld/ELF/SyntheticSections.cpp
@@ -2006,9 +2006,10 @@ void SymbolTableBaseSection::finalizeContents() {
 // The ELF spec requires local symbols to precede globals. We additionally group
 // the locals by file, each led by its first STT_FILE.
 //
-// symbols[firstGlobalIdx, sttFileIdx) can be converted local: move them after
-// the per-file groups, behind the synthetic STT_FILE sttFileSym. Locals added
-// later (e.g. thunks) fall outside that range and stay in their file's group.
+// symbols[firstGlobalIdx, synthSttFileIdx) can be converted local: move them
+// after the per-file groups, behind the synthetic STT_FILE synthSttFileSym.
+// Locals added later (e.g. thunks) fall outside that range and stay in their
+// file's group.
 void SymbolTableBaseSection::sortSymTabSymbols() {
   MapVector<InputFile *, SmallVector<SymbolTableEntry, 0>> arr;
   SmallVector<SymbolTableEntry, 0> localized, globals;
@@ -2017,9 +2018,9 @@ void SymbolTableBaseSection::sortSymTabSymbols() {
     const SymbolTableEntry &s = symbols[i];
     if (!s.sym->isLocal())
       globals.push_back(s);
-    else if (sttFileSym && i >= firstGlobalIdx && i < sttFileIdx)
+    else if (synthSttFileSym && i >= firstGlobalIdx && i < synthSttFileIdx)
       localized.push_back(s);
-    else if (s.sym != sttFileSym)
+    else if (s.sym != synthSttFileSym)
       arr[s.sym->file].push_back(s);
     else
       fileEntry = s;
@@ -2029,7 +2030,7 @@ void SymbolTableBaseSection::sortSymTabSymbols() {
   for (auto &p : arr)
     for (SymbolTableEntry &entry : p.second)
       *i++ = entry;
-  if (sttFileSym) {
+  if (synthSttFileSym) {
     *i++ = fileEntry;
     i = std::copy(localized.begin(), localized.end(), i);
   }
@@ -2049,10 +2050,11 @@ void SymbolTableBaseSection::maybeAddSttFile() {
   if (llvm::any_of(
           syms.drop_front(firstGlobalIdx),
           [](const SymbolTableEntry &s) { return s.sym->isLocal(); })) {
-    sttFileIdx = symbols.size();
-    sttFileSym = makeDefined(ctx, ctx.internalFile, "", STB_LOCAL, STV_DEFAULT,
-                             STT_FILE, /*value=*/0, /*size=*/0, nullptr);
-    addSymbol(sttFileSym);
+    synthSttFileIdx = symbols.size();
+    synthSttFileSym =
+        makeDefined(ctx, ctx.internalFile, "", STB_LOCAL, STV_DEFAULT, STT_FILE,
+                    /*value=*/0, /*size=*/0, nullptr);
+    addSymbol(synthSttFileSym);
   }
 }
 
diff --git a/lld/ELF/SyntheticSections.h b/lld/ELF/SyntheticSections.h
index 1a29a96013723..54d68d279728c 100644
--- a/lld/ELF/SyntheticSections.h
+++ b/lld/ELF/SyntheticSections.h
@@ -673,13 +673,13 @@ class SymbolTableBaseSection : public SyntheticSection {
 
   // Synthetic STT_FILE with an empty name, added by maybeAddSttFile and placed
   // by sortSymTabSymbols before all symbols demoted to STB_LOCAL.
-  Defined *sttFileSym = nullptr;
+  Defined *synthSttFileSym = nullptr;
 
-  // symbols[firstGlobalIdx, sttFileIdx) were originally non-local and may be
-  // converted to local. sttFileIdx is where sttFileSym lands. Locals added
-  // later (thunks via addSyntheticLocal) fall outside and stay grouped with
-  // their file.
-  size_t firstGlobalIdx = 0, sttFileIdx = 0;
+  // symbols[firstGlobalIdx, synthSttFileIdx) were originally non-local and may
+  // be converted to local. synthSttFileIdx is where synthSttFileSym lands.
+  // Locals added later (thunks via addSyntheticLocal) fall outside and stay
+  // grouped with their file.
+  size_t firstGlobalIdx = 0, synthSttFileIdx = 0;
 
   StringTableSection &strTabSec;
 



More information about the llvm-commits mailing list