[lld] [ELF,SPARC] Handle TLS GD and LD relocations (PR #228197)

Imre Kaloz via llvm-commits llvm-commits at lists.llvm.org
Thu Oct 1 11:54:23 PDT 2026


https://github.com/kaloz created https://github.com/llvm/llvm-project/pull/228197

A general-dynamic reference forms a GOT offset and calls __tls_get_addr:

    sethi %tgd_hi22(sym), %o0
    add   %o0, %tgd_lo10(sym), %o0
    add   %l7, %o0, %o0, %tgd_add(sym)
    call  __tls_get_addr, %tgd_call(sym)

The GOT entry holds the module index and offset. Local dynamic uses
%tldm/%tldo_hix22/%tldo_lox10 the same way; @dtpoff is non-negative, so
HIX/LOX take the plain high/low split.

The call relocation names the TLS symbol, not the callee, so a call
surviving optimization is rebound to __tls_get_addr in
finalizeRelocScan(), after the parallel scan.

An executable optimizes general dynamic to initial exec or local exec,
local dynamic to local exec.

Signed-off-by: Imre Kaloz <kaloz at kernel.org>


>From cc262b73b316e8d20facad323dfba64b4a180059 Mon Sep 17 00:00:00 2001
From: Imre Kaloz <kaloz at kernel.org>
Date: Tue, 1 Sep 2026 23:34:24 +0200
Subject: [PATCH] [ELF,SPARC] Handle TLS GD and LD relocations

A general-dynamic reference forms a GOT offset and calls __tls_get_addr:

    sethi %tgd_hi22(sym), %o0
    add   %o0, %tgd_lo10(sym), %o0
    add   %l7, %o0, %o0, %tgd_add(sym)
    call  __tls_get_addr, %tgd_call(sym)

The GOT entry holds the module index and offset. Local dynamic uses
%tldm/%tldo_hix22/%tldo_lox10 the same way; @dtpoff is non-negative, so
HIX/LOX take the plain high/low split.

The call relocation names the TLS symbol, not the callee, so a call
surviving optimization is rebound to __tls_get_addr in
finalizeRelocScan(), after the parallel scan.

An executable optimizes general dynamic to initial exec or local exec,
local dynamic to local exec.

Signed-off-by: Imre Kaloz <kaloz at kernel.org>
---
 lld/ELF/Arch/SPARCV9.cpp      | 151 ++++++++++++++++++++++++++++++++++
 lld/test/ELF/sparcv9-tls-gd.s |  87 ++++++++++++++++++++
 lld/test/ELF/sparcv9-tls-ld.s |  78 ++++++++++++++++++
 3 files changed, 316 insertions(+)
 create mode 100644 lld/test/ELF/sparcv9-tls-gd.s
 create mode 100644 lld/test/ELF/sparcv9-tls-ld.s

diff --git a/lld/ELF/Arch/SPARCV9.cpp b/lld/ELF/Arch/SPARCV9.cpp
index 7797990571def..d777daea7bfd5 100644
--- a/lld/ELF/Arch/SPARCV9.cpp
+++ b/lld/ELF/Arch/SPARCV9.cpp
@@ -6,7 +6,9 @@
 //
 //===----------------------------------------------------------------------===//
 
+#include "InputFiles.h"
 #include "RelocScan.h"
+#include "SymbolTable.h"
 #include "Symbols.h"
 #include "SyntheticSections.h"
 #include "Target.h"
@@ -30,6 +32,7 @@ class SPARCV9 final : public TargetInfo {
   void writeGotHeader(uint8_t *buf) const override;
   void writePlt(uint8_t *buf, const Symbol &sym,
                 uint64_t pltEntryAddr) const override;
+  void finalizeRelocScan() override;
   template <class ELFT, class RelTy>
   void scanSectionImpl(InputSectionBase &sec, Relocs<RelTy> rels,
                        unsigned shard);
@@ -48,6 +51,8 @@ SPARCV9::SPARCV9(Ctx &ctx) : TargetInfo(ctx) {
   relativeRel = R_SPARC_RELATIVE;
   symbolicRel = R_SPARC_64;
   tlsGotRel = R_SPARC_TLS_TPOFF64;
+  tlsModuleIndexRel = R_SPARC_TLS_DTPMOD64;
+  tlsOffsetRel = R_SPARC_TLS_DTPOFF64;
   gotHeaderEntriesNum = 1;
   pltEntrySize = 32;
   pltHeaderSize = 4 * pltEntrySize;
@@ -118,6 +123,7 @@ void SPARCV9::scanSectionImpl(InputSectionBase &sec, Relocs<RelTy> rels,
     switch (type) {
     case R_SPARC_NONE:
     case R_SPARC_TLS_IE_ADD:
+    case R_SPARC_TLS_LDO_ADD:
       continue;
 
     // Absolute relocations:
@@ -182,6 +188,44 @@ void SPARCV9::scanSectionImpl(InputSectionBase &sec, Relocs<RelTy> rels,
       }
       break;
 
+    // TLS GD relocations. In an executable the sequence is optimized to
+    // Initial Exec for a preemptible symbol and to Local Exec otherwise.
+    case R_SPARC_TLS_GD_HI22:
+    case R_SPARC_TLS_GD_LO10:
+      rs.handleTlsGd(R_TLSGD_GOT, R_GOT_OFF, R_TPREL, type, offset, addend,
+                     sym);
+      continue;
+    case R_SPARC_TLS_GD_ADD:
+      // A marker on the add. R_ABS is a dummy for the unoptimized sequence and
+      // writes nothing; an optimized one rewrites the instruction.
+      rs.handleTlsGd(R_ABS, R_GOT_OFF, R_TPREL, type, offset, addend, sym);
+      continue;
+    case R_SPARC_TLS_GD_CALL:
+      // The call names the TLS symbol rather than __tls_get_addr, so an
+      // unoptimized call is rebound by finalizeRelocScan().
+      rs.handleTlsGd(R_PLT_PC, R_GOT_OFF, R_TPREL, type, offset, addend, sym);
+      continue;
+
+    // TLS LD relocations. In an executable the sequence is optimized to
+    // Local Exec.
+    case R_SPARC_TLS_LDM_HI22:
+    case R_SPARC_TLS_LDM_LO10:
+      rs.handleTlsLd(R_TLSLD_GOT, type, offset, addend, sym);
+      continue;
+    case R_SPARC_TLS_LDM_ADD:
+      rs.handleTlsLd(R_ABS, type, offset, addend, sym);
+      continue;
+    case R_SPARC_TLS_LDM_CALL:
+      rs.handleTlsLd(R_PLT_PC, type, offset, addend, sym);
+      continue;
+    case R_SPARC_TLS_LDO_HIX22:
+    case R_SPARC_TLS_LDO_LOX10:
+      // @dtpoff is a non-negative offset into the module's TLS block, so it
+      // uses the plain high/low split despite the HIX/LOX names. Local Exec
+      // makes it the negative @tpoff, which needs the complement encoding.
+      expr = ctx.arg.shared ? R_DTPREL : R_TPREL;
+      break;
+
     // TLS LE relocations:
     case R_SPARC_TLS_LE_HIX22:
     case R_SPARC_TLS_LE_LOX10:
@@ -394,11 +438,118 @@ void SPARCV9::relocate(uint8_t *loc, const Relocation &rel,
                        : 0x80100000 | (insn & 0x3e00001f));
     break;
   }
+  case R_SPARC_TLS_DTPMOD64:
+  case R_SPARC_TLS_DTPOFF64:
+  case R_SPARC_TLS_TPOFF64:
+    // V-xword64. A GOT slot the link resolves: the module index of the output
+    // module, or an offset within a module whose TLS block is known.
+    write64be(loc, val);
+    break;
+  case R_SPARC_TLS_GD_HI22: {
+    // T-imm22. Local Exec encodes the complement, as R_SPARC_TLS_LE_HIX22 does.
+    uint64_t v = rel.expr == R_TPREL ? ~val : val;
+    write32be(loc, (read32be(loc) & ~0x003fffff) | ((v >> 10) & 0x003fffff));
+    break;
+  }
+  case R_SPARC_TLS_GD_LO10:
+    if (rel.expr == R_TPREL)
+      // add %rs1, imm, %rd -> xor %rs1, imm, %rd, T-simm13.
+      write32be(loc, (read32be(loc) & ~0x00001fff) | 0x80182000 |
+                         (val & 0x000003ff) | 0x1c00);
+    else
+      // T-simm10
+      write32be(loc, (read32be(loc) & ~0x000003ff) | (val & 0x000003ff));
+    break;
+  case R_SPARC_TLS_GD_ADD:
+    if (rel.expr == R_GOT_OFF)
+      // Initial Exec: add %rs1, %rs2, %rd -> ldx [%rs1 + %rs2], %rd.
+      write32be(loc, (read32be(loc) & 0x3e07c01f) | 0xc0000000 | (0x0b << 19));
+    else if (rel.expr == R_TPREL)
+      // Local Exec: the GOT pointer becomes the thread pointer, %rs1 -> %g7.
+      write32be(loc, (read32be(loc) & ~0x0007c000) | (7 << 14));
+    break;
+  case R_SPARC_TLS_GD_CALL:
+    if (rel.expr == R_GOT_OFF)
+      write32be(loc, 0x9001c008); // add %g7, %o0, %o0
+    else if (rel.expr == R_TPREL)
+      write32be(loc, 0x01000000); // nop
+    else
+      // V-disp30, the call to __tls_get_addr.
+      write32be(loc, (read32be(loc) & ~0x3fffffff) | ((val >> 2) & 0x3fffffff));
+    break;
+  case R_SPARC_TLS_LDM_HI22:
+    if (rel.expr == R_TPREL)
+      write32be(loc, 0x01000000); // nop
+    else
+      // T-imm22
+      write32be(loc,
+                (read32be(loc) & ~0x003fffff) | ((val >> 10) & 0x003fffff));
+    break;
+  case R_SPARC_TLS_LDM_LO10:
+    if (rel.expr == R_TPREL)
+      write32be(loc, 0x01000000); // nop
+    else
+      // T-simm10
+      write32be(loc, (read32be(loc) & ~0x000003ff) | (val & 0x000003ff));
+    break;
+  case R_SPARC_TLS_LDM_ADD:
+    if (rel.expr == R_TPREL)
+      write32be(loc, 0x01000000); // nop
+    break;
+  case R_SPARC_TLS_LDM_CALL:
+    if (rel.expr == R_TPREL)
+      // Local Exec: the paired LDO add takes the thread pointer from %o0.
+      write32be(loc, 0x90100007); // mov %g7, %o0
+    else
+      // V-disp30, the call to __tls_get_addr.
+      write32be(loc, (read32be(loc) & ~0x3fffffff) | ((val >> 2) & 0x3fffffff));
+    break;
+  case R_SPARC_TLS_LDO_HIX22: {
+    // T-imm22
+    uint64_t v = rel.expr == R_TPREL ? ~val : val;
+    write32be(loc, (read32be(loc) & ~0x003fffff) | ((v >> 10) & 0x003fffff));
+    break;
+  }
+  case R_SPARC_TLS_LDO_LOX10:
+    // T-simm13. Only the negative @tpoff needs the sign extension bits.
+    write32be(loc, (read32be(loc) & ~0x00001fff) | (val & 0x000003ff) |
+                       (rel.expr == R_TPREL ? 0x1c00 : 0));
+    break;
   default:
     llvm_unreachable("unknown relocation");
   }
 }
 
+void SPARCV9::finalizeRelocScan() {
+  Symbol *tga = nullptr;
+
+  // R_SPARC_TLS_GD_CALL/LDM_CALL name the TLS symbol, not the callee. Rebind
+  // the calls that survived optimization (recorded as R_PLT_PC by
+  // scanSectionImpl) to __tls_get_addr. The symbol table cannot be reached
+  // from scanSectionImpl, which runs in parallel.
+  for (ELFFileBase *f : ctx.objectFiles) {
+    for (InputSectionBase *s : f->getSections()) {
+      auto *isec = dyn_cast_or_null<InputSection>(s);
+      if (!isec || !isec->isLive())
+        continue;
+      for (Relocation &rel : isec->relocs()) {
+        if (rel.expr != R_PLT_PC || (rel.type != R_SPARC_TLS_GD_CALL &&
+                                     rel.type != R_SPARC_TLS_LDM_CALL))
+          continue;
+        if (!tga) {
+          tga = ctx.symtab->addSymbol(Undefined{ctx.internalFile,
+                                                "__tls_get_addr", STB_GLOBAL,
+                                                STV_DEFAULT, STT_FUNC});
+          tga->isUsedInRegularObj = true;
+          tga->isPreemptible = true;
+          tga->setFlags(NEEDS_PLT | USED);
+        }
+        rel.sym = tga;
+      }
+    }
+  }
+}
+
 void SPARCV9::writeGotHeader(uint8_t *buf) const {
   // _GLOBAL_OFFSET_TABLE_[0] = _DYNAMIC
   write64be(buf, ctx.in.dynamic->getVA());
diff --git a/lld/test/ELF/sparcv9-tls-gd.s b/lld/test/ELF/sparcv9-tls-gd.s
new file mode 100644
index 0000000000000..ddde0a65c79b7
--- /dev/null
+++ b/lld/test/ELF/sparcv9-tls-gd.s
@@ -0,0 +1,87 @@
+# REQUIRES: sparc
+# RUN: rm -rf %t && split-file %s %t && cd %t
+# RUN: llvm-mc -filetype=obj -triple=sparcv9 a.s -o a.o
+# RUN: llvm-mc -filetype=obj -triple=sparcv9 b.s -o b.o
+# RUN: ld.lld -shared b.o -o b.so
+# RUN: ld.lld -shared a.o b.so -o a.so
+# RUN: llvm-readelf -r a.so | FileCheck %s --check-prefix=GD-REL
+# RUN: llvm-objdump -d -j .text --no-show-raw-insn --no-print-imm-hex a.so | FileCheck %s --check-prefix=GD
+
+## Each sequence gets a two-slot GOT entry holding the module index and the
+## offset within that module's TLS block. a0 is hidden, so its offset is known
+## at link time and only the module index needs a dynamic relocation. The call
+## goes through the PLT even though the relocation names the TLS symbol rather
+## than __tls_get_addr.
+# GD-REL:      Relocation section '.rela.dyn' {{.*}} contains 3 entries:
+# GD-REL:      R_SPARC_TLS_DTPMOD64 0{{$}}
+# GD-REL-DAG:  R_SPARC_TLS_DTPMOD64 {{.*}} b + 0
+# GD-REL-DAG:  R_SPARC_TLS_DTPOFF64 {{.*}} b + 0
+# GD-REL:      Relocation section '.rela.plt' {{.*}} contains 1 entries:
+# GD-REL:      R_SPARC_JMP_SLOT {{.*}} __tls_get_addr + 0
+
+# GD-LABEL:   <_start>:
+# GD-NEXT:      sethi 0, %o0
+# GD-NEXT:      add %o0, 8, %o0
+# GD-NEXT:      add %l7, %o0, %o0
+# GD-NEXT:      call
+# GD-NEXT:      nop
+# GD-NEXT:      sethi 0, %o1
+# GD-NEXT:      add %o1, 24, %o1
+# GD-NEXT:      add %l7, %o1, %o0
+# GD-NEXT:      call
+# GD-NEXT:      nop
+
+## In an executable a0 is not preemptible, so its sequence becomes Local Exec:
+## the sethi holds the complement of the offset, the add becomes an xor, the
+## GOT pointer becomes the thread pointer and the call is dropped. b is
+## preemptible, so its sequence becomes Initial Exec instead: the add becomes
+## the GOT load and the call becomes the thread-pointer add.
+# RUN: ld.lld a.o b.so -o a
+# RUN: llvm-readelf -r a | FileCheck %s --check-prefix=EXE-REL
+# RUN: llvm-objdump -d -j .text --no-show-raw-insn --no-print-imm-hex a | FileCheck %s --check-prefix=EXE
+
+# EXE-REL:     Relocation section '.rela.dyn' {{.*}} contains 1 entries:
+# EXE-REL:     R_SPARC_TLS_TPOFF64 {{.*}} b + 0
+# EXE-REL-NOT: R_SPARC_TLS_DTPMOD64
+# EXE-REL-NOT: __tls_get_addr
+
+# EXE-LABEL:   <_start>:
+# EXE-NEXT:      sethi 0, %o0
+# EXE-NEXT:      xor %o0, -8, %o0
+# EXE-NEXT:      add %g7, %o0, %o0
+# EXE-NEXT:      nop
+# EXE-NEXT:      nop
+# EXE-NEXT:      sethi 0, %o1
+# EXE-NEXT:      add %o1, 8, %o1
+# EXE-NEXT:      ldx [%l7+%o1], %o0
+# EXE-NEXT:      add %g7, %o0, %o0
+# EXE-NEXT:      nop
+
+#--- a.s
+.globl _start
+_start:
+## a0 is hidden, so it is not preemptible in an executable.
+  sethi %tgd_hi22(a0), %o0
+  add   %o0, %tgd_lo10(a0), %o0
+  add   %l7, %o0, %o0, %tgd_add(a0)
+  call  __tls_get_addr, %tgd_call(a0)
+   nop
+
+## b is defined in a DSO, so it stays preemptible in an executable.
+  sethi %tgd_hi22(b), %o1
+  add   %o1, %tgd_lo10(b), %o1
+  add   %l7, %o1, %o0, %tgd_add(b)
+  call  __tls_get_addr, %tgd_call(b)
+   nop
+
+.section .tbss,"awT", at nobits
+.globl a0
+.hidden a0
+a0:
+  .xword 0
+
+#--- b.s
+.section .tbss,"awT", at nobits
+.globl b
+b:
+  .xword 0
diff --git a/lld/test/ELF/sparcv9-tls-ld.s b/lld/test/ELF/sparcv9-tls-ld.s
new file mode 100644
index 0000000000000..843cb2d856cfa
--- /dev/null
+++ b/lld/test/ELF/sparcv9-tls-ld.s
@@ -0,0 +1,78 @@
+# REQUIRES: sparc
+# RUN: rm -rf %t && split-file %s %t && cd %t
+# RUN: llvm-mc -filetype=obj -triple=sparcv9 a.s -o a.o
+# RUN: ld.lld -shared a.o -o a.so
+# RUN: llvm-readelf -r a.so | FileCheck %s --check-prefix=LD-REL
+# RUN: llvm-objdump -d -j .text --no-show-raw-insn --no-print-imm-hex a.so | FileCheck %s --check-prefix=LD
+
+## Local Dynamic needs only the module index, so one GOT entry and one dynamic
+## relocation serve every symbol in the module. The per-symbol offsets are
+## link-time constants.
+# LD-REL:      Relocation section '.rela.dyn' {{.*}} contains 1 entries:
+# LD-REL:      R_SPARC_TLS_DTPMOD64 0{{$}}
+# LD-REL:      Relocation section '.rela.plt' {{.*}} contains 1 entries:
+# LD-REL:      R_SPARC_JMP_SLOT {{.*}} __tls_get_addr + 0
+
+# LD-LABEL:   <_start>:
+# LD-NEXT:      sethi 0, %o0
+# LD-NEXT:      add %o0, 8, %o0
+# LD-NEXT:      add %l7, %o0, %o0
+# LD-NEXT:      call
+# LD-NEXT:      nop
+## a0 is at offset 0 in the TLS block, a1 at 8.
+# LD-NEXT:      sethi 0, %o1
+# LD-NEXT:      xor %o1, 0, %o1
+# LD-NEXT:      add %o0, %o1, %o2
+# LD-NEXT:      sethi 0, %o3
+# LD-NEXT:      xor %o3, 8, %o3
+# LD-NEXT:      add %o0, %o3, %o4
+
+## In an executable the whole sequence becomes Local Exec: the module-index
+## setup is dropped, the call loads the thread pointer into %o0, and the
+## per-symbol offsets become the negative @tpoff, encoded as a complement.
+# RUN: ld.lld a.o -o a
+# RUN: llvm-readelf -r a | FileCheck %s --check-prefix=LE-REL
+# RUN: llvm-objdump -d -j .text --no-show-raw-insn --no-print-imm-hex a | FileCheck %s --check-prefix=LE
+
+# LE-REL-NOT:  R_SPARC_TLS
+# LE-REL-NOT:  __tls_get_addr
+
+# LE-LABEL:   <_start>:
+# LE-NEXT:      nop
+# LE-NEXT:      nop
+# LE-NEXT:      nop
+# LE-NEXT:      mov %g7, %o0
+# LE-NEXT:      nop
+## a0 - tp = -0x10, a1 - tp = -8.
+# LE-NEXT:      sethi 0, %o1
+# LE-NEXT:      xor %o1, -16, %o1
+# LE-NEXT:      add %o0, %o1, %o2
+# LE-NEXT:      sethi 0, %o3
+# LE-NEXT:      xor %o3, -8, %o3
+# LE-NEXT:      add %o0, %o3, %o4
+
+#--- a.s
+.globl _start
+_start:
+  sethi %tldm_hi22(a0), %o0
+  add   %o0, %tldm_lo10(a0), %o0
+  add   %l7, %o0, %o0, %tldm_add(a0)
+  call  __tls_get_addr, %tldm_call(a0)
+   nop
+
+  sethi %tldo_hix22(a0), %o1
+  xor   %o1, %tldo_lox10(a0), %o1
+  add   %o0, %o1, %o2, %tldo_add(a0)
+
+  sethi %tldo_hix22(a1), %o3
+  xor   %o3, %tldo_lox10(a1), %o3
+  add   %o0, %o3, %o4, %tldo_add(a1)
+
+.section .tbss,"awT", at nobits
+.globl a0, a1
+.hidden a0
+.hidden a1
+a0:
+  .xword 0
+a1:
+  .xword 0



More information about the llvm-commits mailing list