[lld] [lld][ELF][RISCV] Add Zcmt table-jump relaxation support (PR #206338)

Ying Chen via llvm-commits llvm-commits at lists.llvm.org
Sun Jun 28 07:38:30 PDT 2026


https://github.com/punkyc created https://github.com/llvm/llvm-project/pull/206338

Implement opt-in RISC-V Zcmt table-jump relaxation for lld.

The new `--riscv-relax-zcmt` option rewrites profitable relaxable
`R_RISCV_CALL`, `R_RISCV_CALL_PLT`, and `R_RISCV_JAL` sites to `cm.jt` or
`cm.jalt` after ordinary RISC-V relaxation reaches a fixed point. The linker
emits the psABI `.riscv.jvt` section, defines hidden `__jvt_base$` when a table
is selected, and updates output RISC-V attributes when linker-generated Zcmt
instructions are emitted.

The relaxation is enabled only for final non-position-independent executable
links. PIE, shared-object output, static PIE, and relocatable links are
disabled. Dynamically linked non-PIE executables are supported: a direct table
entry stores `sym.getVA() + addend`, while an `R_PLT_PC` entry stores
`sym.getPltVA() + addend`, preserving the executable PLT path for preemptible
symbols.

Candidate selection is payload-profitable only. Sites are grouped by semantic
target, addend, direct-vs-PLT target, and JT-vs-JALT kind. JT candidates use
indices 0..31 and JALT candidates use indices 32..255. If no positive net
payload saving is found, lld emits no `.riscv.jvt`, defines no `__jvt_base$`,
and rewrites nothing, matching the psABI rule that table-jump relaxation is
ignored when there is no additional gain. If an input explicitly references
`__jvt_base$` but no profitable table is selected, lld reports an error because
there is no generated JVT for the runtime to initialize.

The implementation also:

* emits `.riscv.jvt` as psABI `SHT_PROGBITS` with `SHF_ALLOC | SHF_EXECINSTR`,
  minimum 64-byte alignment, and XLEN-sized entries;
* leaves CSR `jvt` initialization to the program/runtime;
* gates the transform on compatible RVC/Zcmt-capable inputs;
* rejects the incompatible Zcd/compressed-FP profile for this model;
* creates output RISC-V attributes when needed for linker-generated Zcmt code;
* removes stale call/JAL/relax relocations from `--emit-relocs` output for
  rewritten sites.

The previous Zcmt lld work can be found in #77884, #163142, and #183450, that provide a valuable reference. 

Per the LLVM AI tool policy, DOUBAO assisted with this PR: code correction check, the testcases. I have reviewed and understand all of it, ran the tests locally, and I am accountable for the contribution. The commits also carry an Assisted-by: DOUBAO trailer.



>From b71c13ff6072bd7250b2818e40d8150026bff66c Mon Sep 17 00:00:00 2001
From: punkyc <chenying at inchitech.com>
Date: Sun, 28 Jun 2026 16:27:10 +0800
Subject: [PATCH] Add Zcmt relaxation support

Implement --riscv-relax-zcmt for table jump relaxation, including
.riscv.jvt creation, __jvt_base$ handling, output attributes, mode/option
diagnostics, and focused ELF tests.

Assisted-by: DOUBAO
---
 lld/ELF/Arch/RISCV.cpp                    | 593 +++++++++++++++++++++-
 lld/ELF/Config.h                          |   1 +
 lld/ELF/Driver.cpp                        |   6 +
 lld/ELF/Options.td                        |   4 +
 lld/test/ELF/riscv-zcmt-addend.s          |  39 ++
 lld/test/ELF/riscv-zcmt-attributes.s      | 118 +++++
 lld/test/ELF/riscv-zcmt-discard-script.s  |  32 ++
 lld/test/ELF/riscv-zcmt-emit-relocs.s     |  31 ++
 lld/test/ELF/riscv-zcmt-gates.s           |  80 +++
 lld/test/ELF/riscv-zcmt-jal-already-rvc.s |  41 ++
 lld/test/ELF/riscv-zcmt-jvt-base.s        |  60 +++
 lld/test/ELF/riscv-zcmt-mixed-jt-jalt.s   |  32 ++
 lld/test/ELF/riscv-zcmt-no-profit.s       |  20 +
 lld/test/ELF/riscv-zcmt-norelax.s         |  24 +
 lld/test/ELF/riscv-zcmt-pic-disabled.s    |  22 +
 lld/test/ELF/riscv-zcmt-plt.s             |  35 ++
 lld/test/ELF/riscv-zcmt-relax.s           |  59 +++
 lld/test/ELF/riscv-zcmt-tie-order.s       |  35 ++
 lld/test/ELF/riscv-zcmt-vma-zero.s        |  33 ++
 lld/test/ELF/target-specific-options.s    |   4 +-
 20 files changed, 1260 insertions(+), 9 deletions(-)
 create mode 100644 lld/test/ELF/riscv-zcmt-addend.s
 create mode 100644 lld/test/ELF/riscv-zcmt-attributes.s
 create mode 100644 lld/test/ELF/riscv-zcmt-discard-script.s
 create mode 100644 lld/test/ELF/riscv-zcmt-emit-relocs.s
 create mode 100644 lld/test/ELF/riscv-zcmt-gates.s
 create mode 100644 lld/test/ELF/riscv-zcmt-jal-already-rvc.s
 create mode 100644 lld/test/ELF/riscv-zcmt-jvt-base.s
 create mode 100644 lld/test/ELF/riscv-zcmt-mixed-jt-jalt.s
 create mode 100644 lld/test/ELF/riscv-zcmt-no-profit.s
 create mode 100644 lld/test/ELF/riscv-zcmt-norelax.s
 create mode 100644 lld/test/ELF/riscv-zcmt-pic-disabled.s
 create mode 100644 lld/test/ELF/riscv-zcmt-plt.s
 create mode 100644 lld/test/ELF/riscv-zcmt-relax.s
 create mode 100644 lld/test/ELF/riscv-zcmt-tie-order.s
 create mode 100644 lld/test/ELF/riscv-zcmt-vma-zero.s

diff --git a/lld/ELF/Arch/RISCV.cpp b/lld/ELF/Arch/RISCV.cpp
index 576e7a4614572..24bc9b616a1ef 100644
--- a/lld/ELF/Arch/RISCV.cpp
+++ b/lld/ELF/Arch/RISCV.cpp
@@ -10,14 +10,17 @@
 #include "OutputSections.h"
 #include "RelocScan.h"
 #include "Symbols.h"
+#include "SymbolTable.h"
 #include "SyntheticSections.h"
 #include "Target.h"
+#include "llvm/ADT/DenseMap.h"
 #include "llvm/Support/ELFAttributes.h"
 #include "llvm/Support/LEB128.h"
 #include "llvm/Support/RISCVAttributeParser.h"
 #include "llvm/Support/RISCVAttributes.h"
 #include "llvm/Support/TimeProfiler.h"
 #include "llvm/TargetParser/RISCVISAInfo.h"
+#include <optional>
 
 using namespace llvm;
 using namespace llvm::object;
@@ -26,11 +29,63 @@ using namespace llvm::ELF;
 using namespace lld;
 using namespace lld::elf;
 
+static void addZcmtOutputAttributes(Ctx &ctx);
+static bool discardZcmtOutputAttributes(Ctx &ctx);
+static bool shouldCreateZcmtSyntheticSections(Ctx &ctx);
+
 namespace {
 
+struct ZcmtJvtEntry {
+  const Symbol *sym = nullptr;
+  int64_t addend = 0;
+  bool usePlt = false;
+};
+
+class RISCVJvtSection final : public SyntheticSection {
+public:
+  explicit RISCVJvtSection(Ctx &ctx)
+      : SyntheticSection(ctx, ".riscv.jvt", SHT_PROGBITS,
+                         SHF_ALLOC | SHF_EXECINSTR, 64) {
+    entsize = ctx.arg.wordsize;
+  }
+
+  size_t getSize() const override { return entries.size() * ctx.arg.wordsize; }
+  bool isNeeded() const override { return keep; }
+  void writeTo(uint8_t *buf) override {
+    for (const ZcmtJvtEntry &entry : entries) {
+      uint64_t va = 0;
+      if (entry.sym)
+        va = (entry.usePlt ? entry.sym->getPltVA(ctx)
+                           : entry.sym->getVA(ctx)) +
+             entry.addend;
+      if (ctx.arg.is64) {
+        write64(ctx, buf, va);
+        buf += 8;
+      } else {
+        write32(ctx, buf, va);
+        buf += 4;
+      }
+    }
+  }
+
+  bool keep = false;
+  SmallVector<ZcmtJvtEntry, 0> entries;
+};
+
+struct ZcmtCandidateGroup {
+  Symbol *sym = nullptr;
+  int64_t addend = 0;
+  bool usePlt = false;
+  bool isJalt = false;
+  uint64_t benefit = 0;
+  uint64_t firstSeen = 0;
+  SmallVector<std::pair<InputSection *, uint64_t>, 0> sites;
+};
+
 class RISCV final : public TargetInfo {
 public:
   RISCV(Ctx &);
+  void initTargetSpecificSections() override;
   uint32_t calcEFlags() const override;
   int64_t getImplicitAddend(const uint8_t *buf, RelType type) const override;
   void writeGotHeader(uint8_t *buf) const override;
@@ -64,6 +119,20 @@ class RISCV final : public TargetInfo {
   InputSection *baseSec = nullptr;
   // r_offset and r_addend pairs.
   SmallVector<std::pair<uint64_t, uint64_t>, 0> synthesizedAligns;
+
+  mutable std::unique_ptr<RISCVJvtSection> jvtSec;
+  mutable Defined *jvtSym = nullptr;
+  mutable DenseMap<std::pair<const InputSection *, uint64_t>, uint8_t>
+      zcmtSelectedSites;
+  mutable bool zcmtSelected = false;
+  mutable bool zcmtSelectionDone = false;
+  mutable bool zcmtWarnedDisabled = false;
+
+  bool zcmtEnabled() const;
+  std::optional<uint8_t> getZcmtIndex(const InputSection &sec,
+                                      uint64_t offset) const;
+  bool selectZcmtCandidates() const;
+  void discardEmptyJvt() const;
 };
 
 } // end anonymous namespace
@@ -74,6 +143,7 @@ class RISCV final : public TargetInfo {
 #define INTERNAL_R_RISCV_GPREL_S 257
 #define INTERNAL_R_RISCV_X0REL_I 258
 #define INTERNAL_R_RISCV_X0REL_S 259
+#define INTERNAL_R_RISCV_ZCMT_JUMP 260
 
 const uint64_t dtpOffset = 0x800;
 
@@ -128,6 +198,8 @@ static uint32_t setLO12_S(uint32_t insn, uint32_t imm) {
          (extractBits(imm, 4, 0) << 7);
 }
 
+static uint32_t getEFlags(Ctx &ctx, InputFile *f);
+
 RISCV::RISCV(Ctx &ctx) : TargetInfo(ctx) {
   copyRel = R_RISCV_COPY;
   pltRel = R_RISCV_JUMP_SLOT;
@@ -158,6 +230,113 @@ RISCV::RISCV(Ctx &ctx) : TargetInfo(ctx) {
   ipltEntrySize = 16;
 }
 
+void RISCV::initTargetSpecificSections() {
+  if (!shouldCreateZcmtSyntheticSections(ctx))
+    return;
+  jvtSec = std::make_unique<RISCVJvtSection>(ctx);
+  jvtSec->keep = true;
+  ctx.inputSections.push_back(jvtSec.get());
+
+  if (Symbol *s = ctx.symtab->find("__jvt_base$")) {
+    if (s->isDefined() || s->isCommon()) {
+      Err(ctx) << "duplicate definition of __jvt_base$";
+      return;
+    }
+    ctx.synthesizedSymbols.push_back(s);
+    s->resolve(ctx, Defined{ctx, ctx.internalFile, StringRef(), STB_GLOBAL,
+                            STV_HIDDEN, STT_NOTYPE, 0, 0, jvtSec.get()});
+    s->isUsedInRegularObj = true;
+    jvtSym = cast<Defined>(s);
+  }
+}
+
+static bool shouldCreateZcmtSyntheticSections(Ctx &ctx) {
+  return ctx.arg.relaxZcmt && !ctx.arg.shared && !ctx.arg.pie &&
+         !ctx.arg.relocatable && ctx.arg.relax;
+}
+
+static bool hasZcmtCompatibleProfile(Ctx &ctx, InputFile *file,
+                                     bool diagnose) {
+  for (InputSectionBase *sec : file->getSections()) {
+    if (!sec || sec == &InputSection::discarded ||
+        sec->type != SHT_RISCV_ATTRIBUTES)
+      continue;
+
+    RISCVAttributeParser parser;
+    if (Error e = parser.parse(sec->content(), llvm::endianness::little)) {
+      if (diagnose)
+        Warn(ctx) << "--riscv-relax-zcmt is disabled because " << sec
+                  << " could not be parsed: " << std::move(e);
+      else
+        consumeError(std::move(e));
+      return false;
+    }
+
+    std::optional<StringRef> arch = parser.getAttributeString(RISCVAttrs::ARCH);
+    if (!arch)
+      continue;
+
+    auto maybeInfo = RISCVISAInfo::parseNormalizedArchString(*arch);
+    if (!maybeInfo) {
+      if (diagnose)
+        Warn(ctx) << "--riscv-relax-zcmt is disabled because " << sec << ": "
+                  << *arch << ": " << maybeInfo.takeError();
+      else
+        consumeError(maybeInfo.takeError());
+      return false;
+    }
+
+    const RISCVISAInfo &info = **maybeInfo;
+    if (info.hasExtension("zcd")) {
+      if (diagnose)
+        Warn(ctx) << "--riscv-relax-zcmt is disabled because " << sec
+                  << " advertises incompatible Zcd/compressed-FP profile";
+      return false;
+    }
+  }
+
+  return true;
+}
+
+bool RISCV::zcmtEnabled() const {
+  if (!ctx.arg.relaxZcmt)
+    return false;
+
+  if (ctx.arg.shared || ctx.arg.pie) {
+    if (!zcmtWarnedDisabled) {
+      Warn(ctx) << "--riscv-relax-zcmt is disabled for PIE and shared links";
+      zcmtWarnedDisabled = true;
+    }
+    return false;
+  }
+
+  if (!ctx.arg.relax) {
+    if (!zcmtWarnedDisabled) {
+      Warn(ctx) << "--riscv-relax-zcmt requires --relax";
+      zcmtWarnedDisabled = true;
+    }
+    return false;
+  }
+
+  for (InputFile *file : ctx.objectFiles)
+    if ((getEFlags(ctx, file) & EF_RISCV_RVC) == 0) {
+      if (!zcmtWarnedDisabled) {
+        Warn(ctx) << "--riscv-relax-zcmt is disabled because " << file
+                  << " is not marked RVC-capable";
+        zcmtWarnedDisabled = true;
+      }
+      return false;
+    }
+
+  for (InputFile *file : ctx.objectFiles)
+    if (!hasZcmtCompatibleProfile(ctx, file, !zcmtWarnedDisabled)) {
+      zcmtWarnedDisabled = true;
+      return false;
+    }
+
+  return true;
+}
+
 static uint32_t getEFlags(Ctx &ctx, InputFile *f) {
   if (ctx.arg.is64)
     return cast<ObjFile<ELF64LE>>(f)->getObj().getHeader().e_flags;
@@ -697,6 +876,244 @@ static bool relaxable(ArrayRef<Relocation> relocs, size_t i) {
   return i + 1 != relocs.size() && relocs[i + 1].type == R_RISCV_RELAX;
 }
 
+static uint16_t encodeZcmtJump(uint8_t index) {
+  return 0xa002 | (uint16_t(index) << 2);
+}
+
+std::optional<uint8_t> RISCV::getZcmtIndex(const InputSection &sec,
+                                           uint64_t offset) const {
+  auto it = zcmtSelectedSites.find({&sec, offset});
+  if (it == zcmtSelectedSites.end())
+    return std::nullopt;
+  return it->second;
+}
+
+static std::optional<bool> getZcmtKind(const InputSection &sec,
+                                       const Relocation &r) {
+  uint32_t rd;
+  if (r.type == R_RISCV_JAL) {
+    uint32_t insn = read32le(sec.content().data() + r.offset);
+    rd = extractBits(insn, 11, 7);
+  } else {
+    uint64_t insnPair = read64le(sec.content().data() + r.offset);
+    rd = extractBits(insnPair, 32 + 11, 32 + 7);
+  }
+  if (rd == X_X0)
+    return false;
+  if (rd == X_RA)
+    return true;
+  return std::nullopt;
+}
+
+static bool sameZcmtGroup(const ZcmtCandidateGroup &group, const Symbol *sym,
+                          int64_t addend, bool usePlt, bool isJalt) {
+  return group.sym == sym && group.addend == addend && group.usePlt == usePlt &&
+         group.isJalt == isJalt;
+}
+
+static uint64_t getZcmtBenefit(RelType origType, RelType relaxedType) {
+  if (origType == R_RISCV_CALL || origType == R_RISCV_CALL_PLT) {
+    if (relaxedType == R_RISCV_NONE)
+      return 6;
+    if (relaxedType == R_RISCV_JAL)
+      return 2;
+    return 0;
+  }
+  if (origType == R_RISCV_JAL &&
+      (relaxedType == R_RISCV_NONE || relaxedType == R_RISCV_JAL))
+    return 2;
+  return 0;
+}
+
+static void addZcmtCandidate(SmallVectorImpl<ZcmtCandidateGroup> &groups,
+                             InputSection &sec, const Relocation &r,
+                             uint64_t benefit, bool isJalt,
+                             uint64_t ordinal) {
+  const bool usePlt = r.expr == R_PLT_PC;
+  for (ZcmtCandidateGroup &group : groups) {
+    if (!sameZcmtGroup(group, r.sym, r.addend, usePlt, isJalt))
+      continue;
+    group.benefit += benefit;
+    group.sites.push_back({&sec, r.offset});
+    return;
+  }
+
+  ZcmtCandidateGroup &group = groups.emplace_back();
+  group.sym = r.sym;
+  group.addend = r.addend;
+  group.usePlt = usePlt;
+  group.isJalt = isJalt;
+  group.benefit = benefit;
+  group.firstSeen = ordinal;
+  group.sites.push_back({&sec, r.offset});
+}
+
+bool RISCV::selectZcmtCandidates() const {
+  assert(jvtSec && "JVT synthetic section must exist before Zcmt selection");
+  zcmtSelectionDone = true;
+  zcmtSelectedSites.clear();
+  jvtSec->entries.clear();
+
+  SmallVector<ZcmtCandidateGroup, 0> jtGroups;
+  SmallVector<ZcmtCandidateGroup, 0> jaltGroups;
+  SmallVector<InputSection *, 0> storage;
+  uint64_t ordinal = 0;
+
+  for (OutputSection *osec : ctx.outputSections) {
+    if (!(osec->flags & SHF_EXECINSTR))
+      continue;
+    for (InputSection *sec : getInputSections(*osec, storage)) {
+      if (!sec->relaxAux || !sec->relaxAux->relocDeltas)
+        continue;
+
+      MutableArrayRef<Relocation> relocs = sec->relocs();
+      RelaxAux &aux = *sec->relaxAux;
+      for (auto [i, r] : llvm::enumerate(relocs)) {
+        if (r.type != R_RISCV_CALL && r.type != R_RISCV_CALL_PLT &&
+            r.type != R_RISCV_JAL)
+          continue;
+        if (!relaxable(relocs, i))
+          continue;
+        if (!r.sym || (r.sym->isUndefined() && !r.sym->isUndefWeak()))
+          continue;
+
+        std::optional<bool> kind = getZcmtKind(*sec, r);
+        if (!kind)
+          continue;
+
+        uint64_t benefit = getZcmtBenefit(r.type, aux.relocTypes[i]);
+        if (benefit == 0)
+          continue;
+
+        addZcmtCandidate(*kind ? jaltGroups : jtGroups, *sec, r, benefit,
+                         *kind, ordinal++);
+      }
+    }
+  }
+
+  auto orderGroups = [](const ZcmtCandidateGroup &a,
+                        const ZcmtCandidateGroup &b) {
+    if (a.benefit != b.benefit)
+      return a.benefit > b.benefit;
+    return a.firstSeen < b.firstSeen;
+  };
+  llvm::stable_sort(jtGroups, orderGroups);
+  llvm::stable_sort(jaltGroups, orderGroups);
+
+  SmallVector<uint64_t, 33> jtPrefix(1, 0);
+  for (size_t i = 0, e = std::min<size_t>(32, jtGroups.size()); i != e; ++i)
+    jtPrefix.push_back(jtPrefix.back() + jtGroups[i].benefit);
+  SmallVector<uint64_t, 225> jaltPrefix(1, 0);
+  for (size_t i = 0, e = std::min<size_t>(224, jaltGroups.size()); i != e; ++i)
+    jaltPrefix.push_back(jaltPrefix.back() + jaltGroups[i].benefit);
+
+  const int64_t entryWidth = ctx.arg.wordsize;
+  int64_t bestNet = 0;
+  size_t bestJt = 0;
+  size_t bestJalt = 0;
+  for (size_t k = 1, e = jtPrefix.size(); k != e; ++k) {
+    int64_t net = int64_t(jtPrefix[k]) - int64_t(k) * entryWidth;
+    if (net > bestNet) {
+      bestNet = net;
+      bestJt = k;
+      bestJalt = 0;
+    }
+  }
+
+  const size_t freeJt = std::min<size_t>(32, jtGroups.size());
+  for (size_t m = 1, e = jaltPrefix.size(); m != e; ++m) {
+    int64_t net = int64_t(jtPrefix[freeJt]) + int64_t(jaltPrefix[m]) -
+                  int64_t(32 + m) * entryWidth;
+    if (net > bestNet) {
+      bestNet = net;
+      bestJt = freeJt;
+      bestJalt = m;
+    }
+  }
+
+  if (bestNet <= 0) {
+    if (jvtSym)
+      Err(ctx) << "__jvt_base$ is referenced but no profitable Zcmt jump "
+                  "table was selected";
+    jvtSec->keep = false;
+    discardEmptyJvt();
+    return discardZcmtOutputAttributes(ctx);
+  }
+
+  auto addSelectedSites = [&](const ZcmtCandidateGroup &group, uint8_t index) {
+    for (auto [sec, offset] : group.sites)
+      zcmtSelectedSites[{sec, offset}] = index;
+  };
+  auto setEntry = [&](size_t index, const ZcmtCandidateGroup &group) {
+    jvtSec->entries[index] = {group.sym, group.addend, group.usePlt};
+  };
+
+  const size_t entryCount = bestJalt ? 32 + bestJalt : bestJt;
+  jvtSec->entries.resize(entryCount);
+  for (size_t i = 0; i != bestJt; ++i) {
+    addSelectedSites(jtGroups[i], i);
+    setEntry(i, jtGroups[i]);
+  }
+  for (size_t i = 0; i != bestJalt; ++i) {
+    const uint8_t index = 32 + i;
+    addSelectedSites(jaltGroups[i], index);
+    setEntry(index, jaltGroups[i]);
+  }
+
+  if (Symbol *s = ctx.symtab->find("__jvt_base$"))
+    if ((s->isDefined() || s->isCommon()) && s != jvtSym) {
+      Err(ctx) << "duplicate definition of __jvt_base$";
+      return false;
+    }
+  const bool hadEarlyJvtSym = jvtSym;
+  if (!jvtSym) {
+    jvtSym =
+        cast<Defined>(ctx.symtab->addSymbol(Defined{
+            ctx, ctx.internalFile, "__jvt_base$", STB_GLOBAL, STV_HIDDEN,
+            STT_NOTYPE, 0, 0, jvtSec.get()}));
+  }
+  if (ctx.in.symTab && !hadEarlyJvtSym)
+    ctx.in.symTab->addSymbol(jvtSym);
+
+  addZcmtOutputAttributes(ctx);
+  zcmtSelected = true;
+  return true;
+}
+
+void RISCV::discardEmptyJvt() const {
+  if (!jvtSec || !jvtSec->getParent())
+    return;
+
+  OutputSection *osec = jvtSec->getParent();
+  // A linker script may place .riscv.jvt into another output section such as
+  // .text. Only remove the parent when it is the orphan output section created
+  // solely for this synthetic input section.
+  const bool orphanJvt =
+      osec->name == ".riscv.jvt" &&
+      llvm::is_contained(ctx.script->orphanSections, jvtSec.get());
+  for (SectionCommand *cmd : osec->commands) {
+    auto *isd = dyn_cast<InputSectionDescription>(cmd);
+    if (!isd)
+      continue;
+    llvm::erase(isd->sectionBases, jvtSec.get());
+    llvm::erase(isd->sections, jvtSec.get());
+  }
+  llvm::erase(ctx.inputSections, jvtSec.get());
+  llvm::erase_if(ctx.script->orphanSections,
+                 [&](const InputSectionBase *sec) { return sec == jvtSec.get(); });
+  jvtSec->parent = nullptr;
+  if (orphanJvt) {
+    llvm::erase(ctx.outputSections, osec);
+    llvm::erase_if(ctx.script->sectionCommands, [&](const SectionCommand *cmd) {
+      if (auto *osd = dyn_cast<OutputDesc>(cmd))
+        return &osd->osec == osec;
+      return false;
+    });
+    for (auto [i, sec] : llvm::enumerate(ctx.outputSections))
+      sec->sectionIndex = i + 1;
+  }
+}
+
 static void tlsdescToIe(Ctx &ctx, uint8_t *loc, const Relocation &rel,
                         uint64_t val) {
   switch (rel.type) {
@@ -755,6 +1172,7 @@ void RISCV::relocateAlloc(InputSection &sec, uint8_t *buf) const {
     case R_RISCV_ALIGN:
     case R_RISCV_RELAX:
     case R_RISCV_TPREL_ADD:
+    case INTERNAL_R_RISCV_ZCMT_JUMP:
       continue;
     case R_RISCV_TLSDESC_HI20:
       if (rel.expr == R_TLSDESC_PC) {
@@ -1025,8 +1443,25 @@ static bool relax(Ctx &ctx, int pass, InputSection &sec) {
       }
       break;
     }
+    case R_RISCV_JAL:
+      if (std::optional<uint8_t> index =
+              static_cast<const RISCV &>(*ctx.target).getZcmtIndex(sec,
+                                                                   r.offset)) {
+        sec.relaxAux->relocTypes[i] = INTERNAL_R_RISCV_ZCMT_JUMP;
+        sec.relaxAux->writes.push_back(encodeZcmtJump(*index));
+        remove = 2;
+      }
+      break;
     case R_RISCV_CALL:
     case R_RISCV_CALL_PLT:
+      if (std::optional<uint8_t> index =
+              static_cast<const RISCV &>(*ctx.target).getZcmtIndex(sec,
+                                                                   r.offset)) {
+        sec.relaxAux->relocTypes[i] = INTERNAL_R_RISCV_ZCMT_JUMP;
+        sec.relaxAux->writes.push_back(encodeZcmtJump(*index));
+        remove = 6;
+        break;
+      }
       // Prevent oscillation between states by disallowing the increment of
       // `remove` after a few passes. The previous `remove` value is
       // `cur-delta`.
@@ -1106,6 +1541,17 @@ bool RISCV::relaxOnce(int pass) const {
   if (pass == 0)
     initSymbolAnchors(ctx);
 
+  const bool enableZcmt = zcmtEnabled();
+  if (!enableZcmt && jvtSec && jvtSec->keep) {
+    if (jvtSym)
+      Err(ctx) << "__jvt_base$ is referenced but --riscv-relax-zcmt could not "
+                  "emit a jump table";
+    jvtSec->keep = false;
+    discardEmptyJvt();
+    discardZcmtOutputAttributes(ctx);
+    zcmtSelectionDone = true;
+  }
+
   SmallVector<InputSection *, 0> storage;
   bool changed = false;
   for (OutputSection *osec : ctx.outputSections) {
@@ -1115,6 +1561,8 @@ bool RISCV::relaxOnce(int pass) const {
       if (sec->relaxAux)
         changed |= relax(ctx, pass, *sec);
   }
+  if (!changed && enableZcmt && !zcmtSelectionDone)
+    changed = selectZcmtCandidates();
   return changed;
 }
 
@@ -1310,6 +1758,10 @@ void RISCV::finalizeRelax(int passes) const {
             skip = 4;
             write32le(p, aux.writes[writesIdx++]);
             break;
+          case INTERNAL_R_RISCV_ZCMT_JUMP:
+            skip = 2;
+            write16le(p, aux.writes[writesIdx++]);
+            break;
           case R_RISCV_32:
             // Used by relaxTlsLe to write a uint32_t then suppress the handling
             // in relocateAlloc.
@@ -1340,6 +1792,21 @@ void RISCV::finalizeRelax(int passes) const {
         } while (++i != e && rels[i].offset == cur);
         delta = aux.relocDeltas[i - 1];
       }
+      if (zcmtSelected) {
+        bool eraseNextRelax = false;
+        llvm::erase_if(sec->relocations, [&](const Relocation &rel) {
+          if (rel.type == INTERNAL_R_RISCV_ZCMT_JUMP) {
+            eraseNextRelax = true;
+            return true;
+          }
+          if (eraseNextRelax && rel.type == R_RISCV_RELAX) {
+            eraseNextRelax = false;
+            return true;
+          }
+          eraseNextRelax = false;
+          return false;
+        });
+      }
     }
   }
 }
@@ -1358,12 +1825,16 @@ class RISCVAttributesSection final : public SyntheticSection {
   }
 
   size_t getSize() const override { return size; }
+  bool isNeeded() const override { return keep; }
   void writeTo(uint8_t *buf) override;
+  void updateSize();
 
   static constexpr StringRef vendor = "riscv";
   DenseMap<unsigned, unsigned> intAttr;
   DenseMap<unsigned, StringRef> strAttr;
   size_t size = 0;
+  bool keep = true;
+  bool createdForZcmt = false;
 };
 } // namespace
 
@@ -1571,17 +2042,113 @@ mergeAttributesSection(Ctx &ctx,
     }
   }
 
+  merged.updateSize();
+  return &merged;
+}
+
+static void addZcmtOutputAttributes(Ctx &ctx) {
+  if (!ctx.in.riscvAttributes)
+    return;
+
+  auto &attrs = static_cast<RISCVAttributesSection &>(*ctx.in.riscvAttributes);
+  unsigned xlen = ctx.arg.is64 ? 64 : 32;
+  RISCVISAUtils::OrderedExtensionMap exts;
+  if (StringRef arch = attrs.strAttr.lookup(RISCVAttrs::ARCH);
+      !arch.empty()) {
+    auto maybeInfo = RISCVISAInfo::parseNormalizedArchString(arch);
+    if (!maybeInfo) {
+      Err(ctx) << ".riscv.attributes: " << arch << ": "
+               << maybeInfo.takeError();
+      return;
+    }
+    RISCVISAInfo &info = **maybeInfo;
+    xlen = info.getXLen();
+    exts = info.getExtensions();
+  }
+
+  auto addExt = [&](StringRef name, RISCVISAUtils::ExtensionVersion version) {
+    auto [it, inserted] = exts.try_emplace(name.str(), version);
+    if (!inserted && std::tie(it->second.Major, it->second.Minor) <
+                         std::tie(version.Major, version.Minor))
+      it->second = version;
+  };
+  addExt("zca", {1, 0});
+  addExt("zicsr", {2, 0});
+  addExt("zcmt", {1, 0});
+
+  if (auto result = RISCVISAInfo::createFromExtMap(xlen, exts)) {
+    attrs.strAttr[RISCVAttrs::ARCH] = ctx.saver.save((*result)->toString());
+    attrs.updateSize();
+  } else {
+    Err(ctx) << result.takeError();
+  }
+}
+
+static bool discardZcmtOutputAttributes(Ctx &ctx) {
+  if (!ctx.in.riscvAttributes)
+    return false;
+
+  auto &attrs = static_cast<RISCVAttributesSection &>(*ctx.in.riscvAttributes);
+  // Drop the placeholder that was kept only so late Zcmt selection could
+  // populate attributes if it emitted table-jump instructions.
+  if (!attrs.createdForZcmt || attrs.size != 0)
+    return false;
+
+  attrs.keep = false;
+  OutputSection *osec = attrs.getParent();
+  if (!osec)
+    return false;
+
+  const bool orphanAttrs =
+      osec->name == ".riscv.attributes" &&
+      llvm::is_contained(ctx.script->orphanSections, &attrs);
+  for (SectionCommand *cmd : osec->commands) {
+    auto *isd = dyn_cast<InputSectionDescription>(cmd);
+    if (!isd)
+      continue;
+    llvm::erase(isd->sectionBases, &attrs);
+    llvm::erase(isd->sections, &attrs);
+  }
+  llvm::erase(ctx.inputSections, &attrs);
+  llvm::erase_if(ctx.script->orphanSections,
+                 [&](const InputSectionBase *sec) { return sec == &attrs; });
+  attrs.parent = nullptr;
+
+  if (!orphanAttrs)
+    return false;
+
+  // Orphan SHT_RISCV_ATTRIBUTES sections get an automatic PT_RISCV_ATTRIBUTES.
+  llvm::erase_if(ctx.phdrs, [&](const std::unique_ptr<PhdrEntry> &phdr) {
+    return phdr->p_type == PT_RISCV_ATTRIBUTES && phdr->firstSec == osec &&
+           phdr->lastSec == osec;
+  });
+  if (ctx.out.programHeaders)
+    ctx.out.programHeaders->size =
+        (ctx.arg.is64 ? sizeof(Elf64_Phdr) : sizeof(Elf32_Phdr)) *
+        ctx.phdrs.size();
+
+  llvm::erase(ctx.outputSections, osec);
+  llvm::erase_if(ctx.script->sectionCommands, [&](const SectionCommand *cmd) {
+    if (auto *osd = dyn_cast<OutputDesc>(cmd))
+      return &osd->osec == osec;
+    return false;
+  });
+  for (auto [i, sec] : llvm::enumerate(ctx.outputSections))
+    sec->sectionIndex = i + 1;
+  return true;
+}
+
+void RISCVAttributesSection::updateSize() {
   // The total size of headers: format-version [ <section-length> "vendor-name"
   // [ <file-tag> <size>.
-  size_t size = 5 + merged.vendor.size() + 1 + 5;
-  for (auto &attr : merged.intAttr)
+  size_t newSize = 5 + vendor.size() + 1 + 5;
+  for (auto &attr : intAttr)
     if (attr.second != 0)
-      size += getULEB128Size(attr.first) + getULEB128Size(attr.second);
-  for (auto &attr : merged.strAttr)
+      newSize += getULEB128Size(attr.first) + getULEB128Size(attr.second);
+  for (auto &attr : strAttr)
     if (!attr.second.empty())
-      size += getULEB128Size(attr.first) + attr.second.size() + 1;
-  merged.size = size;
-  return &merged;
+      newSize += getULEB128Size(attr.first) + attr.second.size() + 1;
+  size = newSize;
 }
 
 void RISCVAttributesSection::writeTo(uint8_t *buf) {
@@ -1619,8 +2186,18 @@ void elf::mergeRISCVAttributesSections(Ctx &ctx) {
       llvm::find_if(ctx.inputSections,
                     [](auto *s) { return s->type == SHT_RISCV_ATTRIBUTES; }) -
       ctx.inputSections.begin();
-  if (place == ctx.inputSections.size())
+  if (place == ctx.inputSections.size()) {
+    if (shouldCreateZcmtSyntheticSections(ctx)) {
+      // Zcmt selection runs after output sections are created. Insert an empty
+      // attributes section now so it can be populated if relaxation emits Zcmt.
+      ctx.in.riscvAttributes = std::make_unique<RISCVAttributesSection>(ctx);
+      auto &attrs =
+          static_cast<RISCVAttributesSection &>(*ctx.in.riscvAttributes);
+      attrs.createdForZcmt = true;
+      ctx.inputSections.push_back(&attrs);
+    }
     return;
+  }
 
   // Extract all SHT_RISCV_ATTRIBUTES sections into `sections`.
   SmallVector<InputSectionBase *, 0> sections;
diff --git a/lld/ELF/Config.h b/lld/ELF/Config.h
index 12b16ded61fca..fa054de5f8de8 100644
--- a/lld/ELF/Config.h
+++ b/lld/ELF/Config.h
@@ -397,6 +397,7 @@ struct Config {
   bool rejectMismatch;
   bool relax;
   bool relaxGP;
+  bool relaxZcmt;
   bool relocatable;
   bool resolveGroups;
   bool relrGlibc = false;
diff --git a/lld/ELF/Driver.cpp b/lld/ELF/Driver.cpp
index 7ec7dfcae6bca..a326df2dd4281 100644
--- a/lld/ELF/Driver.cpp
+++ b/lld/ELF/Driver.cpp
@@ -381,6 +381,8 @@ static void checkOptions(Ctx &ctx) {
   if (ctx.arg.emachine != EM_RISCV) {
     if (ctx.arg.relaxGP)
       ErrAlways(ctx) << "--relax-gp is only supported on RISC-V targets";
+    if (ctx.arg.relaxZcmt)
+      ErrAlways(ctx) << "--riscv-relax-zcmt is only supported on RISC-V targets";
     if (ctx.arg.zZicfilpUnlabeledReport != ReportPolicy::None)
       ErrAlways(ctx) << "-z zicfilip-unlabeled-report is only supported on "
                         "RISC-V targets";
@@ -429,6 +431,8 @@ static void checkOptions(Ctx &ctx) {
       ErrAlways(ctx) << "-r and --debug-names may not be used together";
     if (!ctx.arg.zSectionHeader)
       ErrAlways(ctx) << "-r and -z nosectionheader may not be used together";
+    if (ctx.arg.emachine == EM_RISCV && ctx.arg.relaxZcmt)
+      Warn(ctx) << "--riscv-relax-zcmt is disabled for relocatable links";
   }
 
   if (ctx.arg.executeOnly) {
@@ -1544,6 +1548,8 @@ static void readConfigs(Ctx &ctx, opt::InputArgList &args) {
   ctx.arg.rejectMismatch = !args.hasArg(OPT_no_warn_mismatch);
   ctx.arg.relax = args.hasFlag(OPT_relax, OPT_no_relax, true);
   ctx.arg.relaxGP = args.hasFlag(OPT_relax_gp, OPT_no_relax_gp, false);
+  ctx.arg.relaxZcmt =
+      args.hasFlag(OPT_riscv_relax_zcmt, OPT_no_riscv_relax_zcmt, false);
   ctx.arg.rpath = getRpath(args);
   ctx.arg.relocatable = args.hasArg(OPT_relocatable);
   ctx.arg.resolveGroups =
diff --git a/lld/ELF/Options.td b/lld/ELF/Options.td
index 64c42eb49607d..923a1e06db18b 100644
--- a/lld/ELF/Options.td
+++ b/lld/ELF/Options.td
@@ -426,6 +426,10 @@ defm relax_gp: BB<"relax-gp",
   "Enable global pointer relaxation",
   "Disable global pointer relaxation (default)">;
 
+defm riscv_relax_zcmt: BB<"riscv-relax-zcmt",
+  "Enable experimental RISC-V Zcmt table-jump relaxation",
+  "Disable experimental RISC-V Zcmt table-jump relaxation (default)">;
+
 defm remap_inputs: EEq<"remap-inputs",
   "Remap input files matching <from-glob> to <to-file>">,
   MetaVarName<"<from-glob>=<to-file>">;
diff --git a/lld/test/ELF/riscv-zcmt-addend.s b/lld/test/ELF/riscv-zcmt-addend.s
new file mode 100644
index 0000000000000..b64ddc75949c2
--- /dev/null
+++ b/lld/test/ELF/riscv-zcmt-addend.s
@@ -0,0 +1,39 @@
+# REQUIRES: riscv
+
+# RUN: llvm-mc -filetype=obj -triple=riscv64 -mattr=+c,+zcmt,+relax %s -o %t.o
+# RUN: ld.lld -e _start --riscv-relax-zcmt %t.o -o %t
+# RUN: llvm-objdump -d --no-show-raw-insn -M no-aliases %t | FileCheck %s
+
+# CHECK-LABEL: <_start>:
+# CHECK-NEXT: cm.jt 0x0
+# CHECK-NEXT: cm.jt 0x0
+# CHECK-NEXT: cm.jt 0x0
+# CHECK-NEXT: cm.jt 0x0
+# CHECK-NEXT: cm.jt 0x0
+# CHECK-NEXT: cm.jt 0x1
+# CHECK-NEXT: cm.jt 0x1
+# CHECK-NEXT: cm.jt 0x1
+# CHECK-NEXT: cm.jt 0x1
+# CHECK-NEXT: cm.jt 0x1
+# CHECK-NOT: cm.jt
+# CHECK-LABEL: <callee>:
+
+.attribute arch, "rv64i2p1_zicsr2p0_zca1p0_zcmt1p0"
+.option rvc
+.globl _start
+_start:
+  .rept 5
+  tail callee
+  .endr
+  .rept 5
+1:
+  auipc zero, 0
+  jalr zero, zero, 0
+  .reloc 1b, R_RISCV_CALL_PLT, callee+4
+  .reloc 1b, R_RISCV_RELAX
+  .endr
+  .space 4096
+callee:
+  nop
+  nop
+  ret
diff --git a/lld/test/ELF/riscv-zcmt-attributes.s b/lld/test/ELF/riscv-zcmt-attributes.s
new file mode 100644
index 0000000000000..dbe90c9c0df1d
--- /dev/null
+++ b/lld/test/ELF/riscv-zcmt-attributes.s
@@ -0,0 +1,118 @@
+# REQUIRES: riscv
+
+# RUN: rm -rf %t && split-file %s %t && cd %t
+
+# RUN: llvm-mc -filetype=obj -triple=riscv64 -mattr=+c,+relax noattr.s -o noattr.o
+# RUN: ld.lld -e _start --riscv-relax-zcmt noattr.o -o noattr
+# RUN: llvm-objdump -d --no-show-raw-insn -M no-aliases noattr | FileCheck %s --check-prefix=JUMP
+# RUN: llvm-readelf -S -s noattr | FileCheck %s --check-prefix=HAS-JVT
+# RUN: llvm-readelf -A noattr | FileCheck %s --check-prefix=ATTR
+
+# RUN: llvm-mc -filetype=obj -triple=riscv64 -mattr=+c,+relax noattr-nocandidate.s -o noattr-nocandidate.o
+# RUN: ld.lld -e _start --riscv-relax-zcmt noattr-nocandidate.o -o noattr-nocandidate
+# RUN: llvm-readelf -h -S -l -s -A noattr-nocandidate | FileCheck %s \
+# RUN:   --check-prefix=NO-ATTR --implicit-check-not=.riscv.attributes \
+# RUN:   --implicit-check-not=RISCV_ATTRIBUTES --implicit-check-not=__jvt_base$ \
+# RUN:   --implicit-check-not="TagName: arch"
+
+# RUN: llvm-mc -filetype=obj -triple=riscv64 -mattr=+c,+relax nozcmt.s -o nozcmt.o
+# RUN: ld.lld -e _start --riscv-relax-zcmt nozcmt.o -o nozcmt
+# RUN: llvm-objdump -d --no-show-raw-insn -M no-aliases nozcmt | FileCheck %s --check-prefix=JUMP
+# RUN: llvm-readelf -S -s nozcmt | FileCheck %s --check-prefix=HAS-JVT
+# RUN: llvm-readelf -A nozcmt | FileCheck %s --check-prefix=ATTR
+
+# RUN: llvm-mc -filetype=obj -triple=riscv64 -mattr=+c,+zcmt,+relax candidate.s -o candidate.o
+# RUN: llvm-mc -filetype=obj -triple=riscv64 -mattr=+c,+relax support-nozcmt.s -o support-nozcmt.o
+# RUN: ld.lld -e _start --riscv-relax-zcmt candidate.o support-nozcmt.o -o mixed
+# RUN: llvm-objdump -d --no-show-raw-insn -M no-aliases mixed | FileCheck %s --check-prefix=JUMP
+# RUN: llvm-readelf -S -s mixed | FileCheck %s --check-prefix=HAS-JVT
+# RUN: llvm-readelf -A mixed | FileCheck %s --check-prefix=ATTR
+
+# RUN: llvm-mc -filetype=obj -triple=riscv64 -mattr=+c,+relax compatible-nocandidate.s -o compatible-nocandidate.o
+# RUN: ld.lld -e _start --riscv-relax-zcmt compatible-nocandidate.o -o compatible-nocandidate
+# RUN: llvm-readelf -S -s compatible-nocandidate | FileCheck /dev/null \
+# RUN:   --implicit-check-not=.riscv.jvt --implicit-check-not=__jvt_base$
+
+# RUN: llvm-mc -filetype=obj -triple=riscv64 -mattr=+c,+zcmt,+relax zcd.s -o zcd.o
+# RUN: ld.lld -e _start --riscv-relax-zcmt zcd.o -o zcd 2>&1 | FileCheck %s --check-prefix=ZCD
+# RUN: llvm-readelf -S -s zcd | FileCheck /dev/null \
+# RUN:   --implicit-check-not=.riscv.jvt --implicit-check-not=__jvt_base$
+
+# JUMP: cm.jt
+# HAS-JVT: .riscv.jvt
+# HAS-JVT: __jvt_base$
+# ATTR: TagName: arch
+# ATTR-NEXT: Value:
+# ATTR-SAME: zicsr
+# ATTR-SAME: zca
+# ATTR-SAME: zcmt
+# NO-ATTR: ELF Header:
+# ZCD: warning: --riscv-relax-zcmt is disabled because
+# ZCD-SAME: advertises incompatible Zcd/compressed-FP profile
+
+#--- noattr.s
+.option rvc
+.globl _start
+_start:
+  .rept 5
+  tail callee
+  .endr
+  .space 4096
+callee:
+  ret
+
+#--- noattr-nocandidate.s
+.option rvc
+.globl _start
+_start:
+  ret
+
+#--- nozcmt.s
+.attribute arch, "rv64i2p1_zicsr2p0_zca1p0"
+.option rvc
+.globl _start
+_start:
+  .rept 5
+  tail callee
+  .endr
+  .space 4096
+callee:
+  ret
+
+#--- candidate.s
+.attribute arch, "rv64i2p1_zicsr2p0_zca1p0_zcmt1p0"
+.option rvc
+.globl _start
+_start:
+  .rept 5
+  tail callee
+  .endr
+  .space 4096
+callee:
+  ret
+
+#--- support-nozcmt.s
+.attribute arch, "rv64i2p1_zca1p0"
+.option rvc
+.globl support
+support:
+  ret
+
+#--- compatible-nocandidate.s
+.attribute arch, "rv64i2p1_zca1p0"
+.option rvc
+.globl _start
+_start:
+  ret
+
+#--- zcd.s
+.attribute arch, "rv64i2p1_zicsr2p0_zca1p0_zcd1p0"
+.option rvc
+.globl _start
+_start:
+  .rept 5
+  tail callee
+  .endr
+  .space 4096
+callee:
+  ret
diff --git a/lld/test/ELF/riscv-zcmt-discard-script.s b/lld/test/ELF/riscv-zcmt-discard-script.s
new file mode 100644
index 0000000000000..36a1ff980ebc5
--- /dev/null
+++ b/lld/test/ELF/riscv-zcmt-discard-script.s
@@ -0,0 +1,32 @@
+# REQUIRES: riscv
+
+# RUN: rm -rf %t && split-file %s %t && cd %t
+# RUN: llvm-mc -filetype=obj -triple=riscv64 -mattr=+c,+zcmt,+relax a.s -o a.o
+# RUN: ld.lld -T lds --riscv-relax-zcmt a.o -o a
+# RUN: llvm-readelf -S -s a | FileCheck %s --check-prefix=SEC \
+# RUN:   --implicit-check-not=.riscv.jvt --implicit-check-not=__jvt_base$
+# RUN: llvm-objdump -d --no-show-raw-insn -M no-aliases a | FileCheck %s \
+# RUN:   --check-prefix=DIS --implicit-check-not=cm.jt
+
+# SEC: .text
+# SEC-DAG: callee
+# SEC-DAG: _start
+
+# DIS-LABEL: <_start>:
+# DIS-NEXT: jal zero, {{.*}} <callee>
+
+#--- lds
+ENTRY(_start)
+SECTIONS {
+  .text : { *(.riscv.jvt) *(.text) }
+}
+
+#--- a.s
+.attribute arch, "rv64i2p1_zicsr2p0_zca1p0_zcmt1p0"
+.option rvc
+.globl _start
+_start:
+  tail callee
+  .space 4096
+callee:
+  ret
diff --git a/lld/test/ELF/riscv-zcmt-emit-relocs.s b/lld/test/ELF/riscv-zcmt-emit-relocs.s
new file mode 100644
index 0000000000000..a2935b6be1fa7
--- /dev/null
+++ b/lld/test/ELF/riscv-zcmt-emit-relocs.s
@@ -0,0 +1,31 @@
+# REQUIRES: riscv
+
+# RUN: llvm-mc -filetype=obj -triple=riscv64 -mattr=+c,+zcmt,+relax %s -o %t.o
+# RUN: ld.lld -e _start --emit-relocs --riscv-relax-zcmt %t.o -o %t
+# RUN: llvm-readelf -r %t | FileCheck %s --check-prefix=RELOCS \
+# RUN:   --implicit-check-not=R_RISCV_CALL \
+# RUN:   --implicit-check-not=R_RISCV_CALL_PLT \
+# RUN:   --implicit-check-not=R_RISCV_JAL \
+# RUN:   --implicit-check-not=R_RISCV_RELAX
+# RUN: llvm-objdump -d --no-show-raw-insn -M no-aliases %t | FileCheck %s \
+# RUN:   --check-prefix=DIS
+
+# RELOCS: Relocation section '.rela.text'
+
+# DIS-LABEL: <_start>:
+# DIS-NEXT: cm.jt 0
+# DIS-NEXT: cm.jt 0
+# DIS-NEXT: cm.jt 0
+# DIS-NEXT: cm.jt 0
+# DIS-NEXT: cm.jt 0
+
+.attribute arch, "rv64i2p1_zicsr2p0_zca1p0_zcmt1p0"
+.option rvc
+.globl _start
+_start:
+  .rept 5
+  tail callee
+  .endr
+  .space 4096
+callee:
+  ret
diff --git a/lld/test/ELF/riscv-zcmt-gates.s b/lld/test/ELF/riscv-zcmt-gates.s
new file mode 100644
index 0000000000000..05cccd9b40426
--- /dev/null
+++ b/lld/test/ELF/riscv-zcmt-gates.s
@@ -0,0 +1,80 @@
+# REQUIRES: riscv
+
+# RUN: rm -rf %t && split-file %s %t && cd %t
+# RUN: llvm-mc -filetype=obj -triple=riscv64 -mattr=+c,+zcmt,+relax zcmt.s -o zcmt.o
+# RUN: llvm-mc -filetype=obj -triple=riscv64 -mattr=+relax norvc.s -o norvc.o
+
+# RUN: ld.lld -e _start --riscv-relax-zcmt zcmt.o -o enabled
+# RUN: llvm-readelf -S -s enabled | FileCheck %s --check-prefix=HAS-JVT
+# RUN: llvm-objdump -d --no-show-raw-insn -M no-aliases enabled \
+# RUN:   | FileCheck %s --check-prefix=ZCMT
+
+# RUN: ld.lld -e _start zcmt.o -o default
+# RUN: llvm-readelf -S -s default | FileCheck /dev/null \
+# RUN:   --implicit-check-not=.riscv.jvt --implicit-check-not=__jvt_base$
+# RUN: llvm-objdump -d --no-show-raw-insn -M no-aliases default \
+# RUN:   | FileCheck %s --check-prefix=NOZCMT \
+# RUN:     --implicit-check-not=cm.jt --implicit-check-not=cm.jalt
+
+# RUN: ld.lld -e _start --riscv-relax-zcmt --no-riscv-relax-zcmt zcmt.o -o no-zcmt
+# RUN: llvm-readelf -S -s no-zcmt | FileCheck /dev/null \
+# RUN:   --implicit-check-not=.riscv.jvt --implicit-check-not=__jvt_base$
+# RUN: llvm-objdump -d --no-show-raw-insn -M no-aliases no-zcmt \
+# RUN:   | FileCheck %s --check-prefix=NOZCMT \
+# RUN:     --implicit-check-not=cm.jt --implicit-check-not=cm.jalt
+
+# RUN: ld.lld -e _start --no-relax --riscv-relax-zcmt zcmt.o -o no-relax 2>&1 \
+# RUN:   | FileCheck %s --check-prefix=NO-RELAX-WARN
+# RUN: llvm-readelf -S -s no-relax | FileCheck /dev/null \
+# RUN:   --implicit-check-not=.riscv.jvt --implicit-check-not=__jvt_base$
+# RUN: llvm-objdump -d --no-show-raw-insn -M no-aliases no-relax \
+# RUN:   | FileCheck %s --check-prefix=NO-RELAX-DIS \
+# RUN:     --implicit-check-not=cm.jt --implicit-check-not=cm.jalt
+
+# RUN: ld.lld -e _start --riscv-relax-zcmt norvc.o -o norvc 2>&1 \
+# RUN:   | FileCheck %s --check-prefix=NORVC-WARN
+# RUN: llvm-readelf -S -s norvc | FileCheck /dev/null \
+# RUN:   --implicit-check-not=.riscv.jvt --implicit-check-not=__jvt_base$
+# RUN: llvm-objdump -d --no-show-raw-insn -M no-aliases norvc \
+# RUN:   | FileCheck %s --check-prefix=NOZCMT \
+# RUN:     --implicit-check-not=cm.jt --implicit-check-not=cm.jalt
+
+# HAS-JVT: .riscv.jvt
+# HAS-JVT: __jvt_base$
+
+# ZCMT-LABEL: <_start>:
+# ZCMT-COUNT-5: cm.jt 0x0
+# ZCMT-NOT: cm.jt
+# ZCMT-LABEL: <callee>:
+
+# NOZCMT-LABEL: <_start>:
+# NOZCMT-NEXT: jal zero, {{.*}} <callee>
+
+# NO-RELAX-WARN: warning: --riscv-relax-zcmt requires --relax
+# NO-RELAX-DIS-LABEL: <_start>:
+# NO-RELAX-DIS-NEXT: auipc
+# NO-RELAX-DIS-NEXT: jalr zero, {{.*}} <callee>
+
+# NORVC-WARN: warning: --riscv-relax-zcmt is disabled because norvc.o is not marked RVC-capable
+
+#--- zcmt.s
+.attribute arch, "rv64i2p1_zicsr2p0_zca1p0_zcmt1p0"
+.option rvc
+.globl _start
+_start:
+  .rept 5
+  tail callee
+  .endr
+  .space 4096
+callee:
+  ret
+
+#--- norvc.s
+.globl _start
+_start:
+  .rept 5
+  tail callee
+  .endr
+  .space 4096
+callee:
+  ret
diff --git a/lld/test/ELF/riscv-zcmt-jal-already-rvc.s b/lld/test/ELF/riscv-zcmt-jal-already-rvc.s
new file mode 100644
index 0000000000000..331a57e8eca01
--- /dev/null
+++ b/lld/test/ELF/riscv-zcmt-jal-already-rvc.s
@@ -0,0 +1,41 @@
+# REQUIRES: riscv
+
+# RUN: rm -rf %t && split-file %s %t && cd %t
+
+# RUN: llvm-mc -filetype=obj -triple=riscv64 -mattr=+c,+zcmt,+relax tail64.s -o tail64.o
+# RUN: ld.lld -e _start --riscv-relax-zcmt tail64.o -o tail64
+# RUN: llvm-readelf -S -s tail64 | FileCheck /dev/null \
+# RUN:   --implicit-check-not=.riscv.jvt --implicit-check-not=__jvt_base$
+# RUN: llvm-objdump -d --no-show-raw-insn -M no-aliases tail64 \
+# RUN:   | FileCheck /dev/null --implicit-check-not=cm.jt \
+# RUN:     --implicit-check-not=cm.jalt
+
+# RUN: llvm-mc -filetype=obj -triple=riscv32 -mattr=+c,+zcmt,+relax call32.s -o call32.o
+# RUN: ld.lld -e _start --riscv-relax-zcmt call32.o -o call32
+# RUN: llvm-readelf -S -s call32 | FileCheck /dev/null \
+# RUN:   --implicit-check-not=.riscv.jvt --implicit-check-not=__jvt_base$
+# RUN: llvm-objdump -d --no-show-raw-insn -M no-aliases call32 \
+# RUN:   | FileCheck /dev/null --implicit-check-not=cm.jt \
+# RUN:     --implicit-check-not=cm.jalt
+
+#--- tail64.s
+.attribute arch, "rv64i2p1_zicsr2p0_zca1p0_zcmt1p0"
+.option rvc
+.globl _start
+_start:
+  .rept 5
+  tail callee
+  .endr
+callee:
+  ret
+
+#--- call32.s
+.attribute arch, "rv32i2p1_zicsr2p0_zca1p0_zcmt1p0"
+.option rvc
+.globl _start
+_start:
+  .rept 67
+  call callee
+  .endr
+callee:
+  ret
diff --git a/lld/test/ELF/riscv-zcmt-jvt-base.s b/lld/test/ELF/riscv-zcmt-jvt-base.s
new file mode 100644
index 0000000000000..d76e4817ae939
--- /dev/null
+++ b/lld/test/ELF/riscv-zcmt-jvt-base.s
@@ -0,0 +1,60 @@
+# REQUIRES: riscv
+
+# RUN: rm -rf %t && split-file %s %t && cd %t
+
+# RUN: llvm-mc -filetype=obj -triple=riscv64 -mattr=+c,+zcmt,+relax duplicate.s -o duplicate.o
+# RUN: not ld.lld -e _start --riscv-relax-zcmt duplicate.o -o duplicate 2>&1 | FileCheck %s --check-prefix=DUP
+
+# RUN: llvm-mc -filetype=obj -triple=riscv64 -mattr=+c,+zcmt,+relax ref-profit.s -o ref-profit.o
+# RUN: ld.lld -e _start --riscv-relax-zcmt ref-profit.o -o ref-profit
+# RUN: llvm-readelf -s ref-profit | FileCheck %s --check-prefix=REF --implicit-check-not=__jvt_base$
+
+# RUN: llvm-mc -filetype=obj -triple=riscv64 -mattr=+c,+zcmt,+relax ref-noprofit.s -o ref-noprofit.o
+# RUN: not ld.lld -e _start --riscv-relax-zcmt ref-noprofit.o -o ref-noprofit 2>&1 | FileCheck %s --check-prefix=REF-NOPROFIT
+
+# DUP: error: duplicate definition of __jvt_base$
+# REF: __jvt_base$
+# REF-NOPROFIT: error: __jvt_base$ is referenced but no profitable Zcmt jump table was selected
+
+#--- duplicate.s
+.attribute arch, "rv64i2p1_zicsr2p0_zca1p0_zcmt1p0"
+.option rvc
+.globl _start
+.globl __jvt_base$
+__jvt_base$:
+  nop
+_start:
+  .rept 5
+  tail callee
+  .endr
+  .space 4096
+callee:
+  ret
+
+#--- ref-profit.s
+.attribute arch, "rv64i2p1_zicsr2p0_zca1p0_zcmt1p0"
+.option rvc
+.globl _start
+.globl __jvt_base$
+_start:
+  la t0, __jvt_base$
+  csrw jvt, t0
+  .rept 5
+  tail callee
+  .endr
+  .space 4096
+callee:
+  ret
+
+#--- ref-noprofit.s
+.attribute arch, "rv64i2p1_zicsr2p0_zca1p0_zcmt1p0"
+.option rvc
+.globl _start
+.globl __jvt_base$
+_start:
+  la t0, __jvt_base$
+  csrw jvt, t0
+  tail callee
+  .space 4096
+callee:
+  ret
diff --git a/lld/test/ELF/riscv-zcmt-mixed-jt-jalt.s b/lld/test/ELF/riscv-zcmt-mixed-jt-jalt.s
new file mode 100644
index 0000000000000..94d1f57ffa162
--- /dev/null
+++ b/lld/test/ELF/riscv-zcmt-mixed-jt-jalt.s
@@ -0,0 +1,32 @@
+# REQUIRES: riscv
+
+# RUN: llvm-mc -filetype=obj -triple=riscv32 -mattr=+c,+zcmt,+relax %s -o %t.o
+# RUN: ld.lld -e _start --riscv-relax-zcmt %t.o -o %t
+# RUN: llvm-readelf -S -s %t | FileCheck %s --check-prefix=SEC
+# RUN: llvm-objdump -d --no-show-raw-insn -M no-aliases %t | FileCheck %s --check-prefix=DIS
+
+# SEC: .riscv.jvt
+# SEC-SAME: 000084
+# SEC: __jvt_base$
+# DIS-LABEL: <_start>:
+# DIS-NEXT: cm.jt 0x0
+# DIS-NEXT: cm.jt 0x0
+# DIS-NEXT: cm.jt 0x0
+# DIS-COUNT-67: cm.jalt 0x20
+# DIS-NOT: cm.jt
+# DIS-NOT: cm.jalt
+# DIS-LABEL: <callee>:
+
+.attribute arch, "rv32i2p1_zicsr2p0_zca1p0_zcmt1p0"
+.option rvc
+.globl _start
+_start:
+  .rept 3
+  tail callee
+  .endr
+  .rept 67
+  call callee
+  .endr
+  .space 4096
+callee:
+  ret
diff --git a/lld/test/ELF/riscv-zcmt-no-profit.s b/lld/test/ELF/riscv-zcmt-no-profit.s
new file mode 100644
index 0000000000000..3f030bb50ca4f
--- /dev/null
+++ b/lld/test/ELF/riscv-zcmt-no-profit.s
@@ -0,0 +1,20 @@
+# REQUIRES: riscv
+
+# RUN: llvm-mc -filetype=obj -triple=riscv64 -mattr=+c,+zcmt,+relax %s -o %t.o
+# RUN: ld.lld -e _start --riscv-relax-zcmt %t.o -o %t
+# RUN: llvm-readelf -S -s %t | FileCheck /dev/null \
+# RUN:   --implicit-check-not=.riscv.jvt --implicit-check-not=__jvt_base$
+# RUN: llvm-objdump -d --no-show-raw-insn -M no-aliases %t | FileCheck %s \
+# RUN:   --check-prefix=DIS --implicit-check-not=cm.jt
+
+# DIS-LABEL: <_start>:
+# DIS-NEXT: jal zero, {{.*}} <callee>
+
+.attribute arch, "rv64i2p1_zicsr2p0_zca1p0_zcmt1p0"
+.option rvc
+.globl _start
+_start:
+  tail callee
+  .space 4096
+callee:
+  ret
diff --git a/lld/test/ELF/riscv-zcmt-norelax.s b/lld/test/ELF/riscv-zcmt-norelax.s
new file mode 100644
index 0000000000000..71cc071c958c8
--- /dev/null
+++ b/lld/test/ELF/riscv-zcmt-norelax.s
@@ -0,0 +1,24 @@
+# REQUIRES: riscv
+
+# RUN: llvm-mc -filetype=obj -triple=riscv64 -mattr=+c,+zcmt,+relax %s -o %t.o
+# RUN: ld.lld -e _start --riscv-relax-zcmt %t.o -o %t
+# RUN: llvm-readelf -S -s %t | FileCheck /dev/null \
+# RUN:   --implicit-check-not=.riscv.jvt --implicit-check-not=__jvt_base$
+# RUN: llvm-objdump -d --no-show-raw-insn -M no-aliases %t | FileCheck %s \
+# RUN:   --check-prefix=DIS --implicit-check-not=cm.jt
+
+# DIS-LABEL: <_start>:
+# DIS-NEXT: auipc
+# DIS-NEXT: jalr zero, {{.*}} <callee>
+
+.attribute arch, "rv64i2p1_zicsr2p0_zca1p0_zcmt1p0"
+.option norelax
+.option rvc
+.globl _start
+_start:
+  .rept 5
+  tail callee
+  .endr
+  .space 4096
+callee:
+  ret
diff --git a/lld/test/ELF/riscv-zcmt-pic-disabled.s b/lld/test/ELF/riscv-zcmt-pic-disabled.s
new file mode 100644
index 0000000000000..236d24feb1786
--- /dev/null
+++ b/lld/test/ELF/riscv-zcmt-pic-disabled.s
@@ -0,0 +1,22 @@
+# REQUIRES: riscv
+
+# RUN: llvm-mc -filetype=obj -triple=riscv64 -mattr=+c,+zcmt,+relax %s -o %t.o
+# RUN: ld.lld -pie -e _start --riscv-relax-zcmt %t.o -o %t.pie 2>&1 | FileCheck %s --check-prefix=PIC-WARN
+# RUN: ld.lld -shared --riscv-relax-zcmt %t.o -o %t.so 2>&1 | FileCheck %s --check-prefix=PIC-WARN
+# RUN: ld.lld -r --riscv-relax-zcmt %t.o -o %t.r 2>&1 | FileCheck %s --check-prefix=REL-WARN
+# RUN: llvm-readelf -S -s %t.pie %t.so %t.r | FileCheck /dev/null \
+# RUN:   --implicit-check-not=.riscv.jvt --implicit-check-not=__jvt_base$
+
+# PIC-WARN: warning: --riscv-relax-zcmt is disabled for PIE and shared links
+# REL-WARN: warning: --riscv-relax-zcmt is disabled for relocatable links
+
+.attribute arch, "rv64i2p1_zicsr2p0_zca1p0_zcmt1p0"
+.option rvc
+.globl _start
+_start:
+  .rept 5
+  tail callee
+  .endr
+  .space 4096
+callee:
+  ret
diff --git a/lld/test/ELF/riscv-zcmt-plt.s b/lld/test/ELF/riscv-zcmt-plt.s
new file mode 100644
index 0000000000000..33e9822663e6c
--- /dev/null
+++ b/lld/test/ELF/riscv-zcmt-plt.s
@@ -0,0 +1,35 @@
+# REQUIRES: riscv
+
+# RUN: rm -rf %t && split-file %s %t && cd %t
+# RUN: llvm-mc -filetype=obj -triple=riscv64 -mattr=+c,+zcmt,+relax shared.s -o shared.o
+# RUN: ld.lld -shared shared.o -o shared.so
+# RUN: llvm-mc -filetype=obj -triple=riscv64 -mattr=+c,+zcmt,+relax main.s -o main.o
+# RUN: ld.lld -e _start --riscv-relax-zcmt main.o shared.so -o main
+# RUN: llvm-readelf -S -s main | FileCheck %s --check-prefix=SEC
+# RUN: llvm-objdump -d --no-show-raw-insn -M no-aliases main | FileCheck %s --check-prefix=DIS
+
+# SEC: .riscv.jvt
+# SEC: __jvt_base$
+# DIS-LABEL: <_start>:
+# DIS-NEXT: cm.jt 0
+# DIS-NEXT: cm.jt 0
+# DIS-NEXT: cm.jt 0
+# DIS-NEXT: cm.jt 0
+# DIS-NEXT: cm.jt 0
+
+#--- shared.s
+.attribute arch, "rv64i2p1_zicsr2p0_zca1p0_zcmt1p0"
+.text
+.globl foo
+foo:
+  ret
+
+#--- main.s
+.attribute arch, "rv64i2p1_zicsr2p0_zca1p0_zcmt1p0"
+.option rvc
+.globl _start
+_start:
+  .rept 5
+  tail foo at plt
+  .endr
+  .space 4096
diff --git a/lld/test/ELF/riscv-zcmt-relax.s b/lld/test/ELF/riscv-zcmt-relax.s
new file mode 100644
index 0000000000000..1a99a439c0dda
--- /dev/null
+++ b/lld/test/ELF/riscv-zcmt-relax.s
@@ -0,0 +1,59 @@
+# REQUIRES: riscv
+
+# RUN: rm -rf %t && split-file %s %t && cd %t
+
+# RUN: llvm-mc -filetype=obj -triple=riscv64 -mattr=+c,+zcmt,+relax jt.s -o jt.o
+# RUN: ld.lld -e _start --riscv-relax-zcmt jt.o -o jt
+# RUN: llvm-readelf -S -s jt | FileCheck %s --check-prefix=JT-SEC
+# RUN: llvm-objdump -d --no-show-raw-insn -M no-aliases jt | FileCheck %s --check-prefix=JT-DIS
+
+# JT-SEC: .riscv.jvt
+# JT-SEC-SAME: PROGBITS
+# JT-SEC-SAME: AX
+# JT-SEC-SAME: 64
+# JT-SEC: __jvt_base$
+# JT-DIS-LABEL: <_start>:
+# JT-DIS-NEXT: cm.jt 0x0
+# JT-DIS-NEXT: cm.jt 0x0
+# JT-DIS-NEXT: cm.jt 0x0
+# JT-DIS-NEXT: cm.jt 0x0
+# JT-DIS-NEXT: cm.jt 0x0
+# JT-DIS-NOT: cm.jt
+# JT-DIS-LABEL: <callee>:
+
+# RUN: llvm-mc -filetype=obj -triple=riscv32 -mattr=+c,+zcmt,+relax jalt.s -o jalt.o
+# RUN: ld.lld -e _start --riscv-relax-zcmt jalt.o -o jalt
+# RUN: llvm-readelf -S -s jalt | FileCheck %s --check-prefix=JALT-SEC
+# RUN: llvm-objdump -d --no-show-raw-insn -M no-aliases jalt | FileCheck %s --check-prefix=JALT-DIS
+
+# JALT-SEC: .riscv.jvt
+# JALT-SEC-SAME: 000084
+# JALT-SEC: __jvt_base$
+# JALT-DIS-LABEL: <_start>:
+# JALT-DIS-COUNT-67: cm.jalt 0x20
+# JALT-DIS-NOT: cm.jalt
+# JALT-DIS-LABEL: <callee>:
+
+#--- jt.s
+.attribute arch, "rv64i2p1_zicsr2p0_zca1p0_zcmt1p0"
+.option rvc
+.globl _start
+_start:
+  .rept 5
+  tail callee
+  .endr
+  .space 4096
+callee:
+  ret
+
+#--- jalt.s
+.attribute arch, "rv32i2p1_zicsr2p0_zca1p0_zcmt1p0"
+.option rvc
+.globl _start
+_start:
+  .rept 67
+  call callee
+  .endr
+  .space 4096
+callee:
+  ret
diff --git a/lld/test/ELF/riscv-zcmt-tie-order.s b/lld/test/ELF/riscv-zcmt-tie-order.s
new file mode 100644
index 0000000000000..35578d3306cfe
--- /dev/null
+++ b/lld/test/ELF/riscv-zcmt-tie-order.s
@@ -0,0 +1,35 @@
+# REQUIRES: riscv
+
+# RUN: llvm-mc -filetype=obj -triple=riscv64 -mattr=+c,+zcmt,+relax %s -o %t.o
+# RUN: ld.lld -e _start --riscv-relax-zcmt %t.o -o %t
+# RUN: llvm-objdump -d --no-show-raw-insn -M no-aliases %t | FileCheck %s
+
+# CHECK-LABEL: <_start>:
+# CHECK-NEXT: cm.jt 0x0
+# CHECK-NEXT: cm.jt 0x0
+# CHECK-NEXT: cm.jt 0x0
+# CHECK-NEXT: cm.jt 0x0
+# CHECK-NEXT: cm.jt 0x0
+# CHECK-NEXT: cm.jt 0x1
+# CHECK-NEXT: cm.jt 0x1
+# CHECK-NEXT: cm.jt 0x1
+# CHECK-NEXT: cm.jt 0x1
+# CHECK-NEXT: cm.jt 0x1
+# CHECK-NOT: cm.jt
+# CHECK-LABEL: <callee0>:
+
+.attribute arch, "rv64i2p1_zicsr2p0_zca1p0_zcmt1p0"
+.option rvc
+.globl _start
+_start:
+  .rept 5
+  tail callee0
+  .endr
+  .rept 5
+  tail callee1
+  .endr
+  .space 4096
+callee0:
+  ret
+callee1:
+  ret
diff --git a/lld/test/ELF/riscv-zcmt-vma-zero.s b/lld/test/ELF/riscv-zcmt-vma-zero.s
new file mode 100644
index 0000000000000..229bb755c2215
--- /dev/null
+++ b/lld/test/ELF/riscv-zcmt-vma-zero.s
@@ -0,0 +1,33 @@
+# REQUIRES: riscv
+
+# RUN: rm -rf %t && split-file %s %t && cd %t
+# RUN: llvm-mc -filetype=obj -triple=riscv64 -mattr=+c,+zcmt,+relax a.s -o a.o
+# RUN: ld.lld -T lds --riscv-relax-zcmt a.o -o a
+# RUN: llvm-readelf -s a | FileCheck %s --check-prefix=SYM
+# RUN: llvm-objdump -d --no-show-raw-insn -M no-aliases a | FileCheck %s --check-prefix=DIS
+
+# SYM: 0000000000000000 0 NOTYPE LOCAL DEFAULT 1 callee
+# DIS-LABEL: <_start>:
+# DIS-NEXT: cm.jt 0
+# DIS-NEXT: cm.jt 0
+
+#--- lds
+ENTRY(_start)
+SECTIONS {
+  . = 0;
+  .text : { *(.text.callee) *(.text.start) }
+}
+
+#--- a.s
+.attribute arch, "rv64i2p1_zicsr2p0_zca1p0_zcmt1p0"
+.option rvc
+.section .text.callee,"ax", at progbits
+callee:
+  ret
+.section .text.start,"ax", at progbits
+.space 4096
+.globl _start
+_start:
+  .rept 5
+  tail callee
+  .endr
diff --git a/lld/test/ELF/target-specific-options.s b/lld/test/ELF/target-specific-options.s
index 3d34ad294eb7b..45347cc464d02 100644
--- a/lld/test/ELF/target-specific-options.s
+++ b/lld/test/ELF/target-specific-options.s
@@ -21,11 +21,13 @@
 # CHECK-AARCH64-NEXT: error: -z gcs-report-dynamic only supported on AArch64
 # CHECK-AARCH64-NEXT: error: -z gcs only supported on AArch64
 
-# RUN: not ld.lld a.o --relax-gp -z zicfilp-unlabeled-report=warning \
+# RUN: not ld.lld a.o --relax-gp --riscv-relax-zcmt \
+# RUN:   -z zicfilp-unlabeled-report=warning \
 # RUN:   -z zicfilp-func-sig-report=warning -z zicfiss-report=warning \
 # RUN:   -z zicfilp=unlabeled -z zicfiss=always 2>&1 | \
 # RUN:   FileCheck %s --check-prefix=ERR-RISCV
 # ERR-RISCV:      error: --relax-gp is only supported on RISC-V targets
+# ERR-RISCV-NEXT: error: --riscv-relax-zcmt is only supported on RISC-V targets
 # ERR-RISCV-NEXT: error: -z zicfilip-unlabeled-report is only supported on RISC-V targets
 # ERR-RISCV-NEXT: error: -z zicfilip-func-sig-report is only supported on RISC-V targets
 # ERR-RISCV-NEXT: error: -z zicfiss-report is only supported on RISC-V targets



More information about the llvm-commits mailing list