[llvm-branch-commits] [lld] [lld][MachO] Support Objective-C class selector stubs (PR #219744)

Kyungwoo Lee via llvm-branch-commits llvm-branch-commits at lists.llvm.org
Wed Oct 7 14:54:21 PDT 2026


https://github.com/kyulee-com updated https://github.com/llvm/llvm-project/pull/219744

>From 6355224e0d548efd769112db78dd2150067ec766 Mon Sep 17 00:00:00 2001
From: Kyungwoo Lee <kyulee at meta.com>
Date: Sat, 29 Aug 2026 15:40:39 -0700
Subject: [PATCH] [lld][MachO] Support Objective-C class selector stubs

Support Objective-C class selector stubs in lld's Mach-O backend.

This consumes the class-stub symbols emitted by Clang in [ObjC] Emit class
msgSend stub calls (#186433), using the form
`_objc_msgSendClass$<selector>$_OBJC_CLASS_$_<class>`.

Class stubs load the class object into `x0`, load the selector into `x1`, and
branch to `_objc_msgSend`. Local class symbols are materialized directly with
`adrp`/`add`; dylib or dynamic class symbols are loaded indirectly.

The implementation also tracks the resolved class symbol before dead stripping
so live class stubs can retain the needed class definition without keeping dead
dylib references alive.
---
 lld/MachO/Arch/ARM64.cpp                      |  85 ++++
 lld/MachO/Driver.cpp                          |  40 +-
 lld/MachO/MarkLive.cpp                        |   4 +
 lld/MachO/SyntheticSections.cpp               | 125 +++++-
 lld/MachO/SyntheticSections.h                 |  21 +-
 lld/MachO/Target.h                            |   8 +
 lld/docs/ReleaseNotes.md                      |   4 +
 lld/test/MachO/arm64-objc-class-stubs-dead.s  |  82 ++++
 lld/test/MachO/arm64-objc-class-stubs.s       | 369 ++++++++++++++++++
 lld/test/MachO/objc-class-stubs-unsupported.s |  46 +++
 10 files changed, 762 insertions(+), 22 deletions(-)
 create mode 100644 lld/test/MachO/arm64-objc-class-stubs-dead.s
 create mode 100644 lld/test/MachO/arm64-objc-class-stubs.s
 create mode 100644 lld/test/MachO/objc-class-stubs-unsupported.s

diff --git a/lld/MachO/Arch/ARM64.cpp b/lld/MachO/Arch/ARM64.cpp
index 7acf48cbbf1233..9d925e88e160e1 100644
--- a/lld/MachO/Arch/ARM64.cpp
+++ b/lld/MachO/Arch/ARM64.cpp
@@ -33,6 +33,11 @@ struct ARM64 : ARM64Common {
   void writeObjCMsgSendStub(uint8_t *buf, Symbol *sym, uint64_t stubsAddr,
                             uint64_t &stubOffset, uint64_t selrefVA,
                             Symbol *objcMsgSend) const override;
+  void writeObjCMsgSendClassStub(uint8_t *buf, Symbol *sym, uint64_t stubsAddr,
+                                 uint64_t &stubOffset, uint64_t selrefVA,
+                                 Symbol *classSym,
+                                 Symbol *objcMsgSend) const override;
+  bool supportsObjCClassStubs() const override { return true; }
   void populateThunk(InputSection *thunk, Symbol *funcSym,
                      int64_t addend) override;
 
@@ -121,6 +126,27 @@ static constexpr uint32_t objcStubsSmallCode[] = {
     0x14000000, // b     _objc_msgSend
 };
 
+static constexpr uint32_t objcClassStubsFastCode[] = {
+    // A locally defined class uses adrp @page and add @pageoff instead.
+    0x90000000, // adrp  x0, _OBJC_CLASS_$_Foo at gotpage
+    0xf9400000, // ldr   x0, [x0, _OBJC_CLASS_$_Foo at gotpageoff]
+    0x90000001, // adrp  x1, @selector("foo")@page
+    0xf9400021, // ldr   x1, [x1, @selector("foo")@pageoff]
+    0x90000010, // adrp  x16, _objc_msgSend at gotpage
+    0xf9400210, // ldr   x16, [x16, _objc_msgSend at gotpageoff]
+    0xd61f0200, // br    x16
+    0xd4200020, // brk   #0x1
+};
+
+static constexpr uint32_t objcClassStubsSmallCode[] = {
+    // A locally defined class uses adrp @page and add @pageoff instead.
+    0x90000000, // adrp  x0, _OBJC_CLASS_$_Foo at gotpage
+    0xf9400000, // ldr   x0, [x0, _OBJC_CLASS_$_Foo at gotpageoff]
+    0x90000001, // adrp  x1, @selector("foo")@page
+    0xf9400021, // ldr   x1, [x1, @selector("foo")@pageoff]
+    0x14000000, // b     _objc_msgSend
+};
+
 void ARM64::writeObjCMsgSendStub(uint8_t *buf, Symbol *sym, uint64_t stubsAddr,
                                  uint64_t &stubOffset, uint64_t selrefVA,
                                  Symbol *objcMsgSend) const {
@@ -152,6 +178,63 @@ void ARM64::writeObjCMsgSendStub(uint8_t *buf, Symbol *sym, uint64_t stubsAddr,
   stubOffset += objcStubSize;
 }
 
+void ARM64::writeObjCMsgSendClassStub(uint8_t *buf, Symbol *stubSym,
+                                      uint64_t stubsAddr, uint64_t &stubOffset,
+                                      uint64_t selrefVA, Symbol *classSym,
+                                      Symbol *objcMsgSend) const {
+  SymbolDiagnostic d = {stubSym, stubSym->getName()};
+  auto *buf32 = reinterpret_cast<uint32_t *>(buf);
+
+  auto pcPageBits = [stubsAddr, stubOffset](int i) {
+    return pageBits(stubsAddr + stubOffset + i * sizeof(uint32_t));
+  };
+  auto writeClassLoad = [&](const uint32_t *code) {
+    uint64_t classVA;
+    uint32_t pageOffCode = code[1];
+    if (auto *defined = dyn_cast<Defined>(classSym)) {
+      classVA = defined->getVA();
+      pageOffCode = 0x91000000; // add x0, x0, _OBJC_CLASS_$_Foo at pageoff
+    } else {
+      classVA = in.got->addr + classSym->gotIndex * LP64::wordSize;
+    }
+    encodePage21(&buf32[0], d, code[0], pageBits(classVA) - pcPageBits(0));
+    encodePageOff12(&buf32[1], d, pageOffCode, classVA);
+  };
+
+  if (config->objcStubsMode == ObjCStubsMode::fast) {
+    writeClassLoad(objcClassStubsFastCode);
+    encodePage21(&buf32[2], d, objcClassStubsFastCode[2],
+                 pageBits(selrefVA) - pcPageBits(2));
+    encodePageOff12(&buf32[3], d, objcClassStubsFastCode[3], selrefVA);
+    uint64_t msgSendStubVA =
+        in.got->addr + objcMsgSend->gotIndex * LP64::wordSize;
+    encodePage21(&buf32[4], d, objcClassStubsFastCode[4],
+                 pageBits(msgSendStubVA) - pcPageBits(4));
+    encodePageOff12(&buf32[5], d, objcClassStubsFastCode[5], msgSendStubVA);
+    buf32[6] = objcClassStubsFastCode[6];
+    buf32[7] = objcClassStubsFastCode[7];
+    assert(target->objcClassStubsFastSize == 8 * sizeof(uint32_t));
+    stubOffset += target->objcClassStubsFastSize;
+    return;
+  }
+
+  assert(config->objcStubsMode == ObjCStubsMode::small);
+  writeClassLoad(objcClassStubsSmallCode);
+  encodePage21(&buf32[2], d, objcClassStubsSmallCode[2],
+               pageBits(selrefVA) - pcPageBits(2));
+  encodePageOff12(&buf32[3], d, objcClassStubsSmallCode[3], selrefVA);
+  uint64_t msgSendStubVA;
+  if (auto *defined = dyn_cast<Defined>(objcMsgSend))
+    msgSendStubVA = defined->getVA();
+  else
+    msgSendStubVA = in.stubs->addr + objcMsgSend->stubsIndex * target->stubSize;
+  uint64_t pcVA = stubsAddr + stubOffset + 4 * sizeof(uint32_t);
+  encodeBranch26(&buf32[4], {nullptr, "objc_msgSend class stub"},
+                 objcClassStubsSmallCode[4], msgSendStubVA - pcVA);
+  assert(target->objcClassStubsSmallSize == 5 * sizeof(uint32_t));
+  stubOffset += target->objcClassStubsSmallSize;
+}
+
 // A thunk is the relaxed variation of stubCode. We don't need the
 // extra indirection through a lazy pointer because the target address
 // is known at link time.
@@ -215,6 +298,8 @@ ARM64::ARM64() : ARM64Common(LP64()) {
   objcStubsFastAlignment = 32;
   objcStubsSmallSize = sizeof(objcStubsSmallCode);
   objcStubsSmallAlignment = 4;
+  objcClassStubsFastSize = sizeof(objcClassStubsFastCode);
+  objcClassStubsSmallSize = sizeof(objcClassStubsSmallCode);
 
   // Branch immediate is two's complement 26 bits, which is implicitly
   // multiplied by 4 (since all functions are 4-aligned: The branch range
diff --git a/lld/MachO/Driver.cpp b/lld/MachO/Driver.cpp
index 0c9a390a8cb307..368baae438a0d1 100644
--- a/lld/MachO/Driver.cpp
+++ b/lld/MachO/Driver.cpp
@@ -1591,13 +1591,41 @@ static void foldIdenticalLiterals() {
   in.wordLiteralSection->finalizeContents();
 }
 
-static void addSynthenticMethnames() {
+static void prepareObjCStubs() {
   std::string &data = *make<std::string>();
   llvm::raw_string_ostream os(data);
-  for (Symbol *sym : symtab->getSymbols())
-    if (isa<Undefined>(sym))
-      if (ObjCStubsSection::isObjCStubSymbol(sym))
-        os << ObjCStubsSection::getMethname(sym) << '\0';
+  // Iterate by index: resolving a class symbol may extract an archive member,
+  // which appends new symbols, including more stubs, to the symbol table.
+  for (size_t i = 0; i < symtab->getSymbols().size(); ++i) {
+    Symbol *sym = symtab->getSymbols()[i];
+    if (!isa<Undefined>(sym))
+      continue;
+    if (ObjCStubsSection::isObjCMsgSendStubSymbol(sym)) {
+      os << ObjCStubsSection::getMethname(sym) << '\0';
+      continue;
+    }
+    if (!ObjCStubsSection::isObjCClassStubSymbol(sym) ||
+        !target->supportsObjCClassStubs())
+      continue;
+    std::optional<ObjCStubsSection::ObjCClassStubNames> names =
+        ObjCStubsSection::parseObjCClassStubSymbol(sym);
+    if (!names)
+      continue;
+    os << names->selectorName << '\0';
+
+    Symbol *classSym = symtab->find(names->classSymbolName);
+    bool needsEarlyUndefinedClassRef =
+        !classSym || isa<LazyArchive>(classSym) || isa<LazyObject>(classSym);
+    // Missing symbols need a placeholder for diagnostics, and lazy symbols need
+    // extraction before markLive(). Do not call addUndefined() for DylibSymbol
+    // here: live stubs reference them later in ObjCStubsSection::addEntry(),
+    // and adding them here would mark real dylib symbols referenced before dead
+    // stripping.
+    if (needsEarlyUndefinedClassRef)
+      classSym = symtab->addUndefined(names->classSymbolName, /*file=*/nullptr,
+                                      /*isWeakRef=*/false);
+    in.objcStubs->recordClassSymbol(sym, classSym);
+  }
 
   if (data.empty())
     return;
@@ -2460,7 +2488,7 @@ bool link(ArrayRef<const char *> argsArr, llvm::raw_ostream &stdoutOS,
     if (config->thinLTOIndexOnly || config->emitLLVM)
       return errorCount() == 0;
 
-    addSynthenticMethnames();
+    prepareObjCStubs();
 
     // LTO may emit a non-hidden (extern) object file symbol even if the
     // corresponding bitcode symbol is hidden. In particular, this happens for
diff --git a/lld/MachO/MarkLive.cpp b/lld/MachO/MarkLive.cpp
index 4134c48ad48ad6..b74b8ea4eb4aa9 100644
--- a/lld/MachO/MarkLive.cpp
+++ b/lld/MachO/MarkLive.cpp
@@ -11,6 +11,7 @@
 #include "OutputSegment.h"
 #include "SymbolTable.h"
 #include "Symbols.h"
+#include "SyntheticSections.h"
 #include "UnwindInfoSection.h"
 
 #include "lld/Common/ErrorHandler.h"
@@ -107,6 +108,9 @@ void MarkLiveImpl<RecordWhyLive>::addSym(
   if constexpr (RecordWhyLive)
     if (!config->whyLive.empty() && config->whyLive.match(s->getName()))
       printWhyLive(s, prev);
+  if (ObjCStubsSection::isObjCClassStubSymbol(s))
+    if (auto *classSym = in.objcStubs->lookupClassSymbol(s))
+      addSym(classSym, prev);
   if (auto *d = dyn_cast<Defined>(s)) {
     if (d->isec())
       enqueue(d->isec(), d->value, prev);
diff --git a/lld/MachO/SyntheticSections.cpp b/lld/MachO/SyntheticSections.cpp
index 8bfdb32bdd66e9..e396966b7dcf90 100644
--- a/lld/MachO/SyntheticSections.cpp
+++ b/lld/MachO/SyntheticSections.cpp
@@ -878,29 +878,116 @@ ObjCStubsSection::ObjCStubsSection()
               : target->objcStubsSmallAlignment;
 }
 
-bool ObjCStubsSection::isObjCStubSymbol(Symbol *sym) {
+bool ObjCStubsSection::isObjCMsgSendStubSymbol(Symbol *sym) {
   return sym->getName().starts_with(objcMsgSendStubPrefix);
 }
 
+bool ObjCStubsSection::isObjCClassStubSymbol(Symbol *sym) {
+  return sym->getName().starts_with(objcMsgSendClassStubPrefix);
+}
+
+bool ObjCStubsSection::isObjCStubSymbol(Symbol *sym) {
+  return isObjCMsgSendStubSymbol(sym) || isObjCClassStubSymbol(sym);
+}
+
 StringRef ObjCStubsSection::getMethname(Symbol *sym) {
   assert(isObjCStubSymbol(sym) && "not an objc stub");
-  auto name = sym->getName();
-  return name.drop_front(objcMsgSendStubPrefix.size());
+  if (isObjCMsgSendStubSymbol(sym))
+    return sym->getName().drop_front(objcMsgSendStubPrefix.size());
+
+  assert(isObjCClassStubSymbol(sym) && "not an objc class stub");
+  std::optional<ObjCClassStubNames> parsed = parseObjCClassStubSymbol(sym);
+  assert(parsed && "malformed objc class stub");
+  return parsed->selectorName;
+}
+
+std::optional<ObjCStubsSection::ObjCClassStubNames>
+ObjCStubsSection::parseObjCClassStubSymbol(Symbol *sym) {
+  assert(isObjCClassStubSymbol(sym) && "not an objc class stub");
+  StringRef name = sym->getName().drop_front(objcMsgSendClassStubPrefix.size());
+  // For _objc_msgSendClass$alloc$_OBJC_CLASS_$_Foo, return
+  // selectorName "alloc" and classSymbolName "_OBJC_CLASS_$_Foo".
+  auto [selectorName, classSymbolName] = name.split('$');
+  if (selectorName.empty() || classSymbolName.empty() ||
+      !classSymbolName.starts_with(objcClassSymbolPrefix))
+    return std::nullopt;
+  return ObjCClassStubNames{selectorName, classSymbolName};
+}
+
+void ObjCStubsSection::recordClassSymbol(Symbol *stubSym, Symbol *classSym) {
+  // SymbolTable updates symbols in place with replaceSymbol(), so this pointer
+  // remains valid if an Undefined later becomes a DylibSymbol or Defined.
+  classSymbols[stubSym] = classSym;
+}
+
+Symbol *ObjCStubsSection::lookupClassSymbol(Symbol *stubSym) const {
+  return classSymbols.lookup(stubSym);
 }
 
-size_t ObjCStubsSection::getStubSize() const {
-  return config->objcStubsMode == ObjCStubsMode::fast
-             ? target->objcStubsFastSize
-             : target->objcStubsSmallSize;
+size_t ObjCStubsSection::getStubSize(Symbol *sym) const {
+  if (config->objcStubsMode == ObjCStubsMode::fast)
+    return isObjCClassStubSymbol(sym) ? target->objcClassStubsFastSize
+                                      : target->objcStubsFastSize;
+  return isObjCClassStubSymbol(sym) ? target->objcClassStubsSmallSize
+                                    : target->objcStubsSmallSize;
 }
 
 void ObjCStubsSection::addEntry(Symbol *sym) {
-  StringRef methname = getMethname(sym);
-  // We create a selref entry for each unique methname.
-  if (!ObjCSelRefsHelper::getSelRef(methname))
-    ObjCSelRefsHelper::makeSelRef(methname);
+  // Replace invalid stubs to avoid follow-on undefined-symbol diagnostics.
+  auto replaceWithEmptyStub = [&] {
+    replaceSymbol<Defined>(
+        sym, sym->getName(), nullptr, isec, /*value=*/stubsSize, /*size=*/0,
+        /*isWeakDef=*/false, /*isExternal=*/true, /*isPrivateExtern=*/true,
+        /*includeInSymtab=*/true, /*isReferencedDynamically=*/false,
+        /*noDeadStrip=*/false);
+  };
+
+  StringRef selectorName;
+  Symbol *classSym = nullptr;
+  if (isObjCClassStubSymbol(sym)) {
+    if (!target->supportsObjCClassStubs()) {
+      error("objc class stubs are not supported for " +
+            getArchitectureName(config->arch()));
+      replaceWithEmptyStub();
+      return;
+    }
+    std::optional<ObjCClassStubNames> parsed = parseObjCClassStubSymbol(sym);
+    if (!parsed) {
+      error("malformed objc class stub symbol " + sym->getName() +
+            "; expected " + objcMsgSendClassStubPrefix + "<selector>" + "$" +
+            objcClassSymbolPrefix + "<class>");
+      replaceWithEmptyStub();
+      return;
+    }
+    selectorName = parsed->selectorName;
+    classSym = lookupClassSymbol(sym);
+    if (!classSym) {
+      error("objc class stub symbol " + sym->getName() +
+            " references missing class symbol " + parsed->classSymbolName);
+      replaceWithEmptyStub();
+      return;
+    }
+    if (auto *undefined = dyn_cast<Undefined>(classSym)) {
+      treatUndefinedSymbol(*undefined, "objc class stub");
+      if (isa<Undefined>(classSym)) {
+        replaceWithEmptyStub();
+        return;
+      }
+    }
+    if (auto *dysym = dyn_cast<DylibSymbol>(classSym)) {
+      dysym->reference(RefState::Strong);
+      in.got->addEntry(dysym);
+    }
+    classSym->used = true;
+  } else {
+    selectorName = getMethname(sym);
+  }
+
+  size_t stubSize = getStubSize(sym);
+  // We create a selref entry for each unique selector.
+  if (!ObjCSelRefsHelper::getSelRef(selectorName))
+    ObjCSelRefsHelper::makeSelRef(selectorName);
 
-  size_t stubSize = getStubSize();
   Defined *newSym = replaceSymbol<Defined>(
       sym, sym->getName(), nullptr, isec,
       /*value=*/stubsSize,
@@ -948,7 +1035,7 @@ void ObjCStubsSection::sortSymbols(
   size_t stubOffset = 0;
   for (Defined *sym : symbols) {
     sym->value = stubOffset;
-    stubOffset += getStubSize();
+    stubOffset += getStubSize(sym);
   }
 }
 
@@ -959,8 +1046,16 @@ void ObjCStubsSection::writeTo(uint8_t *buf) const {
     InputSection *selRef = ObjCSelRefsHelper::getSelRef(methname);
     assert(selRef != nullptr && "no selref for methname");
     auto selrefAddr = selRef->getVA(0);
-    target->writeObjCMsgSendStub(buf + stubOffset, sym, in.objcStubs->addr,
-                                 stubOffset, selrefAddr, objcMsgSend);
+    if (isObjCClassStubSymbol(sym)) {
+      Symbol *classSym = classSymbols.lookup(sym);
+      assert(classSym != nullptr && "no class symbol for objc class stub");
+      target->writeObjCMsgSendClassStub(buf + stubOffset, sym,
+                                        in.objcStubs->addr, stubOffset,
+                                        selrefAddr, classSym, objcMsgSend);
+    } else {
+      target->writeObjCMsgSendStub(buf + stubOffset, sym, in.objcStubs->addr,
+                                   stubOffset, selrefAddr, objcMsgSend);
+    }
   }
 }
 
diff --git a/lld/MachO/SyntheticSections.h b/lld/MachO/SyntheticSections.h
index 713926b72af5b3..a0fc4812b9df99 100644
--- a/lld/MachO/SyntheticSections.h
+++ b/lld/MachO/SyntheticSections.h
@@ -24,6 +24,8 @@
 #include "llvm/Support/MathExtras.h"
 #include "llvm/Support/raw_ostream.h"
 
+#include <optional>
+
 namespace llvm {
 class DWARFUnit;
 } // namespace llvm
@@ -331,18 +333,35 @@ class ObjCStubsSection final : public SyntheticSection {
   void setUp();
 
   static constexpr llvm::StringLiteral objcMsgSendStubPrefix = "_objc_msgSend$";
+  static constexpr llvm::StringLiteral objcMsgSendClassStubPrefix =
+      "_objc_msgSendClass$";
+  static constexpr llvm::StringLiteral objcClassSymbolPrefix = "_OBJC_CLASS_$_";
+  // Plain selector stub: _objc_msgSend$<selector>.
+  static bool isObjCMsgSendStubSymbol(Symbol *sym);
+  // Class-message stub: _objc_msgSendClass$<selector>$_OBJC_CLASS_$_<class>.
+  static bool isObjCClassStubSymbol(Symbol *sym);
+  // Any symbol handled by ObjCStubsSection.
   static bool isObjCStubSymbol(Symbol *sym);
   static StringRef getMethname(Symbol *sym);
   ArrayRef<Defined *> getSymbols() const { return symbols; }
+  struct ObjCClassStubNames {
+    StringRef selectorName;
+    StringRef classSymbolName;
+  };
+  static std::optional<ObjCClassStubNames>
+  parseObjCClassStubSymbol(Symbol *sym);
+  void recordClassSymbol(Symbol *stubSym, Symbol *classSym);
+  Symbol *lookupClassSymbol(Symbol *stubSym) const;
 
   /// Stably sort the stubs by \p priorities and reassign their offsets. Must
   /// run before addresses are assigned.
   void sortSymbols(const llvm::DenseMap<const Symbol *, int> &priorities);
 
 private:
-  size_t getStubSize() const;
+  size_t getStubSize(Symbol *sym) const;
 
   std::vector<Defined *> symbols;
+  llvm::DenseMap<Symbol *, Symbol *> classSymbols;
   // Total byte size of all stubs added so far.
   size_t stubsSize = 0;
   Symbol *objcMsgSend = nullptr;
diff --git a/lld/MachO/Target.h b/lld/MachO/Target.h
index 0c7c0c3817c97d..4165b159c633f1 100644
--- a/lld/MachO/Target.h
+++ b/lld/MachO/Target.h
@@ -73,6 +73,12 @@ class TargetInfo {
                                     uint64_t stubsAddr, uint64_t &stubOffset,
                                     uint64_t selrefVA,
                                     Symbol *objcMsgSend) const = 0;
+  virtual void writeObjCMsgSendClassStub(uint8_t *, Symbol *, uint64_t,
+                                         uint64_t &, uint64_t, Symbol *,
+                                         Symbol *) const {
+    llvm_unreachable("target does not support objc class stubs");
+  }
+  virtual bool supportsObjCClassStubs() const { return false; }
 
   // Init 'thunk' so that it be a direct jump to 'branchTarget'.
   virtual void initICFSafeThunkBody(InputSection *thunk,
@@ -136,6 +142,8 @@ class TargetInfo {
   size_t stubHelperEntrySize;
   size_t objcStubsFastSize;
   size_t objcStubsSmallSize;
+  size_t objcClassStubsFastSize = 0;
+  size_t objcClassStubsSmallSize = 0;
   size_t objcStubsFastAlignment;
   size_t objcStubsSmallAlignment;
   uint8_t p2WordSize;
diff --git a/lld/docs/ReleaseNotes.md b/lld/docs/ReleaseNotes.md
index 827d09bec5776a..a0efe0155daf9b 100644
--- a/lld/docs/ReleaseNotes.md
+++ b/lld/docs/ReleaseNotes.md
@@ -40,6 +40,10 @@ from the [LLVM releases web site](https://llvm.org/releases/).
 * `__objc_stubs` entries are now ordered by the priority of the sections that
   call them, so that stubs reached from prioritized code are laid out together.
   This applies whenever section priorities exist, such as with `-order_file`.
+* Added support for Objective-C class-message stubs
+  (`_objc_msgSendClass$<selector>$_OBJC_CLASS_$_<class>`), which load the
+  class object, selector, and `objc_msgSend` target. Supported on arm64; other
+  architectures report an error.
 
 * Added `--warn-missing-subsections-via-symbols` and
   `--no-warn-missing-subsections-via-symbols` to lld to warn when input object
diff --git a/lld/test/MachO/arm64-objc-class-stubs-dead.s b/lld/test/MachO/arm64-objc-class-stubs-dead.s
new file mode 100644
index 00000000000000..f0f4d6322ea09d
--- /dev/null
+++ b/lld/test/MachO/arm64-objc-class-stubs-dead.s
@@ -0,0 +1,82 @@
+# REQUIRES: aarch64
+
+# Tests that -dead_strip keeps only live ObjC class stubs and does not keep dead
+# class definitions or dylib loads alive.
+
+# RUN: rm -rf %t && split-file %s %t
+
+# Check that only the live class-stub dependency survives dead stripping.
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/main.s -o %t/main.o
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/dylib.s \
+# RUN:   -o %t/dylib.o
+# RUN: %lld -arch arm64 -dylib -install_name @executable_path/libdead.dylib \
+# RUN:   -o %t/libdead.dylib %t/dylib.o
+# RUN: %lld -arch arm64 -lSystem -dead_strip -dead_strip_dylibs \
+# RUN:   -o %t.out %t/main.o %t/libdead.dylib -U _objc_msgSend \
+# RUN:   -objc_stubs_small
+# RUN: llvm-objdump --no-show-raw-insn --section=__TEXT,__objc_stubs \
+# RUN:   --macho %t.out | FileCheck %s --check-prefix=STUBS
+# RUN: llvm-nm %t.out | FileCheck %s --check-prefix=SYMS
+# RUN: llvm-otool -L %t.out | FileCheck %s --check-prefix=LOAD
+
+# STUBS:      Contents of (__TEXT,__objc_stubs) section
+# STUBS-NEXT: _objc_msgSendClass$live$_OBJC_CLASS_$_LiveClass:
+# STUBS-NOT:  _objc_msgSendClass$dead$_OBJC_CLASS_$_DeadClass:
+# STUBS-NOT:  _objc_msgSendClass$missing$_OBJC_CLASS_$_NeverDefined:
+# STUBS-NOT:  _objc_msgSendClass$dylib$_OBJC_CLASS_$_DylibClass:
+
+# SYMS-NOT: _OBJC_CLASS_$_DeadClass
+# SYMS-NOT: _OBJC_CLASS_$_NeverDefined
+# SYMS:     _OBJC_CLASS_$_LiveClass
+# SYMS-NOT: _OBJC_CLASS_$_DeadClass
+# SYMS-NOT: _OBJC_CLASS_$_NeverDefined
+
+# LOAD-NOT: libdead.dylib
+# LOAD:     /usr/lib/libSystem.dylib
+# LOAD-NOT: libdead.dylib
+
+#--- main.s
+# Contains one live class stub and several dead class-stub references.
+.text
+.globl _main
+_main:
+  bl _live
+  ret
+
+.globl _live
+_live:
+  bl _objc_msgSendClass$live$_OBJC_CLASS_$_LiveClass
+  ret
+
+.globl _dead
+_dead:
+  bl _objc_msgSendClass$dead$_OBJC_CLASS_$_DeadClass
+  ret
+
+.globl _missing
+_missing:
+  bl _objc_msgSendClass$missing$_OBJC_CLASS_$_NeverDefined
+  ret
+
+.globl _dylibdead
+_dylibdead:
+  bl _objc_msgSendClass$dylib$_OBJC_CLASS_$_DylibClass
+  ret
+
+.data
+.globl _OBJC_CLASS_$_LiveClass
+_OBJC_CLASS_$_LiveClass:
+  .quad 0
+
+.globl _OBJC_CLASS_$_DeadClass
+_OBJC_CLASS_$_DeadClass:
+  .quad 0
+
+.subsections_via_symbols
+
+#--- dylib.s
+# Provides a dylib class referenced only by dead code.
+.data
+.globl _OBJC_CLASS_$_DylibClass
+_OBJC_CLASS_$_DylibClass:
+  .quad 0
diff --git a/lld/test/MachO/arm64-objc-class-stubs.s b/lld/test/MachO/arm64-objc-class-stubs.s
new file mode 100644
index 00000000000000..6699d789fa4823
--- /dev/null
+++ b/lld/test/MachO/arm64-objc-class-stubs.s
@@ -0,0 +1,369 @@
+# REQUIRES: aarch64
+
+# Tests arm64 ObjC class stub emission for fast/small stubs, local/dylib/archive
+# classes, autolinked archives, dynamic lookup, malformed names, and missing
+# classes.
+
+# RUN: rm -rf %t && split-file %s %t
+
+# Check fast and small stubs for regular selector calls, local class calls, and
+# dylib class calls.
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/main.s -o %t/main.o
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/external.s -o %t/external.o
+# RUN: %lld -arch arm64 -dylib -install_name @executable_path/libexternal.dylib \
+# RUN:   -o %t/libexternal.dylib %t/external.o
+# RUN: %lld -arch arm64 -lSystem -o %t/fast.out %t/main.o \
+# RUN:   %t/libexternal.dylib -objc_stubs_fast
+# RUN: llvm-objdump --no-show-raw-insn --section=__TEXT,__objc_stubs --syms \
+# RUN:   --macho %t/fast.out | FileCheck %s --check-prefix=FAST
+# RUN: %lld -arch arm64 -lSystem -o %t/small.out %t/main.o \
+# RUN:   %t/libexternal.dylib -objc_stubs_small
+# RUN: llvm-objdump --no-show-raw-insn --section=__TEXT,__objc_stubs --syms \
+# RUN:   --macho %t/small.out | FileCheck %s --check-prefix=SMALL
+# RUN: llvm-mc -filetype=obj -triple=arm64e-apple-darwin %t/main.s \
+# RUN:   -o %t/main-arm64e.o
+# RUN: llvm-mc -filetype=obj -triple=arm64e-apple-darwin %t/external.s \
+# RUN:   -o %t/external-arm64e.o
+# RUN: %lld -arch arm64e -dylib -install_name @executable_path/libexternal.dylib \
+# RUN:   -o %t/libexternal-arm64e.dylib %t/external-arm64e.o
+# RUN: %lld -arch arm64e -lSystem -o %t/fast-arm64e.out %t/main-arm64e.o \
+# RUN:   %t/libexternal-arm64e.dylib -objc_stubs_fast
+# RUN: llvm-objdump --no-show-raw-insn --section=__TEXT,__objc_stubs --syms \
+# RUN:   --macho %t/fast-arm64e.out | FileCheck %s --check-prefix=FAST
+
+# Check archive extraction through class-stub references.
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/archive-main.s \
+# RUN:   -o %t/archive-main.o
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/archive-class.s \
+# RUN:   -o %t/archive-class.o
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/archive-nested.s \
+# RUN:   -o %t/archive-nested.o
+# RUN: llvm-ar rcs %t/libarchive.a %t/archive-class.o %t/archive-nested.o
+# RUN: %lld -arch arm64 -lSystem -o %t/archive.out %t/archive-main.o \
+# RUN:   %t/libarchive.a -objc_stubs_fast -U _objc_msgSend
+# RUN: llvm-objdump --no-show-raw-insn --section=__TEXT,__objc_stubs \
+# RUN:   --macho %t/archive.out | FileCheck %s --check-prefix=ARCHIVE
+# RUN: llvm-nm %t/archive.out | FileCheck %s --check-prefix=ARCHIVE-SYMS
+# RUN: %lld -arch arm64 -lSystem -o %t/start-lib.out %t/archive-main.o \
+# RUN:   --start-lib %t/archive-class.o %t/archive-nested.o --end-lib \
+# RUN:   -objc_stubs_fast -U _objc_msgSend
+# RUN: llvm-objdump --no-show-raw-insn --section=__TEXT,__objc_stubs \
+# RUN:   --macho %t/start-lib.out | FileCheck %s --check-prefix=ARCHIVE
+# RUN: llvm-nm %t/start-lib.out | FileCheck %s --check-prefix=ARCHIVE-SYMS
+
+# Check autolink extraction when the archive member defines the class.
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/autolink-main-a.s \
+# RUN:   -o %t/autolink-main-a.o
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/autolink-dep-a.s \
+# RUN:   -o %t/autolink-dep-a.o
+# RUN: llvm-ar rcs %t/libautolinka.a %t/autolink-dep-a.o
+# RUN: %lld -arch arm64 -lSystem -o %t/autolink-a.out \
+# RUN:   %t/autolink-main-a.o -L%t -objc_stubs_fast -U _objc_msgSend
+# RUN: llvm-objdump --no-show-raw-insn --section=__TEXT,__objc_stubs \
+# RUN:   --macho %t/autolink-a.out | FileCheck %s --check-prefix=AUTOLINK-A
+
+# Check autolink extraction when the class is already known.
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/autolink-main-b.s \
+# RUN:   -o %t/autolink-main-b.o
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/autolink-dep-b.s \
+# RUN:   -o %t/autolink-dep-b.o
+# RUN: llvm-ar rcs %t/libautolinkb.a %t/autolink-dep-b.o
+# RUN: %lld -arch arm64 -lSystem -dead_strip -o %t/autolink-b.out \
+# RUN:   %t/autolink-main-b.o -L%t -objc_stubs_fast -U _objc_msgSend
+# RUN: llvm-objdump --no-show-raw-insn --section=__TEXT,__objc_stubs \
+# RUN:   --macho %t/autolink-b.out | FileCheck %s --check-prefix=AUTOLINK-B
+# RUN: llvm-nm %t/autolink-b.out | FileCheck %s --check-prefix=AUTOLINK-B-SYMS
+
+# Check class stubs for dynamic class symbols.
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/dynamic.s \
+# RUN:   -o %t/dynamic.o
+# RUN: %lld -arch arm64 -lSystem -o %t/dynamic.out %t/dynamic.o \
+# RUN:   -objc_stubs_fast -U '_OBJC_CLASS_$_DynamicClass' -U _objc_msgSend
+# RUN: llvm-objdump --no-show-raw-insn --section=__TEXT,__objc_stubs \
+# RUN:   --macho %t/dynamic.out | FileCheck %s --check-prefix=DYNAMIC
+# RUN: %lld -arch arm64 -lSystem -o %t/dynamic-lookup.out %t/dynamic.o \
+# RUN:   -objc_stubs_fast -undefined dynamic_lookup
+# RUN: llvm-objdump --no-show-raw-insn --section=__TEXT,__objc_stubs \
+# RUN:   --macho %t/dynamic-lookup.out | FileCheck %s --check-prefix=DYNAMIC
+
+# Check diagnostics for missing class symbols.
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/missing.s \
+# RUN:   -o %t/missing.o
+# RUN: not %lld -arch arm64 -lSystem -o /dev/null %t/missing.o \
+# RUN:   -objc_stubs_fast 2>&1 | FileCheck %s --check-prefix=MISSING
+
+# Check diagnostics for malformed class-stub symbol names.
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/malformed.s \
+# RUN:   -o %t/malformed.o
+# RUN: not %lld -arch arm64 -lSystem -o /dev/null %t/malformed.o \
+# RUN:   -objc_stubs_fast 2>&1 | FileCheck %s --check-prefix=MALFORMED
+
+# FAST:      Contents of (__TEXT,__objc_stubs) section
+# FAST:      _objc_msgSend$instance:
+# FAST-NEXT: adrp    x1,
+# FAST-NEXT: ldr     x1, {{.*}} ; Objc selector ref: instance
+# FAST-NEXT: adrp    x16,
+# FAST-NEXT: ldr     x16,
+# FAST-NEXT: br      x16
+# FAST-NEXT: brk     #0x1
+# FAST-NEXT: brk     #0x1
+# FAST-NEXT: brk     #0x1
+# FAST-NEXT: _objc_msgSendClass$external$_OBJC_CLASS_$_ExternalClass:
+# FAST-NEXT: adrp    x0,
+# FAST-NEXT: ldr     x0, {{.*}} ; literal pool symbol address: _OBJC_CLASS_$_ExternalClass
+# FAST-NEXT: adrp    x1,
+# FAST-NEXT: ldr     x1, {{.*}} ; Objc selector ref: external
+# FAST-NEXT: adrp    x16,
+# FAST-NEXT: ldr     x16,
+# FAST-NEXT: br      x16
+# FAST-NEXT: brk     #0x1
+# FAST-NEXT: _objc_msgSendClass$local$_OBJC_CLASS_$_LocalClass:
+# FAST-NEXT: adrp    x0, [[#]] ; 0x[[#%x,FAST_LOCAL_CLASS_PAGE:]]
+# FAST-NEXT: add     x0, x0, #0x[[#%x,FAST_LOCAL_CLASS_OFF:]]
+# FAST-NEXT: adrp    x1,
+# FAST-NEXT: ldr     x1, {{.*}} ; Objc selector ref: local
+# FAST-NEXT: adrp    x16,
+# FAST-NEXT: ldr     x16,
+# FAST-NEXT: br      x16
+# FAST-NEXT: brk     #0x1
+# FAST-LABEL: SYMBOL TABLE:
+# FAST-DAG: {{0*}}[[#%x,FAST_LOCAL_CLASS_PAGE+FAST_LOCAL_CLASS_OFF]] g     O __DATA,__data _OBJC_CLASS_$_LocalClass
+
+# SMALL:      Contents of (__TEXT,__objc_stubs) section
+# SMALL:      _objc_msgSend$instance:
+# SMALL-NEXT: adrp    x1,
+# SMALL-NEXT: ldr     x1, {{.*}} ; Objc selector ref: instance
+# SMALL-NEXT: b       _objc_msgSend
+# SMALL-NEXT: _objc_msgSendClass$external$_OBJC_CLASS_$_ExternalClass:
+# SMALL-NEXT: adrp    x0,
+# SMALL-NEXT: ldr     x0, {{.*}} ; literal pool symbol address: _OBJC_CLASS_$_ExternalClass
+# SMALL-NEXT: adrp    x1,
+# SMALL-NEXT: ldr     x1, {{.*}} ; Objc selector ref: external
+# SMALL-NEXT: b       _objc_msgSend
+# SMALL-NEXT: _objc_msgSendClass$local$_OBJC_CLASS_$_LocalClass:
+# SMALL-NEXT: adrp    x0, [[#]] ; 0x[[#%x,SMALL_LOCAL_CLASS_PAGE:]]
+# SMALL-NEXT: add     x0, x0, #0x[[#%x,SMALL_LOCAL_CLASS_OFF:]]
+# SMALL-NEXT: adrp    x1,
+# SMALL-NEXT: ldr     x1, {{.*}} ; Objc selector ref: local
+# SMALL-NEXT: b       _objc_msgSend
+# SMALL-LABEL: SYMBOL TABLE:
+# SMALL-DAG: {{0*}}[[#%x,SMALL_LOCAL_CLASS_PAGE+SMALL_LOCAL_CLASS_OFF]] g     O __DATA,__data _OBJC_CLASS_$_LocalClass
+
+# DYNAMIC:      Contents of (__TEXT,__objc_stubs) section
+# DYNAMIC-NEXT: _objc_msgSendClass$dynamic$_OBJC_CLASS_$_DynamicClass:
+# DYNAMIC-NEXT: adrp    x0,
+# DYNAMIC-NEXT: ldr     x0, {{.*}} ; literal pool symbol address: _OBJC_CLASS_$_DynamicClass
+# DYNAMIC-NEXT: adrp    x1,
+# DYNAMIC-NEXT: ldr     x1, {{.*}} ; Objc selector ref: dynamic
+# DYNAMIC-NEXT: adrp    x16,
+# DYNAMIC-NEXT: ldr     x16, {{.*}} ; literal pool symbol address: _objc_msgSend
+# DYNAMIC-NEXT: br      x16
+# DYNAMIC-NEXT: brk     #0x1
+
+# ARCHIVE:      Contents of (__TEXT,__objc_stubs) section
+# ARCHIVE-NEXT: _objc_msgSendClass$archive$_OBJC_CLASS_$_ArchiveClass:
+# ARCHIVE-NEXT: adrp    x0,
+# ARCHIVE-NEXT: add     x0, x0,
+# ARCHIVE-NEXT: adrp    x1,
+# ARCHIVE-NEXT: ldr     x1, {{.*}} ; Objc selector ref: archive
+# ARCHIVE-NEXT: adrp    x16,
+# ARCHIVE-NEXT: ldr     x16, {{.*}} ; literal pool symbol address: _objc_msgSend
+# ARCHIVE-NEXT: br      x16
+# ARCHIVE-NEXT: brk     #0x1
+# ARCHIVE-NEXT: _objc_msgSendClass$nested$_OBJC_CLASS_$_NestedClass:
+# ARCHIVE-NEXT: adrp    x0,
+# ARCHIVE-NEXT: add     x0, x0,
+#
+# ARCHIVE-SYMS: _OBJC_CLASS_$_ArchiveClass
+# ARCHIVE-SYMS: _OBJC_CLASS_$_NestedClass
+
+# AUTOLINK-A:      Contents of (__TEXT,__objc_stubs) section
+# AUTOLINK-A-NEXT: _objc_msgSendClass$auto$_OBJC_CLASS_$_AutoClass:
+# AUTOLINK-A-NEXT: adrp    x0,
+# AUTOLINK-A-NEXT: add     x0, x0,
+# AUTOLINK-A-NEXT: adrp    x1,
+# AUTOLINK-A-NEXT: ldr     x1, {{.*}} ; Objc selector ref: auto
+# AUTOLINK-A-NEXT: adrp    x16,
+# AUTOLINK-A-NEXT: ldr     x16, {{.*}} ; literal pool symbol address: _objc_msgSend
+# AUTOLINK-A-NEXT: br      x16
+# AUTOLINK-A-NEXT: brk     #0x1
+#
+# AUTOLINK-B:      Contents of (__TEXT,__objc_stubs) section
+# AUTOLINK-B-NEXT: _objc_msgSendClass$late$_OBJC_CLASS_$_LateClass:
+# AUTOLINK-B-NEXT: adrp    x0,
+# AUTOLINK-B-NEXT: add     x0, x0,
+# AUTOLINK-B-NEXT: adrp    x1,
+# AUTOLINK-B-NEXT: ldr     x1, {{.*}} ; Objc selector ref: late
+# AUTOLINK-B-NEXT: adrp    x16,
+# AUTOLINK-B-NEXT: ldr     x16, {{.*}} ; literal pool symbol address: _objc_msgSend
+# AUTOLINK-B-NEXT: br      x16
+# AUTOLINK-B-NEXT: brk     #0x1
+#
+# AUTOLINK-B-SYMS: _OBJC_CLASS_$_LateClass
+
+# MISSING: error: undefined symbol: _OBJC_CLASS_$_MissingClass
+# MISSING-NEXT: >>> referenced by objc class stub
+
+# MALFORMED: error: malformed objc class stub symbol _objc_msgSendClass$malformed; expected _objc_msgSendClass$<selector>$_OBJC_CLASS_$_<class>
+# MALFORMED-NOT: Objc selector ref:
+# MALFORMED-NOT: undefined symbol: _objc_msgSendClass$malformed
+
+#--- main.s
+# Main fast/small case: one regular selector stub plus local and dylib class
+# stubs.
+.section __TEXT,__objc_methname,cstring_literals
+Linstance:
+  .asciz "instance"
+Llocal:
+  .asciz "local"
+Lexternal:
+  .asciz "external"
+
+.section __DATA,__objc_selrefs,literal_pointers,no_dead_strip
+.p2align 3
+  .quad Linstance
+  .quad Llocal
+  .quad Lexternal
+
+.text
+.globl _objc_msgSend
+_objc_msgSend:
+  ret
+
+.globl _main
+_main:
+  bl _objc_msgSend$instance
+  bl _objc_msgSendClass$local$_OBJC_CLASS_$_LocalClass
+  bl _objc_msgSendClass$external$_OBJC_CLASS_$_ExternalClass
+  ret
+
+.data
+.globl _OBJC_CLASS_$_LocalClass
+_OBJC_CLASS_$_LocalClass:
+  .quad 0
+
+.subsections_via_symbols
+
+#--- external.s
+# Provides the dylib-defined class used by main.s.
+.data
+.globl _OBJC_CLASS_$_ExternalClass
+_OBJC_CLASS_$_ExternalClass:
+  .quad 0
+
+#--- dynamic.s
+# Uses a class stub whose class symbol is resolved dynamically.
+.section __TEXT,__objc_methname,cstring_literals
+Ldynamic:
+  .asciz "dynamic"
+
+.section __DATA,__objc_selrefs,literal_pointers,no_dead_strip
+.p2align 3
+  .quad Ldynamic
+
+.text
+.globl _main
+_main:
+  bl _objc_msgSendClass$dynamic$_OBJC_CLASS_$_DynamicClass
+  ret
+
+#--- archive-main.s
+# References a class stub whose class definition must be pulled from an archive.
+.text
+.globl _main
+_main:
+  bl _objc_msgSendClass$archive$_OBJC_CLASS_$_ArchiveClass
+  ret
+
+#--- archive-class.s
+# Archive member defining the class used by archive-main.s. Its class stub
+# extracts another member.
+.text
+.globl _archive_impl
+_archive_impl:
+  bl _objc_msgSendClass$nested$_OBJC_CLASS_$_NestedClass
+  ret
+
+.data
+.globl _OBJC_CLASS_$_ArchiveClass
+_OBJC_CLASS_$_ArchiveClass:
+  .quad 0
+
+#--- archive-nested.s
+# Archive member defining the class used by archive-class.s.
+.data
+.globl _OBJC_CLASS_$_NestedClass
+_OBJC_CLASS_$_NestedClass:
+  .quad 0
+
+#--- autolink-main-a.s
+# Autolinks an archive member that contains both the stub user and class.
+.linker_option "-lautolinka"
+.text
+.globl _main
+_main:
+  bl _dep_a
+  ret
+
+#--- autolink-dep-a.s
+# Autolinked member containing the class-stub reference and class definition.
+.text
+.globl _dep_a
+_dep_a:
+  bl _objc_msgSendClass$auto$_OBJC_CLASS_$_AutoClass
+  ret
+
+.data
+.globl _OBJC_CLASS_$_AutoClass
+_OBJC_CLASS_$_AutoClass:
+  .quad 0
+
+#--- autolink-main-b.s
+# Autolinks an archive member that references a class defined in this object.
+.linker_option "-lautolinkb"
+.text
+.globl _main
+_main:
+  bl _dep_b
+  ret
+
+.data
+.globl _OBJC_CLASS_$_LateClass
+_OBJC_CLASS_$_LateClass:
+  .quad 0
+
+#--- autolink-dep-b.s
+# Autolinked member that introduces a late class-stub reference.
+.text
+.globl _dep_b
+_dep_b:
+  bl _objc_msgSendClass$late$_OBJC_CLASS_$_LateClass
+  ret
+
+#--- missing.s
+# References a live class stub whose class symbol is unresolved.
+.section __TEXT,__objc_methname,cstring_literals
+Lmissing:
+  .asciz "missing"
+
+.section __DATA,__objc_selrefs,literal_pointers,no_dead_strip
+.p2align 3
+  .quad Lmissing
+
+.text
+.globl _objc_msgSend
+_objc_msgSend:
+  ret
+
+.globl _main
+_main:
+  bl _objc_msgSendClass$missing$_OBJC_CLASS_$_MissingClass
+  ret
+
+#--- malformed.s
+# References a class stub symbol without the required class-symbol suffix.
+.text
+.globl _main
+_main:
+  bl _objc_msgSendClass$malformed
+  ret
diff --git a/lld/test/MachO/objc-class-stubs-unsupported.s b/lld/test/MachO/objc-class-stubs-unsupported.s
new file mode 100644
index 00000000000000..007a4b8260ebea
--- /dev/null
+++ b/lld/test/MachO/objc-class-stubs-unsupported.s
@@ -0,0 +1,46 @@
+# REQUIRES: aarch64, x86
+
+# Tests that ObjC class stubs are rejected on architectures without an encoding.
+
+# RUN: rm -rf %t && split-file %s %t
+
+# Check x86_64 rejection.
+# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %t/x86_64.s \
+# RUN:   -o %t/x86_64.o
+# RUN: not %lld -arch x86_64 -lSystem -o /dev/null %t/x86_64.o \
+# RUN:   -objc_stubs_fast 2>&1 | FileCheck %s --check-prefix=X86_64
+
+# Check arm64_32 rejection.
+# RUN: llvm-mc -filetype=obj -triple=arm64_32-apple-watchos %t/arm64_32.s \
+# RUN:   -o %t/arm64_32.o
+# RUN: not %lld-watchos -arch arm64_32 -lSystem -o /dev/null %t/arm64_32.o \
+# RUN:   -objc_stubs_fast 2>&1 | FileCheck %s --check-prefix=ARM64_32
+
+# X86_64: error: objc class stubs are not supported for x86_64
+# ARM64_32: error: objc class stubs are not supported for arm64_32
+
+#--- x86_64.s
+# x86_64 input should reject class-stub symbols.
+.text
+.globl _main
+_main:
+  callq _objc_msgSendClass$foo$_OBJC_CLASS_$_Foo
+  ret
+
+.data
+.globl _OBJC_CLASS_$_Foo
+_OBJC_CLASS_$_Foo:
+  .quad 0
+
+#--- arm64_32.s
+# arm64_32 input should reject class-stub symbols.
+.text
+.globl _main
+_main:
+  bl _objc_msgSendClass$foo$_OBJC_CLASS_$_Foo
+  ret
+
+.data
+.globl _OBJC_CLASS_$_Foo
+_OBJC_CLASS_$_Foo:
+  .long 0



More information about the llvm-branch-commits mailing list