[lld] [ELF,SPARC] Handle TLS IE relocations (PR #213500)

Fangrui Song via llvm-commits llvm-commits at lists.llvm.org
Sat Aug 1 19:23:11 PDT 2026


https://github.com/MaskRay created https://github.com/llvm/llvm-project/pull/213500

An initial-exec reference loads the symbol's TP-relative offset from the
GOT and adds the thread pointer:

```
sethi %tie_hi22(sym), %o0             # R_SPARC_TLS_IE_HI22
add   %o0, %tie_lo10(sym), %o0        # R_SPARC_TLS_IE_LO10
ldx   [%l7 + %o0], %o0, %tie_ldx(sym) # R_SPARC_TLS_IE_LDX
add   %g7, %o0, %o0, %tie_add(sym)    # R_SPARC_TLS_IE_ADD
```

The sethi and add form the GOT offset. In an executable a non-preemptible
symbol is optimized to Local Exec, as GNU ld does: the sethi holds the
complement, the add becomes an xor, and the load becomes a register move,
or a nop when source and destination are the same. The thread pointer add
is unchanged.

Co-authored-by: Kirill A. Korinsky <kirill at korins.ky>
Co-authored-by: LemonBoy <thatlemon at gmail.com>
Co-authored-by: Alex Rønne Petersen <alex at alexrp.com>


>From 073016f02e1106ffe255254c77579e675b2f77dc Mon Sep 17 00:00:00 2001
From: Fangrui Song <i at maskray.me>
Date: Sat, 1 Aug 2026 18:31:07 -0700
Subject: [PATCH] [ELF,SPARC] Handle TLS IE relocations
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

An initial-exec reference loads the symbol's TP-relative offset from the
GOT and adds the thread pointer:

```
sethi %tie_hi22(sym), %o0             # R_SPARC_TLS_IE_HI22
add   %o0, %tie_lo10(sym), %o0        # R_SPARC_TLS_IE_LO10
ldx   [%l7 + %o0], %o0, %tie_ldx(sym) # R_SPARC_TLS_IE_LDX
add   %g7, %o0, %o0, %tie_add(sym)    # R_SPARC_TLS_IE_ADD
```

The sethi and add form the GOT offset. In an executable a non-preemptible
symbol is optimized to Local Exec, as GNU ld does: the sethi holds the
complement, the add becomes an xor, and the load becomes a register move,
or a nop when source and destination are the same. The thread pointer add
is unchanged.

Co-authored-by: Kirill A. Korinsky <kirill at korins.ky>
Co-authored-by: LemonBoy <thatlemon at gmail.com>
Co-authored-by: Alex Rønne Petersen <alex at alexrp.com>
---
 lld/ELF/Arch/SPARCV9.cpp      | 41 +++++++++++++++
 lld/test/ELF/sparcv9-tls-ie.s | 98 +++++++++++++++++++++++++++++++++++
 2 files changed, 139 insertions(+)
 create mode 100644 lld/test/ELF/sparcv9-tls-ie.s

diff --git a/lld/ELF/Arch/SPARCV9.cpp b/lld/ELF/Arch/SPARCV9.cpp
index c35208f027e4e..7f24d8176594c 100644
--- a/lld/ELF/Arch/SPARCV9.cpp
+++ b/lld/ELF/Arch/SPARCV9.cpp
@@ -47,6 +47,7 @@ SPARCV9::SPARCV9(Ctx &ctx) : TargetInfo(ctx) {
   pltRel = R_SPARC_JMP_SLOT;
   relativeRel = R_SPARC_RELATIVE;
   symbolicRel = R_SPARC_64;
+  tlsGotRel = R_SPARC_TLS_TPOFF64;
   gotHeaderEntriesNum = 1;
   pltEntrySize = 32;
   pltHeaderSize = 4 * pltEntrySize;
@@ -188,6 +189,21 @@ void SPARCV9::scanSectionImpl(InputSectionBase &sec, Relocs<RelTy> rels,
       expr = R_TPREL;
       break;
 
+    // TLS IE relocations. In an executable, a non-preemptible symbol is
+    // optimized to Local Exec: the add becomes an xor and the load becomes a
+    // register move.
+    case R_SPARC_TLS_IE_HI22:
+    case R_SPARC_TLS_IE_LO10:
+      rs.handleTlsIe(R_GOT_OFF, type, offset, addend, sym);
+      continue;
+    case R_SPARC_TLS_IE_LD:
+    case R_SPARC_TLS_IE_LDX:
+      if (!ctx.arg.shared && !sym.isPreemptible)
+        sec.addReloc({R_TPREL, type, offset, addend, &sym});
+      continue;
+    case R_SPARC_TLS_IE_ADD:
+      continue;
+
     default:
       Err(ctx) << getErrorLoc(ctx, sec.content().data() + offset)
                << "unknown relocation (" << type.v << ") against symbol "
@@ -354,6 +370,31 @@ void SPARCV9::relocate(uint8_t *loc, const Relocation &rel,
     if (rel.expr == R_GOTREL)
       write32be(loc, (read32be(loc) & 0x3e07c01f) | 0x80000000);
     break;
+  case R_SPARC_TLS_IE_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_IE_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_IE_LD:
+  case R_SPARC_TLS_IE_LDX: {
+    // ld/ldx [%rs1 + %rs2], %rd -> mov %rs2, %rd, or nop if the move is
+    // redundant. Only reached when the sequence is optimized to Local Exec.
+    uint32_t insn = read32be(loc);
+    write32be(loc, ((insn >> 25) & 0x1f) == (insn & 0x1f)
+                       ? 0x01000000
+                       : 0x80100000 | (insn & 0x3e00001f));
+    break;
+  }
   default:
     llvm_unreachable("unknown relocation");
   }
diff --git a/lld/test/ELF/sparcv9-tls-ie.s b/lld/test/ELF/sparcv9-tls-ie.s
new file mode 100644
index 0000000000000..74fb93165577d
--- /dev/null
+++ b/lld/test/ELF/sparcv9-tls-ie.s
@@ -0,0 +1,98 @@
+# 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 -S -r -d a.so | FileCheck %s --check-prefix=IE-REL
+# RUN: llvm-objdump -d -j .text --no-show-raw-insn --no-print-imm-hex a.so | FileCheck %s --check-prefix=IE
+
+## a0 is hidden, so its offset in the TLS block, 0x10010, is known and the
+## dynamic relocation needs no symbol.
+# IE-REL:      [ 9] .got PROGBITS 00000000002003f8 {{[0-9a-f]+}} 000020
+# IE-REL:      (FLAGS) STATIC_TLS
+# IE-REL:      Relocation section '.rela.dyn' {{.*}} contains 3 entries:
+# IE-REL:      0000000000200400 {{[0-9a-f]+}} R_SPARC_TLS_TPOFF64 10010
+# IE-REL-NEXT: 0000000000200410 {{[0-9a-f]+}} R_SPARC_TLS_TPOFF64 {{.*}} b + 0
+# IE-REL-NEXT: 0000000000200408 {{[0-9a-f]+}} R_SPARC_TLS_TPOFF64 {{.*}} a1 + 0
+
+## .got[1] - _GLOBAL_OFFSET_TABLE_ = 0x200400 - 0x2003f8 = 8, then 16 and 24.
+# IE-LABEL:   <_start>:
+# IE-NEXT:      sethi 0, %o0
+# IE-NEXT:      add %o0, 8, %o0
+# IE-NEXT:      ldx [%l7+%o0], %o0
+# IE-NEXT:      add %g7, %o0, %o0
+# IE-NEXT:      sethi 0, %o1
+# IE-NEXT:      add %o1, 16, %o1
+# IE-NEXT:      ld [%l7+%o1], %o2
+# IE-NEXT:      add %g7, %o2, %o2
+# IE-NEXT:      sethi 0, %o3
+# IE-NEXT:      add %o3, 24, %o3
+# IE-NEXT:      ldx [%l7+%o3], %o4
+# IE-NEXT:      add %g7, %o4, %o4
+
+## The add becomes an xor over the complement the sethi holds, and the load
+## becomes a register move, or a nop where it would move a register onto
+## itself. b is preemptible, so only its sequence keeps the GOT load: .got
+## holds the header and one entry, with one dynamic relocation.
+# RUN: ld.lld a.o b.so -o a
+# RUN: llvm-readelf -S -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:      [ 9] .got PROGBITS 0000000000300378 {{[0-9a-f]+}} 000010
+# LE-REL:      Relocation section '.rela.dyn' {{.*}} contains 1 entries:
+# LE-REL:      0000000000300380 {{[0-9a-f]+}} R_SPARC_TLS_TPOFF64 {{.*}} b + 0
+
+## a0 - tp = -0x20008, a1 - tp = -0x30010.
+# LE-LABEL:   <_start>:
+# LE-NEXT:      sethi 128, %o0
+# LE-NEXT:      xor %o0, -8, %o0
+# LE-NEXT:      nop
+# LE-NEXT:      add %g7, %o0, %o0
+# LE-NEXT:      sethi 192, %o1
+# LE-NEXT:      xor %o1, -16, %o1
+# LE-NEXT:      mov %o1, %o2
+# LE-NEXT:      add %g7, %o2, %o2
+# LE-NEXT:      sethi 0, %o3
+# LE-NEXT:      add %o3, 8, %o3
+# LE-NEXT:      ldx [%l7+%o3], %o4
+# LE-NEXT:      add %g7, %o4, %o4
+
+#--- a.s
+.globl _start
+_start:
+  sethi %tie_hi22(a0), %o0
+  add   %o0, %tie_lo10(a0), %o0
+  ldx   [%l7 + %o0], %o0, %tie_ldx(a0)
+  add   %g7, %o0, %o0, %tie_add(a0)
+
+## ld is the 32-bit load, handled like ldx.
+  sethi %tie_hi22(a1), %o1
+  add   %o1, %tie_lo10(a1), %o1
+  ld    [%l7 + %o1], %o2, %tie_ld(a1)
+  add   %g7, %o2, %o2, %tie_add(a1)
+
+## b is defined in a DSO, so it stays preemptible in an executable.
+  sethi %tie_hi22(b), %o3
+  add   %o3, %tie_lo10(b), %o3
+  ldx   [%l7 + %o3], %o4, %tie_ldx(b)
+  add   %g7, %o4, %o4, %tie_add(b)
+
+.section .tbss,"awT", at nobits
+.globl a0, a1
+.hidden a0
+.space 8
+a1:
+  .xword 0
+## Pad so that a0 and a1 are far from the thread pointer, and far enough apart,
+## that their sethi fields are non-zero and differ.
+.space 0x10000
+a0:
+  .xword 0
+.space 0x20000
+
+#--- b.s
+.section .tbss,"awT", at nobits
+.globl b
+b:
+  .xword 0



More information about the llvm-commits mailing list