[lld] [ELF,SPARC] Accept EM_SPARC32PLUS inputs on the 32-bit SPARC target (PR #228201)

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


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

EM_SPARC32PLUS marks an object holding V9 instructions under the
32-bit ABI, so it links with plain EM_SPARC objects and needs no
relocation handling of its own. Rejecting it outright also rejected
the 32-bit libgcc and libatomic distributions ship.

Take it as input, and OR the widest instruction-set bits any
EM_SPARC32PLUS object asks for into the output, as elf32_sparc does:
EF_SPARC_32PLUS, then SUN_US1, then SUN_US3 (implies US1); HAL_R1
selects nothing. gas sets SUN_US1 per UltraSPARC I instruction, not
per -mcpu, so a static link needs the bits OR'd in from such a
library.

The machine is normalized to EM_SPARC during the link, promoted on
write. EM_SPARCV9 and elf32_sparc's other malformed shapes — wrong
class/byte order, or missing EF_SPARC_32PLUS — are rejected.

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


>From 79e933d51b6b1c1b32c8613f884f560bc985c105 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 1/3] [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

>From 183b23b6de8e91e7786146b90fd7e2284e948102 Mon Sep 17 00:00:00 2001
From: Imre Kaloz <kaloz at kernel.org>
Date: Tue, 1 Sep 2026 23:40:59 +0200
Subject: [PATCH 2/3] [ELF,SPARC] Add the 32-bit SPARC target

The 32-bit psABI (EM_SPARC, elf32_sparc) shares its relocation set with
V9, so parametrize the existing target on ABI width rather than
duplicating it. Width-dependent parts: the absolute and TLS-GOT
relocation types, the PLT entry, and the register width a relaxed
initial exec load uses (ld, not ldx).

The PLT entry is three words where V9 uses eight, branching to .PLT0
rather than .PLT1:

    sethi (. - .PLT0), %g1
    ba,a  .PLT0
    nop

A relocated value reaches the target sign-extended to the ABI width,
so a 32-bit address with bit 31 set arrives with every high bit set;
unsigned range checks would take the masked-off bits as an overflow.

Both ABIs share GNU ld's 8K common page size; elf32_sparc's maximum
page size is 64K.

Signed-off-by: Imre Kaloz <kaloz at kernel.org>
---
 lld/ELF/Arch/{SPARCV9.cpp => SPARC.cpp} | 135 +++++++++++++++++-------
 lld/ELF/CMakeLists.txt                  |   2 +-
 lld/ELF/Driver.cpp                      |  16 ++-
 lld/ELF/InputFiles.cpp                  |   3 +
 lld/ELF/InputSection.cpp                |   1 +
 lld/ELF/ScriptParser.cpp                |   1 +
 lld/ELF/SyntheticSections.cpp           |   3 +-
 lld/ELF/Target.cpp                      |   2 +
 lld/ELF/Target.h                        |   1 +
 lld/test/ELF/Inputs/sparc-elf64.yaml    |   6 ++
 lld/test/ELF/sparc32-plt.s              |  36 +++++++
 lld/test/ELF/sparc32-reloc.s            |  28 +++++
 lld/test/ELF/sparc32-tls.s              |  66 ++++++++++++
 13 files changed, 254 insertions(+), 46 deletions(-)
 rename lld/ELF/Arch/{SPARCV9.cpp => SPARC.cpp} (82%)
 create mode 100644 lld/test/ELF/Inputs/sparc-elf64.yaml
 create mode 100644 lld/test/ELF/sparc32-plt.s
 create mode 100644 lld/test/ELF/sparc32-reloc.s
 create mode 100644 lld/test/ELF/sparc32-tls.s

diff --git a/lld/ELF/Arch/SPARCV9.cpp b/lld/ELF/Arch/SPARC.cpp
similarity index 82%
rename from lld/ELF/Arch/SPARCV9.cpp
rename to lld/ELF/Arch/SPARC.cpp
index d777daea7bfd5..8270265d9a4d9 100644
--- a/lld/ELF/Arch/SPARCV9.cpp
+++ b/lld/ELF/Arch/SPARC.cpp
@@ -22,9 +22,13 @@ using namespace lld;
 using namespace lld::elf;
 
 namespace {
-class SPARCV9 final : public TargetInfo {
+// Handles both the 64-bit (V9, EM_SPARCV9) and the 32-bit (V8, EM_SPARC)
+// psABIs, which share their relocation set. The width-dependent parts are the
+// absolute and TLS-GOT relocation types, the PLT entry, and the register width
+// a relaxed TLS load uses.
+class SPARC final : public TargetInfo {
 public:
-  SPARCV9(Ctx &);
+  SPARC(Ctx &, bool is64);
   RelExpr getRelExpr(RelType type, const Symbol &s,
                      const uint8_t *loc) const override;
   RelType getDynRel(RelType type) const override;
@@ -37,36 +41,53 @@ class SPARCV9 final : public TargetInfo {
   void scanSectionImpl(InputSectionBase &sec, Relocs<RelTy> rels,
                        unsigned shard);
   void scanSection(InputSectionBase &sec, unsigned shard) override {
-    elf::scanSection1<SPARCV9, ELF64BE>(*this, sec, shard);
+    if (is64)
+      elf::scanSection1<SPARC, ELF64BE>(*this, sec, shard);
+    else
+      elf::scanSection1<SPARC, ELF32BE>(*this, sec, shard);
   }
   void relocate(uint8_t *loc, const Relocation &rel,
                 uint64_t val) const override;
+
+private:
+  bool is64;
 };
 } // namespace
 
-SPARCV9::SPARCV9(Ctx &ctx) : TargetInfo(ctx) {
+SPARC::SPARC(Ctx &ctx, bool is64) : TargetInfo(ctx), is64(is64) {
   copyRel = R_SPARC_COPY;
   gotRel = R_SPARC_GLOB_DAT;
   pltRel = R_SPARC_JMP_SLOT;
   relativeRel = R_SPARC_RELATIVE;
-  symbolicRel = R_SPARC_64;
-  tlsGotRel = R_SPARC_TLS_TPOFF64;
-  tlsModuleIndexRel = R_SPARC_TLS_DTPMOD64;
-  tlsOffsetRel = R_SPARC_TLS_DTPOFF64;
+  if (is64) {
+    symbolicRel = R_SPARC_64;
+    tlsGotRel = R_SPARC_TLS_TPOFF64;
+    tlsModuleIndexRel = R_SPARC_TLS_DTPMOD64;
+    tlsOffsetRel = R_SPARC_TLS_DTPOFF64;
+    pltEntrySize = 32;
+    defaultMaxPageSize = 0x100000;
+    defaultImageBase = 0x100000;
+  } else {
+    symbolicRel = R_SPARC_32;
+    tlsGotRel = R_SPARC_TLS_TPOFF32;
+    tlsModuleIndexRel = R_SPARC_TLS_DTPMOD32;
+    tlsOffsetRel = R_SPARC_TLS_DTPOFF32;
+    pltEntrySize = 12;
+    defaultMaxPageSize = 0x10000;
+    defaultImageBase = 0x10000;
+  }
   gotHeaderEntriesNum = 1;
-  pltEntrySize = 32;
   pltHeaderSize = 4 * pltEntrySize;
   usesGotPlt = false;
 
+  // Both ABIs share GNU ld's 8K common page size.
   defaultCommonPageSize = 8192;
-  defaultMaxPageSize = 0x100000;
-  defaultImageBase = 0x100000;
 }
 
 // Only needed to support relocations used by relocateNonAlloc and
 // preprocessRelocs.
-RelExpr SPARCV9::getRelExpr(RelType type, const Symbol &s,
-                            const uint8_t *loc) const {
+RelExpr SPARC::getRelExpr(RelType type, const Symbol &s,
+                          const uint8_t *loc) const {
   switch (type) {
   case R_SPARC_8:
   case R_SPARC_16:
@@ -87,13 +108,13 @@ RelExpr SPARCV9::getRelExpr(RelType type, const Symbol &s,
   }
 }
 
-RelType SPARCV9::getDynRel(RelType type) const {
-  if (type == R_SPARC_64)
+RelType SPARC::getDynRel(RelType type) const {
+  if (type == symbolicRel)
     return type;
   return R_SPARC_NONE;
 }
 
-int64_t SPARCV9::getImplicitAddend(const uint8_t *buf, RelType type) const {
+int64_t SPARC::getImplicitAddend(const uint8_t *buf, RelType type) const {
   switch (type) {
   case R_SPARC_64:
   case R_SPARC_GLOB_DAT:
@@ -105,8 +126,8 @@ int64_t SPARCV9::getImplicitAddend(const uint8_t *buf, RelType type) const {
 }
 
 template <class ELFT, class RelTy>
-void SPARCV9::scanSectionImpl(InputSectionBase &sec, Relocs<RelTy> rels,
-                              unsigned shard) {
+void SPARC::scanSectionImpl(InputSectionBase &sec, Relocs<RelTy> rels,
+                            unsigned shard) {
   RelocScan rs(ctx, &sec, shard);
   sec.relocations.reserve(rels.size());
   for (auto it = rels.begin(); it != rels.end(); ++it) {
@@ -257,8 +278,12 @@ void SPARCV9::scanSectionImpl(InputSectionBase &sec, Relocs<RelTy> rels,
   }
 }
 
-void SPARCV9::relocate(uint8_t *loc, const Relocation &rel,
-                       uint64_t val) const {
+void SPARC::relocate(uint8_t *loc, const Relocation &rel, uint64_t val) const {
+  // relocateImpl sign-extends to the ABI width, so on the 32-bit ABI a value
+  // with bit 31 set arrives with every high bit set. The field writes mask
+  // those away, but the unsigned range checks would read them as an overflow.
+  const uint64_t uval = is64 ? val : uint32_t(val);
+
   switch (rel.type) {
   case R_SPARC_8:
     // V-byte8
@@ -274,7 +299,7 @@ void SPARCV9::relocate(uint8_t *loc, const Relocation &rel,
   case R_SPARC_32:
   case R_SPARC_UA32:
     // V-word32
-    checkUInt(ctx, loc, val, 32, rel);
+    checkUInt(ctx, loc, uval, 32, rel);
     write32be(loc, val);
     break;
   case R_SPARC_DISP8:
@@ -325,7 +350,7 @@ void SPARCV9::relocate(uint8_t *loc, const Relocation &rel,
     break;
   case R_SPARC_HI22:
     // V-imm22
-    checkUInt(ctx, loc, val, 32, rel);
+    checkUInt(ctx, loc, uval, 32, rel);
     write32be(loc, (read32be(loc) & ~0x003fffff) | ((val >> 10) & 0x003fffff));
     break;
   case R_SPARC_WDISP22:
@@ -445,6 +470,12 @@ void SPARCV9::relocate(uint8_t *loc, const Relocation &rel,
     // module, or an offset within a module whose TLS block is known.
     write64be(loc, val);
     break;
+  case R_SPARC_TLS_DTPMOD32:
+  case R_SPARC_TLS_DTPOFF32:
+  case R_SPARC_TLS_TPOFF32:
+    // V-word32, the 32-bit ABI's GOT slots.
+    write32be(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;
@@ -462,8 +493,9 @@ void SPARCV9::relocate(uint8_t *loc, const Relocation &rel,
     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));
+      // Initial Exec: add %rs1, %rs2, %rd -> ld/ldx [%rs1 + %rs2], %rd.
+      write32be(loc, (read32be(loc) & 0x3e07c01f) | 0xc0000000 |
+                         (is64 ? (0x0b << 19) : 0));
     else if (rel.expr == R_TPREL)
       // Local Exec: the GOT pointer becomes the thread pointer, %rs1 -> %g7.
       write32be(loc, (read32be(loc) & ~0x0007c000) | (7 << 14));
@@ -520,7 +552,7 @@ void SPARCV9::relocate(uint8_t *loc, const Relocation &rel,
   }
 }
 
-void SPARCV9::finalizeRelocScan() {
+void SPARC::finalizeRelocScan() {
   Symbol *tga = nullptr;
 
   // R_SPARC_TLS_GD_CALL/LDM_CALL name the TLS symbol, not the callee. Rebind
@@ -550,28 +582,51 @@ void SPARCV9::finalizeRelocScan() {
   }
 }
 
-void SPARCV9::writeGotHeader(uint8_t *buf) const {
+void SPARC::writeGotHeader(uint8_t *buf) const {
   // _GLOBAL_OFFSET_TABLE_[0] = _DYNAMIC
-  write64be(buf, ctx.in.dynamic->getVA());
+  if (is64)
+    write64be(buf, ctx.in.dynamic->getVA());
+  else
+    write32be(buf, ctx.in.dynamic->getVA());
 }
 
-void SPARCV9::writePlt(uint8_t *buf, const Symbol & /*sym*/,
-                       uint64_t pltEntryAddr) const {
+void SPARC::writePlt(uint8_t *buf, const Symbol & /*sym*/,
+                     uint64_t pltEntryAddr) const {
+  uint64_t off = pltEntryAddr - ctx.in.plt->getVA();
+  if (is64) {
+    const uint8_t pltData[] = {
+        0x03, 0x00, 0x00, 0x00, // sethi   (. - .PLT0), %g1
+        0x30, 0x68, 0x00, 0x00, // ba,a    %xcc, .PLT1
+        0x01, 0x00, 0x00, 0x00, // nop
+        0x01, 0x00, 0x00, 0x00, // nop
+        0x01, 0x00, 0x00, 0x00, // nop
+        0x01, 0x00, 0x00, 0x00, // nop
+        0x01, 0x00, 0x00, 0x00, // nop
+        0x01, 0x00, 0x00, 0x00  // nop
+    };
+    memcpy(buf, pltData, sizeof(pltData));
+    relocateNoSym(buf, R_SPARC_22, off);
+    relocateNoSym(buf + 4, R_SPARC_WDISP19, -(off + 4 - pltEntrySize));
+    return;
+  }
+
+  // The 32-bit entry is three words and branches to .PLT0, where the 64-bit
+  // one branches to .PLT1. The sethi carries the entry's own offset, which is
+  // how the resolver recovers the relocation index.
   const uint8_t pltData[] = {
-      0x03, 0x00, 0x00, 0x00, // sethi   (. - .PLT0), %g1
-      0x30, 0x68, 0x00, 0x00, // ba,a    %xcc, .PLT1
-      0x01, 0x00, 0x00, 0x00, // nop
-      0x01, 0x00, 0x00, 0x00, // nop
-      0x01, 0x00, 0x00, 0x00, // nop
-      0x01, 0x00, 0x00, 0x00, // nop
-      0x01, 0x00, 0x00, 0x00, // nop
+      0x03, 0x00, 0x00, 0x00, // sethi (. - .PLT0), %g1
+      0x30, 0x80, 0x00, 0x00, // ba,a  .PLT0
       0x01, 0x00, 0x00, 0x00  // nop
   };
   memcpy(buf, pltData, sizeof(pltData));
-
-  uint64_t off = pltEntryAddr - ctx.in.plt->getVA();
   relocateNoSym(buf, R_SPARC_22, off);
-  relocateNoSym(buf + 4, R_SPARC_WDISP19, -(off + 4 - pltEntrySize));
+  relocateNoSym(buf + 4, R_SPARC_WDISP22, -(off + 4));
 }
 
-void elf::setSPARCV9TargetInfo(Ctx &ctx) { ctx.target.reset(new SPARCV9(ctx)); }
+void elf::setSPARCV9TargetInfo(Ctx &ctx) {
+  ctx.target.reset(new SPARC(ctx, /*is64=*/true));
+}
+
+void elf::setSPARC32TargetInfo(Ctx &ctx) {
+  ctx.target.reset(new SPARC(ctx, /*is64=*/false));
+}
diff --git a/lld/ELF/CMakeLists.txt b/lld/ELF/CMakeLists.txt
index e22897c2789d8..0e9983d41a59f 100644
--- a/lld/ELF/CMakeLists.txt
+++ b/lld/ELF/CMakeLists.txt
@@ -32,7 +32,7 @@ add_lld_library(lldELF
   Arch/PPC.cpp
   Arch/PPC64.cpp
   Arch/RISCV.cpp
-  Arch/SPARCV9.cpp
+  Arch/SPARC.cpp
   Arch/SystemZ.cpp
   Arch/X86.cpp
   Arch/X86_64.cpp
diff --git a/lld/ELF/Driver.cpp b/lld/ELF/Driver.cpp
index 4e5a5eb8664d7..f0d47fd5e8706 100644
--- a/lld/ELF/Driver.cpp
+++ b/lld/ELF/Driver.cpp
@@ -173,6 +173,7 @@ static std::tuple<ELFKind, uint16_t, uint8_t> parseEmulation(Ctx &ctx,
           .Cases({"elf_amd64", "elf_x86_64"}, {ELF64LEKind, EM_X86_64})
           .Case("elf_i386", {ELF32LEKind, EM_386})
           .Case("elf_iamcu", {ELF32LEKind, EM_IAMCU})
+          .Case("elf32_sparc", {ELF32BEKind, EM_SPARC})
           .Case("elf64_sparc", {ELF64BEKind, EM_SPARCV9})
           .Case("msp430elf", {ELF32LEKind, EM_MSP430})
           .Case("elf64_amdgpu", {ELF64LEKind, EM_AMDGPU})
@@ -1345,10 +1346,10 @@ static SmallVector<StringRef, 0> getSymbolOrderingFile(Ctx &ctx,
 
 static bool getIsRela(Ctx &ctx, opt::InputArgList &args) {
   // The psABI specifies the default relocation entry format.
-  bool rela =
-      is_contained({EM_AARCH64, EM_AMDGPU, EM_HEXAGON, EM_LOONGARCH, EM_PPC,
-                    EM_PPC64, EM_RISCV, EM_S390, EM_SPARCV9, EM_X86_64},
-                   ctx.arg.emachine);
+  bool rela = is_contained({EM_AARCH64, EM_AMDGPU, EM_HEXAGON, EM_LOONGARCH,
+                            EM_PPC, EM_PPC64, EM_RISCV, EM_S390, EM_SPARC,
+                            EM_SPARCV9, EM_X86_64},
+                           ctx.arg.emachine);
   // If -z rel or -z rela is specified, use the last option.
   for (auto *arg : args.filtered(OPT_z)) {
     StringRef s(arg->getValue());
@@ -2359,6 +2360,13 @@ void LinkerDriver::inferMachineType() {
     if (f->ekind == ELFNoneKind)
       continue;
     if (!inferred) {
+      // EM_SPARC names the 32-bit big-endian ABI, so an object claiming it
+      // with another class or byte order is malformed. A later object is
+      // caught by the ELF kind check in isCompatible.
+      if (f->emachine == EM_SPARC && f->ekind != ELF32BEKind) {
+        Err(ctx) << f.get() << " is incompatible";
+        return;
+      }
       inferred = true;
       ctx.arg.ekind = f->ekind;
       ctx.arg.emachine = f->emachine;
diff --git a/lld/ELF/InputFiles.cpp b/lld/ELF/InputFiles.cpp
index 4af0898cef607..9a919c4c3d5ef 100644
--- a/lld/ELF/InputFiles.cpp
+++ b/lld/ELF/InputFiles.cpp
@@ -1787,6 +1787,9 @@ static uint16_t getBitcodeMachineKind(Ctx &ctx, StringRef path,
   case Triple::riscv32:
   case Triple::riscv64:
     return EM_RISCV;
+  case Triple::sparc:
+  case Triple::sparcel:
+    return EM_SPARC;
   case Triple::sparcv9:
     return EM_SPARCV9;
   case Triple::systemz:
diff --git a/lld/ELF/InputSection.cpp b/lld/ELF/InputSection.cpp
index 13baaa5b8c2ad..a088982e179c4 100644
--- a/lld/ELF/InputSection.cpp
+++ b/lld/ELF/InputSection.cpp
@@ -795,6 +795,7 @@ static int64_t getTlsTpOffset(Ctx &ctx, const Symbol &s) {
     // Variant 2.
   case EM_HEXAGON:
   case EM_S390:
+  case EM_SPARC:
   case EM_SPARCV9:
   case EM_386:
   case EM_X86_64:
diff --git a/lld/ELF/ScriptParser.cpp b/lld/ELF/ScriptParser.cpp
index 0c54fde488292..5b2a7beb22331 100644
--- a/lld/ELF/ScriptParser.cpp
+++ b/lld/ELF/ScriptParser.cpp
@@ -482,6 +482,7 @@ static std::pair<ELFKind, uint16_t> parseBfdName(StringRef s) {
       .Case("elf64-tradlittlemips", {ELF64LEKind, EM_MIPS})
       .Case("elf32-littleriscv", {ELF32LEKind, EM_RISCV})
       .Case("elf64-littleriscv", {ELF64LEKind, EM_RISCV})
+      .Case("elf32-sparc", {ELF32BEKind, EM_SPARC})
       .Case("elf64-sparc", {ELF64BEKind, EM_SPARCV9})
       .Case("elf32-msp430", {ELF32LEKind, EM_MSP430})
       .Case("elf32-loongarch", {ELF32LEKind, EM_LOONGARCH})
diff --git a/lld/ELF/SyntheticSections.cpp b/lld/ELF/SyntheticSections.cpp
index f59e676c2e6f5..392d490083cd6 100644
--- a/lld/ELF/SyntheticSections.cpp
+++ b/lld/ELF/SyntheticSections.cpp
@@ -1306,6 +1306,7 @@ DynamicSection<ELFT>::computeContents() {
     case EM_S390:
       addInSec(DT_PLTGOT, *ctx.in.got);
       break;
+    case EM_SPARC:
     case EM_SPARCV9:
       addInSec(DT_PLTGOT, *ctx.in.plt);
       break;
@@ -2451,7 +2452,7 @@ PltSection::PltSection(Ctx &ctx)
 
   // The PLT needs to be writable on SPARC as the dynamic linker will
   // modify the instructions in the PLT entries.
-  if (ctx.arg.emachine == EM_SPARCV9)
+  if (ctx.arg.emachine == EM_SPARC || ctx.arg.emachine == EM_SPARCV9)
     this->flags |= SHF_WRITE;
 }
 
diff --git a/lld/ELF/Target.cpp b/lld/ELF/Target.cpp
index 89e4dbeed3109..c362762186d79 100644
--- a/lld/ELF/Target.cpp
+++ b/lld/ELF/Target.cpp
@@ -77,6 +77,8 @@ void elf::setTarget(Ctx &ctx) {
     return setPPC64TargetInfo(ctx);
   case EM_RISCV:
     return setRISCVTargetInfo(ctx);
+  case EM_SPARC:
+    return setSPARC32TargetInfo(ctx);
   case EM_SPARCV9:
     return setSPARCV9TargetInfo(ctx);
   case EM_S390:
diff --git a/lld/ELF/Target.h b/lld/ELF/Target.h
index 174e389db155d..ef867fef7cd51 100644
--- a/lld/ELF/Target.h
+++ b/lld/ELF/Target.h
@@ -221,6 +221,7 @@ void setMipsTargetInfo(Ctx &);
 void setPPC64TargetInfo(Ctx &);
 void setPPCTargetInfo(Ctx &);
 void setRISCVTargetInfo(Ctx &);
+void setSPARC32TargetInfo(Ctx &);
 void setSPARCV9TargetInfo(Ctx &);
 void setSystemZTargetInfo(Ctx &);
 void setX86TargetInfo(Ctx &);
diff --git a/lld/test/ELF/Inputs/sparc-elf64.yaml b/lld/test/ELF/Inputs/sparc-elf64.yaml
new file mode 100644
index 0000000000000..737d58b40eaaf
--- /dev/null
+++ b/lld/test/ELF/Inputs/sparc-elf64.yaml
@@ -0,0 +1,6 @@
+--- !ELF
+FileHeader:
+  Class:   ELFCLASS64
+  Data:    ELFDATA2MSB
+  Type:    ET_REL
+  Machine: EM_SPARC
diff --git a/lld/test/ELF/sparc32-plt.s b/lld/test/ELF/sparc32-plt.s
new file mode 100644
index 0000000000000..657716d0d3dc9
--- /dev/null
+++ b/lld/test/ELF/sparc32-plt.s
@@ -0,0 +1,36 @@
+# REQUIRES: sparc
+# RUN: rm -rf %t && split-file %s %t && cd %t
+# RUN: llvm-mc -filetype=obj -triple=sparc a.s -o a.o
+# RUN: llvm-mc -filetype=obj -triple=sparc b.s -o b.o
+# RUN: ld.lld -shared b.o -soname=b.so -o b.so
+# RUN: ld.lld a.o b.so -o a
+# RUN: llvm-readelf -S -r a | FileCheck %s
+# RUN: llvm-objdump -d -j .plt --no-show-raw-insn a | FileCheck %s --check-prefix=PLT
+
+## As on V9 the loader resolves a PLT slot by writing a jump into the entry, so
+## .plt is writable and the JMP_SLOT relocation names the entry rather than a
+## .got.plt slot. The 32-bit entry is 12 bytes and the reserved header is four
+## of them, so the first entry is at .plt + 0x30.
+# CHECK:      .plt PROGBITS [[#%x,PLTADDR:]] {{.*}} WAX
+# CHECK:      Relocation section '.rela.plt' {{.*}} contains 1 entries:
+# CHECK:      {{0*}}[[#%x,PLTADDR+0x30]] {{.*}} R_SPARC_JMP_SLOT {{.*}} g + 0
+
+## The sethi carries the entry's offset from .PLT0, which is how the resolver
+## recovers the relocation index, and the branch goes back to .PLT0.
+# PLT:      <.plt>:
+# PLT:      sethi 0x30, %g1
+# PLT-NEXT: ba,a
+# PLT-NEXT: nop
+
+#--- a.s
+.globl _start
+_start:
+  call g
+  nop
+
+#--- b.s
+.globl g
+.type g, at function
+g:
+  retl
+  nop
diff --git a/lld/test/ELF/sparc32-reloc.s b/lld/test/ELF/sparc32-reloc.s
new file mode 100644
index 0000000000000..34a863baa2d0d
--- /dev/null
+++ b/lld/test/ELF/sparc32-reloc.s
@@ -0,0 +1,28 @@
+# REQUIRES: sparc
+# RUN: llvm-mc -filetype=obj -triple=sparc %s -o %t.o
+# RUN: ld.lld -m elf32_sparc -Ttext=0xf0004000 %t.o -o %t
+# RUN: llvm-objdump -d -j .text --no-show-raw-insn --no-print-imm-hex %t | FileCheck %s
+# RUN: llvm-readelf -x .data %t | FileCheck %s --check-prefix=DATA
+
+## The relocated value is sign-extended to the ABI width, so on the 32-bit ABI
+## an address with bit 31 set arrives with every high bit set. The unsigned
+## range checks must read it as a 32-bit value rather than an overflow.
+# CHECK:      sethi 3932176, %g1
+# CHECK-NEXT: or %g1, 8, %g1
+
+# DATA: 0x{{[0-9a-f]+}} f0004008
+
+## EM_SPARC is the 32-bit big-endian ABI, so an object claiming it with another
+## class or byte order is malformed.
+# RUN: yaml2obj %S/Inputs/sparc-elf64.yaml -o %t64.o
+# RUN: not ld.lld %t64.o -o /dev/null 2>&1 | FileCheck %s --check-prefix=BADCLASS
+
+# BADCLASS: error: {{.*}}64.o is incompatible
+
+.globl _start
+_start:
+  sethi %hi(_start+8), %g1
+  or %g1, %lo(_start+8), %g1
+
+.data
+.word _start + 8
diff --git a/lld/test/ELF/sparc32-tls.s b/lld/test/ELF/sparc32-tls.s
new file mode 100644
index 0000000000000..ffa69cda52fb0
--- /dev/null
+++ b/lld/test/ELF/sparc32-tls.s
@@ -0,0 +1,66 @@
+# REQUIRES: sparc
+# RUN: rm -rf %t && split-file %s %t && cd %t
+# RUN: llvm-mc -filetype=obj -triple=sparc a.s -o a.o
+# RUN: llvm-mc -filetype=obj -triple=sparc b.s -o b.o
+# RUN: ld.lld -shared b.o -soname=b.so -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
+
+## The 32-bit ABI uses the DTPMOD32/DTPOFF32/TPOFF32 GOT slots. a0 is hidden,
+## so its offset is known at link time and only the module index needs a
+## dynamic relocation.
+# GD-REL:      Relocation section '.rela.dyn' {{.*}} contains 3 entries:
+# GD-REL:      R_SPARC_TLS_DTPMOD32 0{{$}}
+# GD-REL-DAG:  R_SPARC_TLS_DTPMOD32 {{.*}} b + 0
+# GD-REL-DAG:  R_SPARC_TLS_DTPOFF32 {{.*}} b + 0
+# GD-REL:      R_SPARC_JMP_SLOT {{.*}} __tls_get_addr + 0
+
+## a0 is not preemptible in an executable, so its sequence becomes Local Exec.
+## b stays preemptible and becomes Initial Exec, where the 32-bit ABI loads the
+## offset with ld rather than V9's ldx.
+# 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:     R_SPARC_TLS_TPOFF32 {{.*}} b + 0
+# EXE-REL-NOT: R_SPARC_TLS_DTPMOD32
+
+## a0 is the only symbol in the TLS block, so a0 - tp = -4.
+# EXE-LABEL:   <_start>:
+# EXE-NEXT:      sethi 0, %o0
+# EXE-NEXT:      xor %o0, -4, %o0
+# EXE-NEXT:      add %g7, %o0, %o0
+# EXE-NEXT:      nop
+# EXE-NEXT:      nop
+# EXE-NEXT:      sethi 0, %o1
+# EXE-NEXT:      add %o1, 4, %o1
+# EXE-NEXT:      ld [%l7+%o1], %o0
+# EXE-NEXT:      add %g7, %o0, %o0
+# EXE-NEXT:      nop
+
+#--- a.s
+.globl _start
+_start:
+  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
+
+  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:
+  .word 0
+
+#--- b.s
+.section .tbss,"awT", at nobits
+.globl b
+b:
+  .word 0

>From 851d2d9c2fbdef567756e863dadcb50121928b74 Mon Sep 17 00:00:00 2001
From: Imre Kaloz <kaloz at kernel.org>
Date: Wed, 2 Sep 2026 02:49:03 +0200
Subject: [PATCH 3/3] [ELF,SPARC] Accept EM_SPARC32PLUS inputs on the 32-bit
 SPARC target
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

EM_SPARC32PLUS marks an object holding V9 instructions under the
32-bit ABI, so it links with plain EM_SPARC objects and needs no
relocation handling of its own. Rejecting it outright also rejected
the 32-bit libgcc and libatomic distributions ship.

Take it as input, and OR the widest instruction-set bits any
EM_SPARC32PLUS object asks for into the output, as elf32_sparc does:
EF_SPARC_32PLUS, then SUN_US1, then SUN_US3 (implies US1); HAL_R1
selects nothing. gas sets SUN_US1 per UltraSPARC I instruction, not
per -mcpu, so a static link needs the bits OR'd in from such a
library.

The machine is normalized to EM_SPARC during the link, promoted on
write. EM_SPARCV9 and elf32_sparc's other malformed shapes — wrong
class/byte order, or missing EF_SPARC_32PLUS — are rejected.

Signed-off-by: Imre Kaloz <kaloz at kernel.org>
---
 lld/ELF/Arch/SPARC.cpp        |  40 +++++++++++
 lld/ELF/Driver.cpp            |  13 ++--
 lld/ELF/InputFiles.cpp        |   8 ++-
 lld/ELF/SyntheticSections.cpp |   4 ++
 lld/test/ELF/sparc32-v8plus.s | 132 ++++++++++++++++++++++++++++++++++
 5 files changed, 191 insertions(+), 6 deletions(-)
 create mode 100644 lld/test/ELF/sparc32-v8plus.s

diff --git a/lld/ELF/Arch/SPARC.cpp b/lld/ELF/Arch/SPARC.cpp
index 8270265d9a4d9..0d33a4b99b06b 100644
--- a/lld/ELF/Arch/SPARC.cpp
+++ b/lld/ELF/Arch/SPARC.cpp
@@ -31,6 +31,7 @@ class SPARC final : public TargetInfo {
   SPARC(Ctx &, bool is64);
   RelExpr getRelExpr(RelType type, const Symbol &s,
                      const uint8_t *loc) const override;
+  uint32_t calcEFlags() const override;
   RelType getDynRel(RelType type) const override;
   int64_t getImplicitAddend(const uint8_t *buf, RelType type) const override;
   void writeGotHeader(uint8_t *buf) const override;
@@ -108,6 +109,45 @@ RelExpr SPARC::getRelExpr(RelType type, const Symbol &s,
   }
 }
 
+uint32_t SPARC::calcEFlags() const {
+  if (is64)
+    return 0;
+
+  // EF_SPARC_32PLUS and the Sun extension bits name nested instruction sets, so
+  // GNU ld's elf32_sparc gives the output the widest one any object file asks
+  // for, whatever the link order. EF_SPARC_SUN_US3 implies EF_SPARC_SUN_US1.
+  // Only the bits of an EM_SPARC32PLUS object count, only object files count,
+  // and EF_SPARC_HAL_R1 selects nothing.
+  enum { Base, V8Plus, UltraSPARC1, UltraSPARC3 } level = Base;
+  for (InputFile *f : ctx.objectFiles) {
+    if (f->emachine != EM_SPARC32PLUS)
+      continue;
+    uint32_t eflags = cast<ObjFile<ELF32BE>>(f)->getObj().getHeader().e_flags;
+    if (!(eflags & EF_SPARC_32PLUS)) {
+      Err(ctx) << f << ": EM_SPARC32PLUS object without EF_SPARC_32PLUS";
+      continue;
+    }
+    if (eflags & EF_SPARC_SUN_US3)
+      level = std::max(level, UltraSPARC3);
+    else if (eflags & EF_SPARC_SUN_US1)
+      level = std::max(level, UltraSPARC1);
+    else
+      level = std::max(level, V8Plus);
+  }
+
+  switch (level) {
+  case Base:
+    return 0;
+  case V8Plus:
+    return EF_SPARC_32PLUS;
+  case UltraSPARC1:
+    return EF_SPARC_32PLUS | EF_SPARC_SUN_US1;
+  case UltraSPARC3:
+    return EF_SPARC_32PLUS | EF_SPARC_SUN_US1 | EF_SPARC_SUN_US3;
+  }
+  llvm_unreachable("invalid level");
+}
+
 RelType SPARC::getDynRel(RelType type) const {
   if (type == symbolicRel)
     return type;
diff --git a/lld/ELF/Driver.cpp b/lld/ELF/Driver.cpp
index f0d47fd5e8706..c8489bec86eee 100644
--- a/lld/ELF/Driver.cpp
+++ b/lld/ELF/Driver.cpp
@@ -2360,16 +2360,19 @@ void LinkerDriver::inferMachineType() {
     if (f->ekind == ELFNoneKind)
       continue;
     if (!inferred) {
-      // EM_SPARC names the 32-bit big-endian ABI, so an object claiming it
-      // with another class or byte order is malformed. A later object is
-      // caught by the ELF kind check in isCompatible.
-      if (f->emachine == EM_SPARC && f->ekind != ELF32BEKind) {
+      // EM_SPARC and EM_SPARC32PLUS name the 32-bit big-endian ABI, so an
+      // object claiming either with another class or byte order is malformed.
+      // A later object is caught by the ELF kind check in isCompatible.
+      if ((f->emachine == EM_SPARC || f->emachine == EM_SPARC32PLUS) &&
+          f->ekind != ELF32BEKind) {
         Err(ctx) << f.get() << " is incompatible";
         return;
       }
       inferred = true;
       ctx.arg.ekind = f->ekind;
-      ctx.arg.emachine = f->emachine;
+      // EM_SPARC32PLUS is the 32-bit SPARC ABI, and is promoted back on
+      // output when an input needs it.
+      ctx.arg.emachine = f->emachine == EM_SPARC32PLUS ? EM_SPARC : f->emachine;
       ctx.arg.mipsN32Abi = ctx.arg.emachine == EM_MIPS && isMipsN32Abi(ctx, *f);
     }
     ctx.arg.osabi = f->osabi;
diff --git a/lld/ELF/InputFiles.cpp b/lld/ELF/InputFiles.cpp
index 9a919c4c3d5ef..49a2ea2a916da 100644
--- a/lld/ELF/InputFiles.cpp
+++ b/lld/ELF/InputFiles.cpp
@@ -267,7 +267,13 @@ static bool isCompatible(Ctx &ctx, InputFile *file) {
   if (!file->isElf() && !isa<BitcodeFile>(file))
     return true;
 
-  if (file->ekind == ctx.arg.ekind && file->emachine == ctx.arg.emachine) {
+  // An EM_SPARC32PLUS object holds V9 instructions in the 32-bit SPARC ABI, so
+  // it links with plain EM_SPARC objects. The output machine is EM_SPARC until
+  // the header is written.
+  uint16_t emachine =
+      file->emachine == EM_SPARC32PLUS ? EM_SPARC : file->emachine;
+
+  if (file->ekind == ctx.arg.ekind && emachine == ctx.arg.emachine) {
     if (ctx.arg.emachine != EM_MIPS)
       return true;
     if (isMipsN32Abi(ctx, *file) == ctx.arg.mipsN32Abi)
diff --git a/lld/ELF/SyntheticSections.cpp b/lld/ELF/SyntheticSections.cpp
index 392d490083cd6..70e7f9f4cb501 100644
--- a/lld/ELF/SyntheticSections.cpp
+++ b/lld/ELF/SyntheticSections.cpp
@@ -4271,6 +4271,10 @@ template <typename ELFT> void elf::writeEhdr(Ctx &ctx, uint8_t *buf) {
   eHdr->e_ident[EI_OSABI] = ctx.arg.osabi;
   eHdr->e_ident[EI_ABIVERSION] = getAbiVersion(ctx);
   eHdr->e_machine = ctx.arg.emachine;
+  // A 32-bit SPARC link whose objects need V9 instructions is tagged
+  // EM_SPARC32PLUS, which goes with EF_SPARC_32PLUS.
+  if (ctx.arg.emachine == EM_SPARC && (ctx.arg.eflags & EF_SPARC_32PLUS))
+    eHdr->e_machine = EM_SPARC32PLUS;
   eHdr->e_version = EV_CURRENT;
   eHdr->e_flags = ctx.arg.eflags;
   eHdr->e_ehsize = sizeof(typename ELFT::Ehdr);
diff --git a/lld/test/ELF/sparc32-v8plus.s b/lld/test/ELF/sparc32-v8plus.s
new file mode 100644
index 0000000000000..9087433c8284b
--- /dev/null
+++ b/lld/test/ELF/sparc32-v8plus.s
@@ -0,0 +1,132 @@
+# REQUIRES: sparc
+# RUN: rm -rf %t && split-file %s %t && cd %t
+# RUN: llvm-mc -filetype=obj -triple=sparc a.s -o v8.o
+# RUN: llvm-mc -filetype=obj -triple=sparc -mattr=+v8plus b.s -o v8plus.o
+# RUN: llvm-mc -filetype=obj -triple=sparcv9 b.s -o v9.o
+# RUN: yaml2obj us1.yaml -o us1.o
+# RUN: yaml2obj us3.yaml -o us3.o
+# RUN: yaml2obj hal.yaml -o hal.o
+
+## An EM_SPARC32PLUS object holds V9 instructions in the 32-bit ABI, so it links
+## with plain EM_SPARC objects. A link needing them is tagged EM_SPARC32PLUS
+## with EF_SPARC_32PLUS, whatever the order the objects appear in.
+# RUN: ld.lld -m elf32_sparc v8.o v8plus.o -o mixed1
+# RUN: llvm-readelf -h mixed1 | FileCheck %s --check-prefix=PLUS
+# RUN: ld.lld -m elf32_sparc v8plus.o v8.o -o mixed2
+# RUN: llvm-readelf -h mixed2 | FileCheck %s --check-prefix=PLUS
+# RUN: ld.lld -m elf32_sparc v8plus.o -o pureplus
+# RUN: llvm-readelf -h pureplus | FileCheck %s --check-prefix=PLUS
+
+# PLUS: Machine: Sparc v8+
+# PLUS: Flags: 0x100, V8+ ABI{{$}}
+
+## A link whose objects are all plain V8 keeps the base machine and no flags.
+# RUN: ld.lld -m elf32_sparc v8.o -o purev8
+# RUN: llvm-readelf -h purev8 | FileCheck %s --check-prefix=V8
+
+# V8: Machine: Sparc{{$}}
+# V8: Flags: 0x0{{$}}
+
+## The extension bits name nested instruction sets, so the output takes the
+## widest one any object asks for. EF_SPARC_SUN_US3 implies EF_SPARC_SUN_US1.
+# RUN: ld.lld -m elf32_sparc v8.o us1.o -o us1a
+# RUN: ld.lld -m elf32_sparc us1.o v8plus.o -o us1b
+# RUN: llvm-readelf -h us1a us1b | FileCheck %s --check-prefix=US1
+
+# US1: Machine: Sparc v8+
+# US1: Flags: 0x300, V8+ ABI, Sun UltraSPARC I extensions{{$}}
+# US1: Machine: Sparc v8+
+# US1: Flags: 0x300, V8+ ABI, Sun UltraSPARC I extensions{{$}}
+
+# RUN: ld.lld -m elf32_sparc us1.o us3.o -o us3a
+# RUN: ld.lld -m elf32_sparc us3.o us1.o -o us3b
+# RUN: llvm-readelf -h us3a us3b | FileCheck %s --check-prefix=US3
+
+# US3: Flags: 0xb00, V8+ ABI, Sun UltraSPARC I extensions, Sun UltraSPARC III extensions{{$}}
+# US3: Flags: 0xb00, V8+ ABI, Sun UltraSPARC I extensions, Sun UltraSPARC III extensions{{$}}
+
+## EF_SPARC_HAL_R1 names no instruction set here, so it selects nothing and is
+## not written out.
+# RUN: ld.lld -m elf32_sparc v8.o hal.o -o hala
+# RUN: llvm-readelf -h hala | FileCheck %s --check-prefix=PLUS
+
+## EM_SPARCV9 is the 64-bit ABI and does not belong in a 32-bit link.
+# RUN: not ld.lld -m elf32_sparc v8.o v9.o -o /dev/null 2>&1 | FileCheck %s --check-prefix=ERR
+
+# ERR: error: v9.o is incompatible with elf32_sparc
+
+## EM_SPARC32PLUS names the 32-bit big-endian ABI, so another class or byte
+## order is malformed, as is leaving out the instruction set flag.
+# RUN: yaml2obj be64.yaml -o be64.o
+# RUN: yaml2obj le32.yaml -o le32.o
+# RUN: yaml2obj bare.yaml -o bare.o
+# RUN: not ld.lld be64.o -o /dev/null 2>&1 | FileCheck %s --check-prefix=BE64
+# RUN: not ld.lld le32.o -o /dev/null 2>&1 | FileCheck %s --check-prefix=LE32
+# RUN: not ld.lld bare.o -o /dev/null 2>&1 | FileCheck %s --check-prefix=BARE
+
+# BE64: error: be64.o is incompatible
+# LE32: error: le32.o is incompatible
+# BARE: error: bare.o: EM_SPARC32PLUS object without EF_SPARC_32PLUS
+
+#--- a.s
+.globl _start
+_start:
+  nop
+
+#--- b.s
+.globl g
+g:
+  nop
+
+#--- us1.yaml
+--- !ELF
+FileHeader:
+  Class:   ELFCLASS32
+  Data:    ELFDATA2MSB
+  Type:    ET_REL
+  Machine: EM_SPARC32PLUS
+  Flags:   [ EF_SPARC_32PLUS, EF_SPARC_SUN_US1 ]
+
+#--- us3.yaml
+--- !ELF
+FileHeader:
+  Class:   ELFCLASS32
+  Data:    ELFDATA2MSB
+  Type:    ET_REL
+  Machine: EM_SPARC32PLUS
+  Flags:   [ EF_SPARC_32PLUS, EF_SPARC_SUN_US3 ]
+
+#--- hal.yaml
+--- !ELF
+FileHeader:
+  Class:   ELFCLASS32
+  Data:    ELFDATA2MSB
+  Type:    ET_REL
+  Machine: EM_SPARC32PLUS
+  Flags:   [ EF_SPARC_32PLUS, EF_SPARC_HAL_R1 ]
+
+#--- be64.yaml
+--- !ELF
+FileHeader:
+  Class:   ELFCLASS64
+  Data:    ELFDATA2MSB
+  Type:    ET_REL
+  Machine: EM_SPARC32PLUS
+  Flags:   [ EF_SPARC_32PLUS, EF_SPARC_SUN_US1 ]
+
+#--- le32.yaml
+--- !ELF
+FileHeader:
+  Class:   ELFCLASS32
+  Data:    ELFDATA2LSB
+  Type:    ET_REL
+  Machine: EM_SPARC32PLUS
+  Flags:   [ EF_SPARC_32PLUS, EF_SPARC_SUN_US1 ]
+
+#--- bare.yaml
+--- !ELF
+FileHeader:
+  Class:   ELFCLASS32
+  Data:    ELFDATA2MSB
+  Type:    ET_REL
+  Machine: EM_SPARC32PLUS



More information about the llvm-commits mailing list