Skip to content

Add initial support for Xtensa to LLD - #202926

Open
gerekon wants to merge 2 commits into
llvm:mainfrom
gerekon:feat/xtensa_lld
Open

Add initial support for Xtensa to LLD#202926
gerekon wants to merge 2 commits into
llvm:mainfrom
gerekon:feat/xtensa_lld

Conversation

@gerekon

@gerekon gerekon commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

No description provided.

@llvmorg-github-actions

llvmorg-github-actions Bot commented Jun 10, 2026

Copy link
Copy Markdown

@llvm/pr-subscribers-llvm-binary-utilities
@llvm/pr-subscribers-backend-xtensa

@llvm/pr-subscribers-lld

Author: Alexey Gerenkov (gerekon)

Changes

Full diff: https://github.com/llvm/llvm-project/pull/202926.diff

9 Files Affected:

  • (added) lld/ELF/Arch/Xtensa.cpp (+212)
  • (modified) lld/ELF/CMakeLists.txt (+1)
  • (modified) lld/ELF/InputFiles.cpp (+2)
  • (modified) lld/ELF/Target.cpp (+2)
  • (modified) lld/ELF/Target.h (+2)
  • (modified) lld/ELF/Writer.cpp (+57)
  • (added) lld/test/ELF/xtensa-reloc.s (+63)
  • (modified) lld/test/lit.cfg.py (+1)
  • (modified) llvm/include/llvm/BinaryFormat/ELFRelocs/Xtensa.def (+6)
diff --git a/lld/ELF/Arch/Xtensa.cpp b/lld/ELF/Arch/Xtensa.cpp
new file mode 100644
index 0000000000000..b763ae7bbd56b
--- /dev/null
+++ b/lld/ELF/Arch/Xtensa.cpp
@@ -0,0 +1,212 @@
+//===- Xtensa.cpp ---------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "InputFiles.h"
+#include "Symbols.h"
+#include "Target.h"
+#include <bitset>
+#include <iostream>
+#include <string>
+
+using namespace llvm;
+using namespace llvm::object;
+using namespace llvm::support::endian;
+using namespace llvm::ELF;
+using namespace lld;
+using namespace lld::elf;
+
+namespace {
+
+class Xtensa final : public TargetInfo {
+public:
+  Xtensa(Ctx &ctx);
+  RelExpr getRelExpr(RelType type, const Symbol &s,
+                     const uint8_t *loc) const override;
+  void relocate(uint8_t *loc, const Relocation &rel,
+                uint64_t val) const override;
+};
+
+} // namespace
+
+Xtensa::Xtensa(Ctx &ctx) : TargetInfo(ctx) {}
+
+RelExpr Xtensa::getRelExpr(RelType type, const Symbol &s,
+                           const uint8_t *loc) const {
+  switch (type) {
+  case R_XTENSA_32:
+    return R_ABS;
+  case R_XTENSA_SLOT0_OP:
+    // This relocation is used for various instructions, with varying ways to
+    // calculate the relocation value. This is unlike most ELF architectures,
+    // and is arguably bad design (see the comment on R_386_GOT32 in X86.cpp).
+    // But that's what compilers emit, so it needs to be supported.
+    //
+    // We work around this by returning R_PC here and calculating the PC address
+    // in Xtensa::relocate based on the relative value. That's ugly. A better
+    // solution would be to look at the instruction here and emit various
+    // Xtensa-specific RelTypes, but that has another problem: the RelExpr enum
+    // is at its maximum size of 64. This will need to be fixed eventually, but
+    // for now hack around it and return R_PC.
+    return R_PC;
+  case R_XTENSA_ASM_EXPAND:
+    // This relocation appears to be emitted by the GNU Xtensa compiler as a
+    // linker relaxation hint. For example, for the following code:
+    //
+    //   .section .foo
+    //   .align  4
+    //   foo:
+    //       nop
+    //       nop
+    //       call0 bar
+    //   .align  4
+    //       bar:
+    //
+    // The call0 instruction is compiled to a l32r and callx0 instruction.
+    // The LLVM Xtensa backend does not emit this relocation.
+    // Because it's a relaxation hint, this relocation can be ignored for now
+    // until linker relaxations are implemented.
+    return R_NONE;
+  case R_XTENSA_DIFF8:
+  case R_XTENSA_DIFF16:
+  case R_XTENSA_DIFF32:
+  case R_XTENSA_PDIFF8:
+  case R_XTENSA_PDIFF16:
+  case R_XTENSA_PDIFF32:
+  case R_XTENSA_NDIFF8:
+  case R_XTENSA_NDIFF16:
+  case R_XTENSA_NDIFF32:
+    // > Xtensa relocations to mark the difference of two local symbols.
+    // > These are only needed to support linker relaxation and can be ignored
+    // > when not relaxing.
+    // Source:
+    // https://github.com/espressif/binutils-gdb/commit/30ce8e47fad9b057b6d7af9e1d43061126d34d20:
+    // Because we don't do linker relaxation, we can ignore these relocations.
+    return R_NONE;
+  default:
+    Err(ctx) << getErrorLoc(ctx, loc) << "unknown relocation (" << type.v
+          << ") against symbol " << &s;
+    return R_NONE;
+  }
+}
+
+static inline bool isRRI8Branch(uint8_t *loc) {
+  // instructions: ball, bany, bbc, bbci, bbs, bbsi, beq, bge, bgeu, blt,
+  // bltu, bnall, bne, bnone
+  if ((loc[0] & 0x0f) == 0b0111)
+    return true;
+  // instructions: beqi, bgei, bnei, blti
+  if ((loc[0] & 0b11'1111) == 0b10'0110)
+    return true;
+  // instructions: bgeui, bltui
+  if ((loc[0] & 0b1011'1111) == 0b1011'0110)
+    return true;
+  if ((loc[0] & 0b0111'1111) == 0b0111'0110) {
+    // instruction: bf
+    if ((loc[1] & 0b1111'0000) == 0b0000'0000)
+      return true;
+    // instruction: bt
+    if ((loc[1] & 0b1111'0000) == 0b0001'0000)
+      return true;
+  }
+  // some other instruction
+  return false;
+}
+
+static inline bool isLoop(uint8_t *loc) {
+  // instructions: loop, loopgtz, loopnez
+  if ((loc[0] & 0b1111'1111) == 0b0111'0110) {
+    // instruction: loop
+    if ((loc[1] & 0b1111'0000) == 0b1000'0000)
+      return true;
+    // instruction: loopgtz
+    if ((loc[1] & 0b1111'0000) == 0b1010'0000)
+      return true;
+    // instruction: loopnez
+    if ((loc[1] & 0b1111'0000) == 0b1001'0000)
+      return true;
+  }
+  // some other instruction
+  return false;
+}
+
+void Xtensa::relocate(uint8_t *loc, const Relocation &rel, uint64_t val) const {
+  switch (rel.type) {
+  case R_XTENSA_32:
+    // R_XTENSA_32 is a partial_inplace relocation (GNU bfd howto:
+    // src_mask = dst_mask = 0xffffffff). The GNU Xtensa assembler stores the
+    // addend in the relocated word itself and leaves the RELA r_addend as 0,
+    // so the result must be S + r_addend + <existing contents>. Objects from
+    // the LLVM Xtensa backend keep the addend in r_addend and zero the field,
+    // so reading the contents back adds 0 and is harmless. Plain
+    // write32le(loc, val) drops the in-place addend, which miscomputes every
+    // R_XTENSA_32 with a nonzero addend -- e.g. the switch jump tables in the
+    // precompiled Espressif Wi-Fi blobs, which then point at a section base
+    // instead of the intended in-section offset.
+    write32le(loc, val + read32le(loc));
+    break;
+  case R_XTENSA_SLOT0_OP: {
+    // HACK: calculate the instruction location based on the PC-relative
+    // relocation value.
+    uint64_t dest = rel.sym->getVA(ctx, rel.addend);
+    uint64_t p = dest - val;
+
+    // This relocation is used for various instructions.
+    // Look at the instruction to determine how to do the relocation.
+    uint8_t opcode = loc[0] & 0x0f;
+    if (opcode == 0b0001) { // RI16 format: l32r
+      int64_t val = dest - ((p + 3) & (uint64_t)0xfffffffc);
+      if ((val < -262144 || val > -4))
+        reportRangeError(ctx, loc, rel, Twine(static_cast<int64_t>(val)), -262141,
+                         -4);
+      checkAlignment(ctx, loc, static_cast<uint64_t>(val), 4, rel);
+      write16le(loc + 1, val >> 2);
+    } else if (opcode == 0b0101) { // call0, call4, call8, call12 (CALL format)
+      uint64_t val = dest - ((p + 4) & (uint64_t)0xfffffffc);
+      checkInt(ctx, loc, static_cast<int64_t>(val) >> 2, 18, rel);
+      checkAlignment(ctx, loc, val, 4, rel);
+      const int64_t target = static_cast<int64_t>(val) >> 2;
+      loc[0] = (loc[0] & 0b0011'1111) | ((target & 0b0000'0011) << 6);
+      loc[1] = target >> 2;
+      loc[2] = target >> 10;
+    } else if ((loc[0] & 0x3f) == 0b00'0110) { // j (CALL format)
+      uint64_t valJ = val - 4;
+      checkInt(ctx, loc, static_cast<int64_t>(valJ), 18, rel);
+      loc[0] = (loc[0] & 0b0011'1111) | ((valJ & 0b0000'0011) << 6);
+      loc[1] = valJ >> 2;
+      loc[2] = valJ >> 10;
+    } else if (isRRI8Branch(loc)) { // RRI8 format (various branch instructions)
+      uint64_t v = val - 4;
+      checkInt(ctx, loc, static_cast<int64_t>(v), 8, rel);
+      loc[2] = v & 0xff;
+    } else if (isLoop(loc)) { // loop instructions
+      uint64_t v = val - 4;
+      checkUInt(ctx, loc, v, 8, rel);
+      loc[2] = v & 0xff;
+    } else if ((loc[0] & 0b1000'1111) == 0b1000'1100) { // RI16 format: beqz.n, bnez.n
+      uint64_t v = val - 4;
+      checkUInt(ctx, loc, v, 6, rel);
+      loc[0] = (loc[0] & 0xcf) | (v & 0x30);
+      loc[1] = (loc[1] & 0x0f) | ((v & 0x0f) << 4);
+    } else if ((loc[0] & 0b0011'1111) == 0b0001'0110) { // BRI12 format: beqz, bgez, bltz, bnez
+      uint64_t v = val - 4;
+      checkInt(ctx, loc, static_cast<int64_t>(v), 12, rel);
+      loc[1] = ((loc[1] & 0x0f)) | ((v & 0x0f) << 4);
+      loc[2] = (v >> 4) & 0xff;
+    } else {
+      Err(ctx) << getErrorLoc(ctx, loc) <<
+            "unknown opcode for relocation: " << loc[0];
+    }
+    break;
+  }
+  default:
+    llvm_unreachable("unknown relocation");
+  }
+}
+
+void elf::setXtensaTargetInfo(Ctx &ctx) { ctx.target.reset(new Xtensa(ctx)); }
+
diff --git a/lld/ELF/CMakeLists.txt b/lld/ELF/CMakeLists.txt
index e22897c2789d8..29eb121b1c87b 100644
--- a/lld/ELF/CMakeLists.txt
+++ b/lld/ELF/CMakeLists.txt
@@ -36,6 +36,7 @@ add_lld_library(lldELF
   Arch/SystemZ.cpp
   Arch/X86.cpp
   Arch/X86_64.cpp
+  Arch/Xtensa.cpp
   ARMErrataFix.cpp
   BPSectionOrderer.cpp
   CallGraphSort.cpp
diff --git a/lld/ELF/InputFiles.cpp b/lld/ELF/InputFiles.cpp
index 2f544f0fe0958..77299bc38bb06 100644
--- a/lld/ELF/InputFiles.cpp
+++ b/lld/ELF/InputFiles.cpp
@@ -1783,6 +1783,8 @@ static uint16_t getBitcodeMachineKind(Ctx &ctx, StringRef path,
     return t.isOSIAMCU() ? EM_IAMCU : EM_386;
   case Triple::x86_64:
     return EM_X86_64;
+  case Triple::xtensa:
+    return EM_XTENSA;
   default:
     ErrAlways(ctx) << path
                    << ": could not infer e_machine from bitcode target triple "
diff --git a/lld/ELF/Target.cpp b/lld/ELF/Target.cpp
index 89e4dbeed3109..89cc9357cac1b 100644
--- a/lld/ELF/Target.cpp
+++ b/lld/ELF/Target.cpp
@@ -83,6 +83,8 @@ void elf::setTarget(Ctx &ctx) {
     return setSystemZTargetInfo(ctx);
   case EM_X86_64:
     return setX86_64TargetInfo(ctx);
+  case EM_XTENSA:
+    return setXtensaTargetInfo(ctx);
   default:
     Fatal(ctx) << "unsupported e_machine value: " << ctx.arg.emachine;
   }
diff --git a/lld/ELF/Target.h b/lld/ELF/Target.h
index c9b2af07149e0..e5c0e4a17c259 100644
--- a/lld/ELF/Target.h
+++ b/lld/ELF/Target.h
@@ -217,6 +217,8 @@ void setSPARCV9TargetInfo(Ctx &);
 void setSystemZTargetInfo(Ctx &);
 void setX86TargetInfo(Ctx &);
 void setX86_64TargetInfo(Ctx &);
+void setXtensaTargetInfo(Ctx &);
+template <class ELFT> TargetInfo *getMipsTargetInfo();
 
 struct ErrorPlace {
   InputSectionBase *isec;
diff --git a/lld/ELF/Writer.cpp b/lld/ELF/Writer.cpp
index c2d05c75df39d..9313e9527749c 100644
--- a/lld/ELF/Writer.cpp
+++ b/lld/ELF/Writer.cpp
@@ -33,6 +33,7 @@
 #include "llvm/Support/TimeProfiler.h"
 #include "llvm/Support/xxhash.h"
 #include <climits>
+#include <map>
 
 #define DEBUG_TYPE "lld"
 
@@ -1216,6 +1217,58 @@ sortISDBySectionOrder(Ctx &ctx, InputSectionDescription *isd,
     isd->sections.push_back(isec);
 }
 
+// Sort Xtensa literal sections in OutputSection. For each literal section we try
+// to find by name text(code) section, which uses these literals. The literal
+// section should always be placed before code section.
+// Also we try to place literal section just before code section.
+static void sortSectionXtensa(OutputSection &osec) {
+  for (SectionCommand *b : osec.commands) {
+    if (auto *isd = dyn_cast<InputSectionDescription>(b)) {
+      std::map<std::string, int> orderedNames;
+      SmallVector<std::pair<InputSection *, int>, 0> orderedSections;
+      int sidx = 0;
+
+      for (InputSection *isec : isd->sections) {
+        if (orderedNames.count(isec->name.str())) {
+          orderedSections.push_back({isec, orderedNames[isec->name.str()]});
+          continue;
+        }
+        //Check if current section contains literals
+        if (isec->name.contains(".literal")) {
+          std::string literalName = isec->name.str();
+          std::size_t pos = literalName.find(".literal");
+          std::string textName;
+          // Reconstructing text(code) section name by literal section name
+          if (pos == 0) {
+            textName = ".text";
+          } else {
+            textName = literalName.substr(0, pos);
+          }
+          textName += literalName.substr(pos + 8, std::string::npos);
+          if (orderedNames.count(textName)) {
+            int textIdx = orderedNames[textName];
+            int literalIdx = textIdx - 1;
+            orderedSections.push_back({isec, literalIdx});
+            orderedNames[isec->name.str()] = literalIdx;
+          } else {
+            orderedSections.push_back({isec, sidx});
+            orderedNames[isec->name.str()] = sidx;
+          }
+        } else {
+          orderedSections.push_back({isec, sidx});
+          orderedNames[isec->name.str()] = sidx;
+        }
+        sidx += 2;
+      }
+
+      llvm::sort(orderedSections, llvm::less_second());
+      isd->sections.clear();
+      for (std::pair<InputSection *, int> p : orderedSections)
+        isd->sections.push_back(p.first);
+    }
+  }
+}
+
 static void sortSection(Ctx &ctx, OutputSection &osec,
                         const DenseMap<const InputSectionBase *, int> &order) {
   StringRef name = osec.name;
@@ -1233,6 +1286,10 @@ static void sortSection(Ctx &ctx, OutputSection &osec,
       if (auto *isd = dyn_cast<InputSectionDescription>(b))
         sortISDBySectionOrder(ctx, isd, order, osec.flags & SHF_EXECINSTR);
 
+  if (ctx.arg.emachine == EM_XTENSA) {
+    sortSectionXtensa(osec);
+  }
+
   if (ctx.script->hasSectionsCommand)
     return;
 
diff --git a/lld/test/ELF/xtensa-reloc.s b/lld/test/ELF/xtensa-reloc.s
new file mode 100644
index 0000000000000..cf0312166ee61
--- /dev/null
+++ b/lld/test/ELF/xtensa-reloc.s
@@ -0,0 +1,63 @@
+# REQUIRES: xtensa
+# RUN: llvm-mc -filetype=obj -triple=xtensa -mcpu=esp32 %s -o %t.o
+# RUN: ld.lld %t.o --defsym=a=0x2000 --section-start=.CALL=0x1000 --defsym=b=0x40 --defsym=c=0x140 --section-start=.BRANCH=0x5000 --defsym=d=0x5010 --section-start=.BR12=0x100 --image-base=0x0  -o %t
+# RUN: llvm-objdump -d --mcpu=esp32 --print-imm-hex %t | FileCheck %s
+
+.section .BR12,"ax",@progbits
+ .globl _start
+ .balign 0x100
+ .type _start,%function
+_start:
+# CHECK-LABEL:section .BR12
+# CHECK:      beqz a2, 0x140
+# CHECK-NEXT: bnez a3, 0x140
+# CHECK-NEXT: bgez a4, 0x140
+# CHECK-NEXT: bltz a5, 0x140
+  beqz a2, c
+  bnez a3, c
+  bgez a4, c
+  bltz a5, c
+
+.section .CALL,"ax",@progbits
+# CHECK-LABEL: section .CALL:
+# CHECK:      call0 0x2000
+# CHECK-NEXT: call0 0x2000
+# CHECK-NEXT: call0 0x2000
+# CHECK-NEXT: call0 0x2000
+# CHECK-NEXT: j     0x2000
+# CHECK-NEXT: j     0x2000
+# CHECK-NEXT: j     0x2000
+# CHECK-NEXT: j     0x40
+# CHECK-NEXT: j     0x140
+# CHECK-NEXT: call0 0x40
+# CHECK-NEXT: call0 0x140
+# CHECK-NEXT: l32r a3, 0x40
+# CHECK-NEXT: callx0 a3
+# CHECK-NEXT: l32r a4, 0x140
+# CHECK-NEXT: callx0 a4
+  call0 a
+  call0 a
+  call0 a
+  call0 a
+  j a
+  j a
+  j a
+  j b
+  j c
+  call0 b
+  call0 c
+  l32r a3, b
+  callx0 a3
+  l32r a4, c
+  callx0 a4
+
+.section .BRANCH,"ax",@progbits
+# CHECK-LABEL: section .BRANCH:
+# CHECK:      beq a3, a4, 0x5010
+# CHECK-NEXT: ball a3, a4, 0x5010
+# CHECK-NEXT: blt a3, a4, 0x5010
+# CHECK-NEXT: bt b0, 0x5010
+  beq a3, a4, d
+  ball a3, a4, d
+  blt a3, a4, d
+  bt b0, d
diff --git a/lld/test/lit.cfg.py b/lld/test/lit.cfg.py
index 39c3d0aa36bfb..ff4236a3d6762 100644
--- a/lld/test/lit.cfg.py
+++ b/lld/test/lit.cfg.py
@@ -99,6 +99,7 @@
                 "SystemZ": "systemz",
                 "WebAssembly": "wasm",
                 "X86": "x86",
+                'Xtensa': 'xtensa',
             },
         ),
         ("--assertion-mode", {"ON": "asserts"}),
diff --git a/llvm/include/llvm/BinaryFormat/ELFRelocs/Xtensa.def b/llvm/include/llvm/BinaryFormat/ELFRelocs/Xtensa.def
index 6791a842181ff..c2e11259164bb 100644
--- a/llvm/include/llvm/BinaryFormat/ELFRelocs/Xtensa.def
+++ b/llvm/include/llvm/BinaryFormat/ELFRelocs/Xtensa.def
@@ -58,3 +58,9 @@ ELF_RELOC(R_XTENSA_TLS_TPOFF,        53)
 ELF_RELOC(R_XTENSA_TLS_FUNC,         54)
 ELF_RELOC(R_XTENSA_TLS_ARG,          55)
 ELF_RELOC(R_XTENSA_TLS_CALL,         56)
+ELF_RELOC(R_XTENSA_PDIFF8,           57)
+ELF_RELOC(R_XTENSA_PDIFF16,          58)
+ELF_RELOC(R_XTENSA_PDIFF32,          59)
+ELF_RELOC(R_XTENSA_NDIFF8,           60)
+ELF_RELOC(R_XTENSA_NDIFF16,          61)
+ELF_RELOC(R_XTENSA_NDIFF32,          62)

@llvmorg-github-actions

Copy link
Copy Markdown

@llvm/pr-subscribers-lld-elf

Author: Alexey Gerenkov (gerekon)

Changes

Full diff: https://github.com/llvm/llvm-project/pull/202926.diff

9 Files Affected:

  • (added) lld/ELF/Arch/Xtensa.cpp (+212)
  • (modified) lld/ELF/CMakeLists.txt (+1)
  • (modified) lld/ELF/InputFiles.cpp (+2)
  • (modified) lld/ELF/Target.cpp (+2)
  • (modified) lld/ELF/Target.h (+2)
  • (modified) lld/ELF/Writer.cpp (+57)
  • (added) lld/test/ELF/xtensa-reloc.s (+63)
  • (modified) lld/test/lit.cfg.py (+1)
  • (modified) llvm/include/llvm/BinaryFormat/ELFRelocs/Xtensa.def (+6)
diff --git a/lld/ELF/Arch/Xtensa.cpp b/lld/ELF/Arch/Xtensa.cpp
new file mode 100644
index 0000000000000..b763ae7bbd56b
--- /dev/null
+++ b/lld/ELF/Arch/Xtensa.cpp
@@ -0,0 +1,212 @@
+//===- Xtensa.cpp ---------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "InputFiles.h"
+#include "Symbols.h"
+#include "Target.h"
+#include <bitset>
+#include <iostream>
+#include <string>
+
+using namespace llvm;
+using namespace llvm::object;
+using namespace llvm::support::endian;
+using namespace llvm::ELF;
+using namespace lld;
+using namespace lld::elf;
+
+namespace {
+
+class Xtensa final : public TargetInfo {
+public:
+  Xtensa(Ctx &ctx);
+  RelExpr getRelExpr(RelType type, const Symbol &s,
+                     const uint8_t *loc) const override;
+  void relocate(uint8_t *loc, const Relocation &rel,
+                uint64_t val) const override;
+};
+
+} // namespace
+
+Xtensa::Xtensa(Ctx &ctx) : TargetInfo(ctx) {}
+
+RelExpr Xtensa::getRelExpr(RelType type, const Symbol &s,
+                           const uint8_t *loc) const {
+  switch (type) {
+  case R_XTENSA_32:
+    return R_ABS;
+  case R_XTENSA_SLOT0_OP:
+    // This relocation is used for various instructions, with varying ways to
+    // calculate the relocation value. This is unlike most ELF architectures,
+    // and is arguably bad design (see the comment on R_386_GOT32 in X86.cpp).
+    // But that's what compilers emit, so it needs to be supported.
+    //
+    // We work around this by returning R_PC here and calculating the PC address
+    // in Xtensa::relocate based on the relative value. That's ugly. A better
+    // solution would be to look at the instruction here and emit various
+    // Xtensa-specific RelTypes, but that has another problem: the RelExpr enum
+    // is at its maximum size of 64. This will need to be fixed eventually, but
+    // for now hack around it and return R_PC.
+    return R_PC;
+  case R_XTENSA_ASM_EXPAND:
+    // This relocation appears to be emitted by the GNU Xtensa compiler as a
+    // linker relaxation hint. For example, for the following code:
+    //
+    //   .section .foo
+    //   .align  4
+    //   foo:
+    //       nop
+    //       nop
+    //       call0 bar
+    //   .align  4
+    //       bar:
+    //
+    // The call0 instruction is compiled to a l32r and callx0 instruction.
+    // The LLVM Xtensa backend does not emit this relocation.
+    // Because it's a relaxation hint, this relocation can be ignored for now
+    // until linker relaxations are implemented.
+    return R_NONE;
+  case R_XTENSA_DIFF8:
+  case R_XTENSA_DIFF16:
+  case R_XTENSA_DIFF32:
+  case R_XTENSA_PDIFF8:
+  case R_XTENSA_PDIFF16:
+  case R_XTENSA_PDIFF32:
+  case R_XTENSA_NDIFF8:
+  case R_XTENSA_NDIFF16:
+  case R_XTENSA_NDIFF32:
+    // > Xtensa relocations to mark the difference of two local symbols.
+    // > These are only needed to support linker relaxation and can be ignored
+    // > when not relaxing.
+    // Source:
+    // https://github.com/espressif/binutils-gdb/commit/30ce8e47fad9b057b6d7af9e1d43061126d34d20:
+    // Because we don't do linker relaxation, we can ignore these relocations.
+    return R_NONE;
+  default:
+    Err(ctx) << getErrorLoc(ctx, loc) << "unknown relocation (" << type.v
+          << ") against symbol " << &s;
+    return R_NONE;
+  }
+}
+
+static inline bool isRRI8Branch(uint8_t *loc) {
+  // instructions: ball, bany, bbc, bbci, bbs, bbsi, beq, bge, bgeu, blt,
+  // bltu, bnall, bne, bnone
+  if ((loc[0] & 0x0f) == 0b0111)
+    return true;
+  // instructions: beqi, bgei, bnei, blti
+  if ((loc[0] & 0b11'1111) == 0b10'0110)
+    return true;
+  // instructions: bgeui, bltui
+  if ((loc[0] & 0b1011'1111) == 0b1011'0110)
+    return true;
+  if ((loc[0] & 0b0111'1111) == 0b0111'0110) {
+    // instruction: bf
+    if ((loc[1] & 0b1111'0000) == 0b0000'0000)
+      return true;
+    // instruction: bt
+    if ((loc[1] & 0b1111'0000) == 0b0001'0000)
+      return true;
+  }
+  // some other instruction
+  return false;
+}
+
+static inline bool isLoop(uint8_t *loc) {
+  // instructions: loop, loopgtz, loopnez
+  if ((loc[0] & 0b1111'1111) == 0b0111'0110) {
+    // instruction: loop
+    if ((loc[1] & 0b1111'0000) == 0b1000'0000)
+      return true;
+    // instruction: loopgtz
+    if ((loc[1] & 0b1111'0000) == 0b1010'0000)
+      return true;
+    // instruction: loopnez
+    if ((loc[1] & 0b1111'0000) == 0b1001'0000)
+      return true;
+  }
+  // some other instruction
+  return false;
+}
+
+void Xtensa::relocate(uint8_t *loc, const Relocation &rel, uint64_t val) const {
+  switch (rel.type) {
+  case R_XTENSA_32:
+    // R_XTENSA_32 is a partial_inplace relocation (GNU bfd howto:
+    // src_mask = dst_mask = 0xffffffff). The GNU Xtensa assembler stores the
+    // addend in the relocated word itself and leaves the RELA r_addend as 0,
+    // so the result must be S + r_addend + <existing contents>. Objects from
+    // the LLVM Xtensa backend keep the addend in r_addend and zero the field,
+    // so reading the contents back adds 0 and is harmless. Plain
+    // write32le(loc, val) drops the in-place addend, which miscomputes every
+    // R_XTENSA_32 with a nonzero addend -- e.g. the switch jump tables in the
+    // precompiled Espressif Wi-Fi blobs, which then point at a section base
+    // instead of the intended in-section offset.
+    write32le(loc, val + read32le(loc));
+    break;
+  case R_XTENSA_SLOT0_OP: {
+    // HACK: calculate the instruction location based on the PC-relative
+    // relocation value.
+    uint64_t dest = rel.sym->getVA(ctx, rel.addend);
+    uint64_t p = dest - val;
+
+    // This relocation is used for various instructions.
+    // Look at the instruction to determine how to do the relocation.
+    uint8_t opcode = loc[0] & 0x0f;
+    if (opcode == 0b0001) { // RI16 format: l32r
+      int64_t val = dest - ((p + 3) & (uint64_t)0xfffffffc);
+      if ((val < -262144 || val > -4))
+        reportRangeError(ctx, loc, rel, Twine(static_cast<int64_t>(val)), -262141,
+                         -4);
+      checkAlignment(ctx, loc, static_cast<uint64_t>(val), 4, rel);
+      write16le(loc + 1, val >> 2);
+    } else if (opcode == 0b0101) { // call0, call4, call8, call12 (CALL format)
+      uint64_t val = dest - ((p + 4) & (uint64_t)0xfffffffc);
+      checkInt(ctx, loc, static_cast<int64_t>(val) >> 2, 18, rel);
+      checkAlignment(ctx, loc, val, 4, rel);
+      const int64_t target = static_cast<int64_t>(val) >> 2;
+      loc[0] = (loc[0] & 0b0011'1111) | ((target & 0b0000'0011) << 6);
+      loc[1] = target >> 2;
+      loc[2] = target >> 10;
+    } else if ((loc[0] & 0x3f) == 0b00'0110) { // j (CALL format)
+      uint64_t valJ = val - 4;
+      checkInt(ctx, loc, static_cast<int64_t>(valJ), 18, rel);
+      loc[0] = (loc[0] & 0b0011'1111) | ((valJ & 0b0000'0011) << 6);
+      loc[1] = valJ >> 2;
+      loc[2] = valJ >> 10;
+    } else if (isRRI8Branch(loc)) { // RRI8 format (various branch instructions)
+      uint64_t v = val - 4;
+      checkInt(ctx, loc, static_cast<int64_t>(v), 8, rel);
+      loc[2] = v & 0xff;
+    } else if (isLoop(loc)) { // loop instructions
+      uint64_t v = val - 4;
+      checkUInt(ctx, loc, v, 8, rel);
+      loc[2] = v & 0xff;
+    } else if ((loc[0] & 0b1000'1111) == 0b1000'1100) { // RI16 format: beqz.n, bnez.n
+      uint64_t v = val - 4;
+      checkUInt(ctx, loc, v, 6, rel);
+      loc[0] = (loc[0] & 0xcf) | (v & 0x30);
+      loc[1] = (loc[1] & 0x0f) | ((v & 0x0f) << 4);
+    } else if ((loc[0] & 0b0011'1111) == 0b0001'0110) { // BRI12 format: beqz, bgez, bltz, bnez
+      uint64_t v = val - 4;
+      checkInt(ctx, loc, static_cast<int64_t>(v), 12, rel);
+      loc[1] = ((loc[1] & 0x0f)) | ((v & 0x0f) << 4);
+      loc[2] = (v >> 4) & 0xff;
+    } else {
+      Err(ctx) << getErrorLoc(ctx, loc) <<
+            "unknown opcode for relocation: " << loc[0];
+    }
+    break;
+  }
+  default:
+    llvm_unreachable("unknown relocation");
+  }
+}
+
+void elf::setXtensaTargetInfo(Ctx &ctx) { ctx.target.reset(new Xtensa(ctx)); }
+
diff --git a/lld/ELF/CMakeLists.txt b/lld/ELF/CMakeLists.txt
index e22897c2789d8..29eb121b1c87b 100644
--- a/lld/ELF/CMakeLists.txt
+++ b/lld/ELF/CMakeLists.txt
@@ -36,6 +36,7 @@ add_lld_library(lldELF
   Arch/SystemZ.cpp
   Arch/X86.cpp
   Arch/X86_64.cpp
+  Arch/Xtensa.cpp
   ARMErrataFix.cpp
   BPSectionOrderer.cpp
   CallGraphSort.cpp
diff --git a/lld/ELF/InputFiles.cpp b/lld/ELF/InputFiles.cpp
index 2f544f0fe0958..77299bc38bb06 100644
--- a/lld/ELF/InputFiles.cpp
+++ b/lld/ELF/InputFiles.cpp
@@ -1783,6 +1783,8 @@ static uint16_t getBitcodeMachineKind(Ctx &ctx, StringRef path,
     return t.isOSIAMCU() ? EM_IAMCU : EM_386;
   case Triple::x86_64:
     return EM_X86_64;
+  case Triple::xtensa:
+    return EM_XTENSA;
   default:
     ErrAlways(ctx) << path
                    << ": could not infer e_machine from bitcode target triple "
diff --git a/lld/ELF/Target.cpp b/lld/ELF/Target.cpp
index 89e4dbeed3109..89cc9357cac1b 100644
--- a/lld/ELF/Target.cpp
+++ b/lld/ELF/Target.cpp
@@ -83,6 +83,8 @@ void elf::setTarget(Ctx &ctx) {
     return setSystemZTargetInfo(ctx);
   case EM_X86_64:
     return setX86_64TargetInfo(ctx);
+  case EM_XTENSA:
+    return setXtensaTargetInfo(ctx);
   default:
     Fatal(ctx) << "unsupported e_machine value: " << ctx.arg.emachine;
   }
diff --git a/lld/ELF/Target.h b/lld/ELF/Target.h
index c9b2af07149e0..e5c0e4a17c259 100644
--- a/lld/ELF/Target.h
+++ b/lld/ELF/Target.h
@@ -217,6 +217,8 @@ void setSPARCV9TargetInfo(Ctx &);
 void setSystemZTargetInfo(Ctx &);
 void setX86TargetInfo(Ctx &);
 void setX86_64TargetInfo(Ctx &);
+void setXtensaTargetInfo(Ctx &);
+template <class ELFT> TargetInfo *getMipsTargetInfo();
 
 struct ErrorPlace {
   InputSectionBase *isec;
diff --git a/lld/ELF/Writer.cpp b/lld/ELF/Writer.cpp
index c2d05c75df39d..9313e9527749c 100644
--- a/lld/ELF/Writer.cpp
+++ b/lld/ELF/Writer.cpp
@@ -33,6 +33,7 @@
 #include "llvm/Support/TimeProfiler.h"
 #include "llvm/Support/xxhash.h"
 #include <climits>
+#include <map>
 
 #define DEBUG_TYPE "lld"
 
@@ -1216,6 +1217,58 @@ sortISDBySectionOrder(Ctx &ctx, InputSectionDescription *isd,
     isd->sections.push_back(isec);
 }
 
+// Sort Xtensa literal sections in OutputSection. For each literal section we try
+// to find by name text(code) section, which uses these literals. The literal
+// section should always be placed before code section.
+// Also we try to place literal section just before code section.
+static void sortSectionXtensa(OutputSection &osec) {
+  for (SectionCommand *b : osec.commands) {
+    if (auto *isd = dyn_cast<InputSectionDescription>(b)) {
+      std::map<std::string, int> orderedNames;
+      SmallVector<std::pair<InputSection *, int>, 0> orderedSections;
+      int sidx = 0;
+
+      for (InputSection *isec : isd->sections) {
+        if (orderedNames.count(isec->name.str())) {
+          orderedSections.push_back({isec, orderedNames[isec->name.str()]});
+          continue;
+        }
+        //Check if current section contains literals
+        if (isec->name.contains(".literal")) {
+          std::string literalName = isec->name.str();
+          std::size_t pos = literalName.find(".literal");
+          std::string textName;
+          // Reconstructing text(code) section name by literal section name
+          if (pos == 0) {
+            textName = ".text";
+          } else {
+            textName = literalName.substr(0, pos);
+          }
+          textName += literalName.substr(pos + 8, std::string::npos);
+          if (orderedNames.count(textName)) {
+            int textIdx = orderedNames[textName];
+            int literalIdx = textIdx - 1;
+            orderedSections.push_back({isec, literalIdx});
+            orderedNames[isec->name.str()] = literalIdx;
+          } else {
+            orderedSections.push_back({isec, sidx});
+            orderedNames[isec->name.str()] = sidx;
+          }
+        } else {
+          orderedSections.push_back({isec, sidx});
+          orderedNames[isec->name.str()] = sidx;
+        }
+        sidx += 2;
+      }
+
+      llvm::sort(orderedSections, llvm::less_second());
+      isd->sections.clear();
+      for (std::pair<InputSection *, int> p : orderedSections)
+        isd->sections.push_back(p.first);
+    }
+  }
+}
+
 static void sortSection(Ctx &ctx, OutputSection &osec,
                         const DenseMap<const InputSectionBase *, int> &order) {
   StringRef name = osec.name;
@@ -1233,6 +1286,10 @@ static void sortSection(Ctx &ctx, OutputSection &osec,
       if (auto *isd = dyn_cast<InputSectionDescription>(b))
         sortISDBySectionOrder(ctx, isd, order, osec.flags & SHF_EXECINSTR);
 
+  if (ctx.arg.emachine == EM_XTENSA) {
+    sortSectionXtensa(osec);
+  }
+
   if (ctx.script->hasSectionsCommand)
     return;
 
diff --git a/lld/test/ELF/xtensa-reloc.s b/lld/test/ELF/xtensa-reloc.s
new file mode 100644
index 0000000000000..cf0312166ee61
--- /dev/null
+++ b/lld/test/ELF/xtensa-reloc.s
@@ -0,0 +1,63 @@
+# REQUIRES: xtensa
+# RUN: llvm-mc -filetype=obj -triple=xtensa -mcpu=esp32 %s -o %t.o
+# RUN: ld.lld %t.o --defsym=a=0x2000 --section-start=.CALL=0x1000 --defsym=b=0x40 --defsym=c=0x140 --section-start=.BRANCH=0x5000 --defsym=d=0x5010 --section-start=.BR12=0x100 --image-base=0x0  -o %t
+# RUN: llvm-objdump -d --mcpu=esp32 --print-imm-hex %t | FileCheck %s
+
+.section .BR12,"ax",@progbits
+ .globl _start
+ .balign 0x100
+ .type _start,%function
+_start:
+# CHECK-LABEL:section .BR12
+# CHECK:      beqz a2, 0x140
+# CHECK-NEXT: bnez a3, 0x140
+# CHECK-NEXT: bgez a4, 0x140
+# CHECK-NEXT: bltz a5, 0x140
+  beqz a2, c
+  bnez a3, c
+  bgez a4, c
+  bltz a5, c
+
+.section .CALL,"ax",@progbits
+# CHECK-LABEL: section .CALL:
+# CHECK:      call0 0x2000
+# CHECK-NEXT: call0 0x2000
+# CHECK-NEXT: call0 0x2000
+# CHECK-NEXT: call0 0x2000
+# CHECK-NEXT: j     0x2000
+# CHECK-NEXT: j     0x2000
+# CHECK-NEXT: j     0x2000
+# CHECK-NEXT: j     0x40
+# CHECK-NEXT: j     0x140
+# CHECK-NEXT: call0 0x40
+# CHECK-NEXT: call0 0x140
+# CHECK-NEXT: l32r a3, 0x40
+# CHECK-NEXT: callx0 a3
+# CHECK-NEXT: l32r a4, 0x140
+# CHECK-NEXT: callx0 a4
+  call0 a
+  call0 a
+  call0 a
+  call0 a
+  j a
+  j a
+  j a
+  j b
+  j c
+  call0 b
+  call0 c
+  l32r a3, b
+  callx0 a3
+  l32r a4, c
+  callx0 a4
+
+.section .BRANCH,"ax",@progbits
+# CHECK-LABEL: section .BRANCH:
+# CHECK:      beq a3, a4, 0x5010
+# CHECK-NEXT: ball a3, a4, 0x5010
+# CHECK-NEXT: blt a3, a4, 0x5010
+# CHECK-NEXT: bt b0, 0x5010
+  beq a3, a4, d
+  ball a3, a4, d
+  blt a3, a4, d
+  bt b0, d
diff --git a/lld/test/lit.cfg.py b/lld/test/lit.cfg.py
index 39c3d0aa36bfb..ff4236a3d6762 100644
--- a/lld/test/lit.cfg.py
+++ b/lld/test/lit.cfg.py
@@ -99,6 +99,7 @@
                 "SystemZ": "systemz",
                 "WebAssembly": "wasm",
                 "X86": "x86",
+                'Xtensa': 'xtensa',
             },
         ),
         ("--assertion-mode", {"ON": "asserts"}),
diff --git a/llvm/include/llvm/BinaryFormat/ELFRelocs/Xtensa.def b/llvm/include/llvm/BinaryFormat/ELFRelocs/Xtensa.def
index 6791a842181ff..c2e11259164bb 100644
--- a/llvm/include/llvm/BinaryFormat/ELFRelocs/Xtensa.def
+++ b/llvm/include/llvm/BinaryFormat/ELFRelocs/Xtensa.def
@@ -58,3 +58,9 @@ ELF_RELOC(R_XTENSA_TLS_TPOFF,        53)
 ELF_RELOC(R_XTENSA_TLS_FUNC,         54)
 ELF_RELOC(R_XTENSA_TLS_ARG,          55)
 ELF_RELOC(R_XTENSA_TLS_CALL,         56)
+ELF_RELOC(R_XTENSA_PDIFF8,           57)
+ELF_RELOC(R_XTENSA_PDIFF16,          58)
+ELF_RELOC(R_XTENSA_PDIFF32,          59)
+ELF_RELOC(R_XTENSA_NDIFF8,           60)
+ELF_RELOC(R_XTENSA_NDIFF16,          61)
+ELF_RELOC(R_XTENSA_NDIFF32,          62)

@gerekon

gerekon commented Jun 10, 2026

Copy link
Copy Markdown
Contributor Author

@andreisfr PTAL
cc @MabezDev

@github-actions

github-actions Bot commented Jun 10, 2026

Copy link
Copy Markdown

✅ With the latest revision this PR passed the Python code formatter.

@github-actions

github-actions Bot commented Jun 10, 2026

Copy link
Copy Markdown

✅ With the latest revision this PR passed the C/C++ code formatter.

@gerekon

gerekon commented Jun 10, 2026

Copy link
Copy Markdown
Contributor Author

Related to espressif#130

@gerekon

gerekon commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

@MaskRay PTAL

Comment thread lld/ELF/Target.h Outdated
@gerekon
gerekon force-pushed the feat/xtensa_lld branch from dc3809d to 90a62c5 Compare July 8, 2026 12:39
@mstorsjo
mstorsjo removed their request for review July 15, 2026 18:42
Comment thread lld/ELF/Arch/Xtensa.cpp

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest to remove '>' from comment lines above.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown

🐧 Linux x64 Test Results

  • 200879 tests passed
  • 5479 tests skipped

✅ The build succeeded and all tests passed.

Comment thread lld/ELF/Writer.cpp
// try to find by name text(code) section, which uses these literals. The
// literal section should always be placed before code section. Also we try to
// place literal section just before code section.
static void sortSectionXtensa(OutputSection &osec) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very unusual - not sure covered by a test. Why do we need this? You should just use an explicit linker script with SORT.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants