[lld] [LLD][COFF] Add support for ARM64X same-address thunks (PR #151255)

Jacek Caban via llvm-commits llvm-commits at lists.llvm.org
Wed Jul 30 08:57:02 PDT 2025


https://github.com/cjacek updated https://github.com/llvm/llvm-project/pull/151255

>From e26cc1e9ee7374d9dbc1d6d8a7f1974ee3233376 Mon Sep 17 00:00:00 2001
From: Jacek Caban <jacek at codeweavers.com>
Date: Tue, 29 Jul 2025 23:37:25 +0200
Subject: [PATCH 1/4] [LLD][COFF] Introduce Symbol::getDefined helper. (NFC)

---
 lld/COFF/Driver.cpp  | 12 ++----------
 lld/COFF/Symbols.cpp |  8 ++++++++
 lld/COFF/Symbols.h   |  4 ++++
 3 files changed, 14 insertions(+), 10 deletions(-)

diff --git a/lld/COFF/Driver.cpp b/lld/COFF/Driver.cpp
index 570b8f9d05906..192a998229e92 100644
--- a/lld/COFF/Driver.cpp
+++ b/lld/COFF/Driver.cpp
@@ -1318,13 +1318,9 @@ void LinkerDriver::convertResources() {
 }
 
 void LinkerDriver::maybeCreateECExportThunk(StringRef name, Symbol *&sym) {
-  Defined *def;
   if (!sym)
     return;
-  if (auto undef = dyn_cast<Undefined>(sym))
-    def = undef->getDefinedWeakAlias();
-  else
-    def = dyn_cast<Defined>(sym);
+  Defined *def = sym->getDefined();
   if (!def)
     return;
 
@@ -1356,11 +1352,7 @@ void LinkerDriver::createECExportThunks() {
     Symbol *sym = ctx.symtab.find(targetName);
     if (!sym)
       continue;
-    Defined *targetSym;
-    if (auto undef = dyn_cast<Undefined>(sym))
-      targetSym = undef->getDefinedWeakAlias();
-    else
-      targetSym = dyn_cast<Defined>(sym);
+    Defined *targetSym = sym->getDefined();
     if (!targetSym)
       continue;
 
diff --git a/lld/COFF/Symbols.cpp b/lld/COFF/Symbols.cpp
index b571ce9ce432c..ba4f95d14bc64 100644
--- a/lld/COFF/Symbols.cpp
+++ b/lld/COFF/Symbols.cpp
@@ -91,6 +91,14 @@ bool Symbol::isLive() const {
   return true;
 }
 
+Defined *Symbol::getDefined() {
+  if (auto d = dyn_cast<Defined>(this))
+    return d;
+  if (auto u = dyn_cast<Undefined>(this))
+    return u->getDefinedWeakAlias();
+  return nullptr;
+}
+
 void Symbol::replaceKeepingName(Symbol *other, size_t size) {
   StringRef origName = getName();
   memcpy(this, other, size);
diff --git a/lld/COFF/Symbols.h b/lld/COFF/Symbols.h
index fd3d8cec8c113..c86ded860876b 100644
--- a/lld/COFF/Symbols.h
+++ b/lld/COFF/Symbols.h
@@ -95,6 +95,10 @@ class Symbol {
            symbolKind == LazyDLLSymbolKind;
   }
 
+  // Get the Defined symbol associated with this symbol, either itself or its
+  // weak alias.
+  Defined *getDefined();
+
 private:
   void computeName();
 

>From 1f423623942b101a0816926e0c40f32e909a3d58 Mon Sep 17 00:00:00 2001
From: Jacek Caban <jacek at codeweavers.com>
Date: Tue, 29 Jul 2025 23:42:28 +0200
Subject: [PATCH 2/4] [LLD][COFF] Move entry thunk offset writing to
 writeSections (NFC)

To make it easier to add entry thunks to other chunk types.
---
 lld/COFF/Chunks.cpp |  6 ------
 lld/COFF/Writer.cpp | 10 +++++++++-
 2 files changed, 9 insertions(+), 7 deletions(-)

diff --git a/lld/COFF/Chunks.cpp b/lld/COFF/Chunks.cpp
index 01752cdc6a9da..4ba5d80b581ef 100644
--- a/lld/COFF/Chunks.cpp
+++ b/lld/COFF/Chunks.cpp
@@ -422,12 +422,6 @@ void SectionChunk::writeTo(uint8_t *buf) const {
 
     applyRelocation(buf + rel.VirtualAddress, rel);
   }
-
-  // Write the offset to EC entry thunk preceding section contents. The low bit
-  // is always set, so it's effectively an offset from the last byte of the
-  // offset.
-  if (Defined *entryThunk = getEntryThunk())
-    write32le(buf - sizeof(uint32_t), entryThunk->getRVA() - rva + 1);
 }
 
 void SectionChunk::applyRelocation(uint8_t *off,
diff --git a/lld/COFF/Writer.cpp b/lld/COFF/Writer.cpp
index 076561807af47..62019f19292a1 100644
--- a/lld/COFF/Writer.cpp
+++ b/lld/COFF/Writer.cpp
@@ -2544,7 +2544,15 @@ void Writer::writeSections() {
     }
 
     parallelForEach(sec->chunks, [&](Chunk *c) {
-      c->writeTo(secBuf + c->getRVA() - sec->getRVA());
+      uint8_t *buf = secBuf + c->getRVA() - sec->getRVA();
+      c->writeTo(buf);
+
+      // Write the offset to EC entry thunk preceding section contents. The low
+      // bit is always set, so it's effectively an offset from the last byte of
+      // the offset.
+      if (Defined *entryThunk = c->getEntryThunk())
+        write32le(buf - sizeof(uint32_t),
+                  entryThunk->getRVA() - c->getRVA() + 1);
     });
   }
 }

>From c34bad05b47c25087dfef2ad1eec74a52138ae36 Mon Sep 17 00:00:00 2001
From: Jacek Caban <jacek at codeweavers.com>
Date: Tue, 29 Jul 2025 21:59:24 +0200
Subject: [PATCH 3/4] [LLD][COFF] Add support for ARM64X same-address thunks

Fixes MSVC CRT thread-local constructors support on hybrid ARM64X targets.

-arm64xsameaddress is an undocumented option that ensures the specified
function has the same address in both native and EC views of hybrid images.
To achieve this, the linker emits additional thunks and replaces the symbols
of those functions with the thunk symbol (the same thunk is used in both
views). The thunk code jumps to the native function (similar to range
extension thunks), but additional ARM64X relocations are emitted to replace
the target with the EC function in the EC view.

MSVC appears to generate thunks even for non-hybrid ARM64EC images. As a side
effect, the native symbol is pulled in. Since this is used in the CRT for
thread-local constructors, it results in the image containing unnecessary
native code. Because these thunks do not appear to be useful in that context,
we limit this behavior to actual hybrid targets. This may change if
compatibility requires it.

The tricky part is that thunks should be skipped if the symbol is not live in
either view, and symbol replacement must be reflected in weak aliases. This
requires thunk generation to happen before resolving weak aliases but after the
GC pass. To enable this, the markLive call was moved earlier, and the final
weak alias resolution was postponed until afterward. This requires more code to
be aware of weak aliases, which previously could assume they were already
resolved.
---
 lld/COFF/Chunks.cpp                   |  15 +++-
 lld/COFF/Chunks.h                     |  32 +++++++-
 lld/COFF/Config.h                     |   3 +
 lld/COFF/Driver.cpp                   |  76 ++++++++++--------
 lld/COFF/Driver.h                     |   2 +
 lld/COFF/DriverUtils.cpp              |  16 ++++
 lld/COFF/MarkLive.cpp                 |   5 +-
 lld/COFF/Options.td                   |   4 +-
 lld/COFF/SymbolTable.cpp              |  42 ++++++++--
 lld/COFF/SymbolTable.h                |   6 +-
 lld/COFF/Writer.cpp                   |  20 ++++-
 lld/test/COFF/arm64x-sameaddress.test | 107 ++++++++++++++++++++++++--
 12 files changed, 278 insertions(+), 50 deletions(-)

diff --git a/lld/COFF/Chunks.cpp b/lld/COFF/Chunks.cpp
index 4ba5d80b581ef..b9231b5d9bec5 100644
--- a/lld/COFF/Chunks.cpp
+++ b/lld/COFF/Chunks.cpp
@@ -875,6 +875,18 @@ void RangeExtensionThunkARM64::writeTo(uint8_t *buf) const {
   applyArm64Imm(buf + 4, target->getRVA() & 0xfff, 0);
 }
 
+void SameAddressThunkARM64EC::setDynamicRelocs(COFFLinkerContext &ctx) const {
+  // Add ARM64X relocations replacing adrp/add instructions with a version using
+  // the hybrid target.
+  RangeExtensionThunkARM64 hybridView(ARM64EC, hybridTarget);
+  uint8_t buf[sizeof(arm64Thunk)];
+  hybridView.setRVA(rva);
+  hybridView.writeTo(buf);
+  ctx.dynamicRelocs->set(this, *reinterpret_cast<uint32_t *>(buf));
+  ctx.dynamicRelocs->set(Arm64XRelocVal(this, sizeof(uint32_t)),
+                         *reinterpret_cast<uint32_t *>(buf + sizeof(uint32_t)));
+}
+
 LocalImportChunk::LocalImportChunk(COFFLinkerContext &c, Defined *s)
     : sym(s), ctx(c) {
   setAlignment(ctx.config.wordsize);
@@ -1258,7 +1270,8 @@ void DynamicRelocsChunk::finalize() {
 }
 
 // Set the reloc value. The reloc entry must be allocated beforehand.
-void DynamicRelocsChunk::set(uint32_t rva, Arm64XRelocVal value) {
+void DynamicRelocsChunk::set(Arm64XRelocVal offset, Arm64XRelocVal value) {
+  uint32_t rva = offset.get();
   auto entry =
       llvm::find_if(arm64xRelocs, [rva](const Arm64XDynamicRelocEntry &e) {
         return e.offset.get() == rva;
diff --git a/lld/COFF/Chunks.h b/lld/COFF/Chunks.h
index d03a64cc6b812..6d88f5ec73776 100644
--- a/lld/COFF/Chunks.h
+++ b/lld/COFF/Chunks.h
@@ -193,6 +193,8 @@ class NonSectionChunk : public Chunk {
   // allowed ranges. Return the additional space required for the extension.
   virtual uint32_t extendRanges() { return 0; };
 
+  virtual Defined *getEntryThunk() const { return nullptr; };
+
   static bool classof(const Chunk *c) { return c->kind() >= OtherKind; }
 
 protected:
@@ -633,7 +635,7 @@ class ImportThunkChunkARM64EC : public ImportThunkChunk {
   bool verifyRanges() override;
   uint32_t extendRanges() override;
 
-  Defined *exitThunk;
+  Defined *exitThunk = nullptr;
   Defined *sym = nullptr;
   bool extended = false;
 
@@ -675,6 +677,26 @@ class RangeExtensionThunkARM64 : public NonSectionCodeChunk {
   MachineTypes machine;
 };
 
+// A chunk used to guarantee the same address for a function in both views of
+// a hybrid image. Similar to RangeExtensionThunkARM64 chunks, it calls the
+// target symbol using a BR instruction. It also contains an entry thunk for EC
+// compatibility and additional ARM64X relocations that swap targets between
+// views.
+class SameAddressThunkARM64EC : public RangeExtensionThunkARM64 {
+public:
+  explicit SameAddressThunkARM64EC(Defined *t, Defined *hybridTarget,
+                                   Defined *entryThunk)
+      : RangeExtensionThunkARM64(ARM64EC, t), hybridTarget(hybridTarget),
+        entryThunk(entryThunk) {}
+
+  Defined *getEntryThunk() const override { return entryThunk; }
+  void setDynamicRelocs(COFFLinkerContext &ctx) const;
+
+private:
+  Defined *hybridTarget;
+  Defined *entryThunk;
+};
+
 // Windows-specific.
 // See comments for DefinedLocalImport class.
 class LocalImportChunk : public NonSectionChunk {
@@ -843,13 +865,13 @@ class Arm64XRelocVal {
 public:
   Arm64XRelocVal(uint64_t value = 0) : value(value) {}
   Arm64XRelocVal(Defined *sym, int32_t offset = 0) : sym(sym), value(offset) {}
-  Arm64XRelocVal(Chunk *chunk, int32_t offset = 0)
+  Arm64XRelocVal(const Chunk *chunk, int32_t offset = 0)
       : chunk(chunk), value(offset) {}
   uint64_t get() const;
 
 private:
   Defined *sym = nullptr;
-  Chunk *chunk = nullptr;
+  const Chunk *chunk = nullptr;
   uint64_t value;
 };
 
@@ -884,7 +906,7 @@ class DynamicRelocsChunk : public NonSectionChunk {
     arm64xRelocs.emplace_back(type, size, offset, value);
   }
 
-  void set(uint32_t rva, Arm64XRelocVal value);
+  void set(Arm64XRelocVal offset, Arm64XRelocVal value);
 
 private:
   std::vector<Arm64XDynamicRelocEntry> arm64xRelocs;
@@ -940,6 +962,8 @@ inline bool Chunk::isHotPatchable() const {
 inline Defined *Chunk::getEntryThunk() const {
   if (auto *c = dyn_cast<const SectionChunkEC>(this))
     return c->entryThunk;
+  if (auto *c = dyn_cast<const NonSectionChunk>(this))
+    return c->getEntryThunk();
   return nullptr;
 }
 
diff --git a/lld/COFF/Config.h b/lld/COFF/Config.h
index 91b6e632fa7ed..a03bb57641670 100644
--- a/lld/COFF/Config.h
+++ b/lld/COFF/Config.h
@@ -223,6 +223,9 @@ struct Configuration {
   StringRef manifestUIAccess = "'false'";
   StringRef manifestFile;
 
+  // used for /arm64xsameaddress
+  std::vector<std::pair<Symbol *, Symbol *>> sameAddresses;
+
   // used for /dwodir
   StringRef dwoDir;
 
diff --git a/lld/COFF/Driver.cpp b/lld/COFF/Driver.cpp
index 192a998229e92..7580b469e4f87 100644
--- a/lld/COFF/Driver.cpp
+++ b/lld/COFF/Driver.cpp
@@ -500,7 +500,9 @@ void LinkerDriver::parseDirectives(InputFile *file) {
       file->symtab.parseAlternateName(arg->getValue());
       break;
     case OPT_arm64xsameaddress:
-      if (!file->symtab.isEC())
+      if (file->symtab.isEC())
+        parseSameAddress(arg->getValue());
+      else
         Warn(ctx) << arg->getSpelling()
                   << " is not allowed in non-ARM64EC files (" << toString(file)
                   << ")";
@@ -2295,6 +2297,13 @@ void LinkerDriver::linkerMain(ArrayRef<const char *> argsArr) {
        args.filtered(OPT_dependentloadflag, OPT_dependentloadflag_opt))
     parseDependentLoadFlags(arg);
 
+  for (auto *arg : args.filtered(OPT_arm64xsameaddress)) {
+    if (ctx.hybridSymtab)
+      parseSameAddress(arg->getValue());
+    else
+      Warn(ctx) << arg->getSpelling() << " is allowed only on EC targets";
+  }
+
   if (tar) {
     llvm::TimeTraceScope timeScope("Reproducer: response file");
     tar->append(
@@ -2668,12 +2677,46 @@ void LinkerDriver::linkerMain(ArrayRef<const char *> argsArr) {
     createECExportThunks();
 
   // Resolve remaining undefined symbols and warn about imported locals.
+  std::vector<Undefined *> aliases;
   ctx.forEachSymtab(
-      [&](SymbolTable &symtab) { symtab.resolveRemainingUndefines(); });
+      [&](SymbolTable &symtab) { symtab.resolveRemainingUndefines(aliases); });
 
   if (errorCount())
     return;
 
+  ctx.forEachActiveSymtab([](SymbolTable &symtab) {
+    symtab.initializeECThunks();
+    symtab.initializeLoadConfig();
+  });
+
+  // Identify unreferenced COMDAT sections.
+  if (config->doGC) {
+    if (config->mingw) {
+      // markLive doesn't traverse .eh_frame, but the personality function is
+      // only reached that way. The proper solution would be to parse and
+      // traverse the .eh_frame section, like the ELF linker does.
+      // For now, just manually try to retain the known possible personality
+      // functions. This doesn't bring in more object files, but only marks
+      // functions that already have been included to be retained.
+      ctx.forEachSymtab([&](SymbolTable &symtab) {
+        for (const char *n : {"__gxx_personality_v0", "__gcc_personality_v0",
+                              "rust_eh_personality"}) {
+          Defined *d = dyn_cast_or_null<Defined>(symtab.findUnderscore(n));
+          if (d && !d->isGCRoot) {
+            d->isGCRoot = true;
+            config->gcroot.push_back(d);
+          }
+        }
+      });
+    }
+
+    markLive(ctx);
+  }
+
+  ctx.symtab.initializeSameAddressThunks();
+  for (auto alias : aliases)
+    alias->resolveWeakAlias();
+
   if (config->mingw) {
     // Make sure the crtend.o object is the last object file. This object
     // file can contain terminating section chunks that need to be placed
@@ -2765,35 +2808,6 @@ void LinkerDriver::linkerMain(ArrayRef<const char *> argsArr) {
   if (auto *arg = args.getLastArg(OPT_print_symbol_order))
     config->printSymbolOrder = arg->getValue();
 
-  if (ctx.symtab.isEC())
-    ctx.symtab.initializeECThunks();
-  ctx.forEachActiveSymtab(
-      [](SymbolTable &symtab) { symtab.initializeLoadConfig(); });
-
-  // Identify unreferenced COMDAT sections.
-  if (config->doGC) {
-    if (config->mingw) {
-      // markLive doesn't traverse .eh_frame, but the personality function is
-      // only reached that way. The proper solution would be to parse and
-      // traverse the .eh_frame section, like the ELF linker does.
-      // For now, just manually try to retain the known possible personality
-      // functions. This doesn't bring in more object files, but only marks
-      // functions that already have been included to be retained.
-      ctx.forEachSymtab([&](SymbolTable &symtab) {
-        for (const char *n : {"__gxx_personality_v0", "__gcc_personality_v0",
-                              "rust_eh_personality"}) {
-          Defined *d = dyn_cast_or_null<Defined>(symtab.findUnderscore(n));
-          if (d && !d->isGCRoot) {
-            d->isGCRoot = true;
-            config->gcroot.push_back(d);
-          }
-        }
-      });
-    }
-
-    markLive(ctx);
-  }
-
   // Needs to happen after the last call to addFile().
   convertResources();
 
diff --git a/lld/COFF/Driver.h b/lld/COFF/Driver.h
index 5a9bd5c6d9682..b500ac8bba569 100644
--- a/lld/COFF/Driver.h
+++ b/lld/COFF/Driver.h
@@ -214,6 +214,8 @@ class LinkerDriver {
   void parsePDBPageSize(StringRef);
   void parseSection(StringRef);
 
+  void parseSameAddress(StringRef);
+
   // Parses a MS-DOS stub file
   void parseDosStub(StringRef path);
 
diff --git a/lld/COFF/DriverUtils.cpp b/lld/COFF/DriverUtils.cpp
index d8b41c7f45400..dc4039f116f25 100644
--- a/lld/COFF/DriverUtils.cpp
+++ b/lld/COFF/DriverUtils.cpp
@@ -328,6 +328,22 @@ void LinkerDriver::parseSwaprun(StringRef arg) {
   } while (!arg.empty());
 }
 
+void LinkerDriver::parseSameAddress(StringRef arg) {
+  auto mangledName = getArm64ECMangledFunctionName(arg);
+  Symbol *sym = ctx.symtab.addUndefined(mangledName ? *mangledName : arg);
+
+  // MSVC appears to generate thunks even for non-hybrid ARM64EC images.
+  // As a side effect, the native symbol is pulled in. Since this is used
+  // in the CRT for thread-local constructors, it results in the image
+  // containing unnecessary native code. As these thunks don't appear to
+  // be useful, we limit this behavior to actual hybrid targets. This may
+  // change if compatibility becomes necessary.
+  if (ctx.config.machine != ARM64X)
+    return;
+  Symbol *nativeSym = ctx.hybridSymtab->addUndefined(arg);
+  ctx.config.sameAddresses.emplace_back(sym, nativeSym);
+}
+
 // An RAII temporary file class that automatically removes a temporary file.
 namespace {
 class TemporaryFile {
diff --git a/lld/COFF/MarkLive.cpp b/lld/COFF/MarkLive.cpp
index f40810c6805aa..78f5030e8fc2b 100644
--- a/lld/COFF/MarkLive.cpp
+++ b/lld/COFF/MarkLive.cpp
@@ -49,7 +49,10 @@ void markLive(COFFLinkerContext &ctx) {
       addSym(file->impchkThunk->exitThunk);
   };
 
-  addSym = [&](Symbol *b) {
+  addSym = [&](Symbol *s) {
+    Defined *b = s->getDefined();
+    if (!b)
+      return;
     if (auto *sym = dyn_cast<DefinedRegular>(b)) {
       enqueue(sym->getChunk());
     } else if (auto *sym = dyn_cast<DefinedImportData>(b)) {
diff --git a/lld/COFF/Options.td b/lld/COFF/Options.td
index 0d66b49a4fdb8..2c393cc94b5e3 100644
--- a/lld/COFF/Options.td
+++ b/lld/COFF/Options.td
@@ -31,6 +31,9 @@ multiclass B_priv<string name> {
 def align   : P<"align", "Section alignment">;
 def aligncomm : P<"aligncomm", "Set common symbol alignment">;
 def alternatename : P<"alternatename", "Define weak alias">;
+def arm64xsameaddress
+    : P<"arm64xsameaddress", "Generate a thunk for the symbol with the same "
+                             "address in both native and EC views on ARM64X.">;
 def base    : P<"base", "Base address of the program">;
 def color_diagnostics: Flag<["--"], "color-diagnostics">,
     HelpText<"Alias for --color-diagnostics=always">;
@@ -373,4 +376,3 @@ def tlbid : P_priv<"tlbid">;
 def tlbout : P_priv<"tlbout">;
 def verbose_all : P_priv<"verbose">;
 def guardsym : P_priv<"guardsym">;
-def arm64xsameaddress : P_priv<"arm64xsameaddress">;
diff --git a/lld/COFF/SymbolTable.cpp b/lld/COFF/SymbolTable.cpp
index 189e75dfc3ff5..de04cdff6483d 100644
--- a/lld/COFF/SymbolTable.cpp
+++ b/lld/COFF/SymbolTable.cpp
@@ -452,7 +452,7 @@ void SymbolTable::reportUnresolvable() {
   reportProblemSymbols(undefs, /*localImports=*/nullptr, true);
 }
 
-void SymbolTable::resolveRemainingUndefines() {
+void SymbolTable::resolveRemainingUndefines(std::vector<Undefined *> &aliases) {
   llvm::TimeTraceScope timeScope("Resolve remaining undefined symbols");
   SmallPtrSet<Symbol *, 8> undefs;
   DenseMap<Symbol *, Symbol *> localImports;
@@ -468,8 +468,10 @@ void SymbolTable::resolveRemainingUndefines() {
     StringRef name = undef->getName();
 
     // A weak alias may have been resolved, so check for that.
-    if (undef->resolveWeakAlias())
+    if (undef->getWeakAlias()) {
+      aliases.push_back(undef);
       continue;
+    }
 
     // If we can resolve a symbol by removing __imp_ prefix, do that.
     // This odd rule is for compatibility with MSVC linker.
@@ -620,10 +622,10 @@ void SymbolTable::initializeECThunks() {
     return;
 
   for (auto it : entryThunks) {
-    auto *to = dyn_cast<Defined>(it.second);
+    Defined *to = it.second->getDefined();
     if (!to)
       continue;
-    auto *from = dyn_cast<DefinedRegular>(it.first);
+    auto *from = dyn_cast_or_null<DefinedRegular>(it.first->getDefined());
     // We need to be able to add padding to the function and fill it with an
     // offset to its entry thunks. To ensure that padding the function is
     // feasible, functions are required to be COMDAT symbols with no offset.
@@ -642,7 +644,8 @@ void SymbolTable::initializeECThunks() {
     Symbol *sym = exitThunks.lookup(file->thunkSym);
     if (!sym)
       sym = exitThunks.lookup(file->impECSym);
-    file->impchkThunk->exitThunk = dyn_cast_or_null<Defined>(sym);
+    if (sym)
+      file->impchkThunk->exitThunk = sym->getDefined();
   }
 
   // On ARM64EC, the __imp_ symbol references the auxiliary IAT, while the
@@ -659,6 +662,35 @@ void SymbolTable::initializeECThunks() {
   });
 }
 
+void SymbolTable::initializeSameAddressThunks() {
+  for (auto iter : ctx.config.sameAddresses) {
+    auto sym = dyn_cast_or_null<DefinedRegular>(iter.first->getDefined());
+    if (!sym || !sym->isLive())
+      continue;
+    auto nativeSym =
+        dyn_cast_or_null<DefinedRegular>(iter.second->getDefined());
+    if (!nativeSym || !nativeSym->isLive())
+      continue;
+    Defined *entryThunk = sym->getChunk()->getEntryThunk();
+    if (!entryThunk)
+      continue;
+
+    // Replace symbols with symbols referencing the thunk. Store the original
+    // symbol as equivalent DefinedSynthetic instances for use in the thunk
+    // itself.
+    auto symClone = make<DefinedSynthetic>(sym->getName(), sym->getChunk(),
+                                           sym->getValue());
+    auto nativeSymClone = make<DefinedSynthetic>(
+        nativeSym->getName(), nativeSym->getChunk(), nativeSym->getValue());
+    SameAddressThunkARM64EC *thunk =
+        make<SameAddressThunkARM64EC>(nativeSymClone, symClone, entryThunk);
+    sameAddressThunks.push_back(thunk);
+
+    replaceSymbol<DefinedSynthetic>(sym, sym->getName(), thunk);
+    replaceSymbol<DefinedSynthetic>(nativeSym, nativeSym->getName(), thunk);
+  }
+}
+
 Symbol *SymbolTable::addUndefined(StringRef name, InputFile *f,
                                   bool overrideLazy) {
   auto [s, wasInserted] = insert(name, f);
diff --git a/lld/COFF/SymbolTable.h b/lld/COFF/SymbolTable.h
index 7eb067640dc85..aadd366c7d39f 100644
--- a/lld/COFF/SymbolTable.h
+++ b/lld/COFF/SymbolTable.h
@@ -31,6 +31,7 @@ class DefinedAbsolute;
 class DefinedRegular;
 class ImportThunkChunk;
 class LazyArchive;
+class SameAddressThunkARM64EC;
 class SectionChunk;
 class Symbol;
 
@@ -67,7 +68,7 @@ class SymbolTable {
   // Try to resolve any undefined symbols and update the symbol table
   // accordingly, then print an error message for any remaining undefined
   // symbols and warn about imported local symbols.
-  void resolveRemainingUndefines();
+  void resolveRemainingUndefines(std::vector<Undefined *> &aliases);
 
   // Try to resolve undefined symbols with alternate names.
   void resolveAlternateNames();
@@ -140,6 +141,7 @@ class SymbolTable {
   void addEntryThunk(Symbol *from, Symbol *to);
   void addExitThunk(Symbol *from, Symbol *to);
   void initializeECThunks();
+  void initializeSameAddressThunks();
 
   void reportDuplicate(Symbol *existing, InputFile *newFile,
                        SectionChunk *newSc = nullptr,
@@ -159,6 +161,8 @@ class SymbolTable {
   // A list of EC EXP+ symbols.
   std::vector<Symbol *> expSymbols;
 
+  std::vector<SameAddressThunkARM64EC *> sameAddressThunks;
+
   // A list of DLL exports.
   std::vector<Export> exports;
   llvm::DenseSet<StringRef> directivesExports;
diff --git a/lld/COFF/Writer.cpp b/lld/COFF/Writer.cpp
index 62019f19292a1..4c7a52eda151b 100644
--- a/lld/COFF/Writer.cpp
+++ b/lld/COFF/Writer.cpp
@@ -314,6 +314,7 @@ class Writer {
   uint32_t dataDirOffset64;
 
   OutputSection *textSec;
+  OutputSection *wowthkSec;
   OutputSection *hexpthkSec;
   OutputSection *bssSec;
   OutputSection *rdataSec;
@@ -1076,8 +1077,10 @@ void Writer::createSections() {
 
   // Try to match the section order used by link.exe.
   textSec = createSection(".text", code | r | x);
-  if (isArm64EC(ctx.config.machine))
+  if (isArm64EC(ctx.config.machine)) {
+    wowthkSec = createSection(".wowthk", code | r | x);
     hexpthkSec = createSection(".hexpthk", code | r | x);
+  }
   bssSec = createSection(".bss", bss | r | w);
   rdataSec = createSection(".rdata", data | r);
   buildidSec = createSection(".buildid", data | r);
@@ -2310,6 +2313,18 @@ void Writer::createECChunks() {
       ctx.symtab.findUnderscore("__arm64x_redirection_metadata");
   replaceSymbol<DefinedSynthetic>(entryPointsSym, entryPointsSym->getName(),
                                   entryPoints);
+
+  for (auto thunk : ctx.symtab.sameAddressThunks) {
+    wowthkSec->insertChunkAtStart(thunk);
+    if (ctx.config.machine == ARM64X) {
+      // Relocation values are set later in setECSymbols.
+      ctx.dynamicRelocs->add(IMAGE_DVRT_ARM64X_FIXUP_TYPE_VALUE,
+                             sizeof(uint32_t), thunk);
+      ctx.dynamicRelocs->add(IMAGE_DVRT_ARM64X_FIXUP_TYPE_VALUE,
+                             sizeof(uint32_t),
+                             Arm64XRelocVal(thunk, sizeof(uint32_t)));
+    }
+  }
 }
 
 // MinGW specific. Gather all relocations that are imported from a DLL even
@@ -2519,6 +2534,9 @@ void Writer::setECSymbols() {
           chpeSym->getRVA() + offsetof(chpe_metadata, ExtraRFETableSize),
           pdata.last->getRVA() + pdata.last->getSize() - pdata.first->getRVA());
   }
+
+  for (SameAddressThunkARM64EC *thunk : ctx.symtab.sameAddressThunks)
+    thunk->setDynamicRelocs(ctx);
 }
 
 // Write section contents to a mmap'ed file.
diff --git a/lld/test/COFF/arm64x-sameaddress.test b/lld/test/COFF/arm64x-sameaddress.test
index c69be9d268c3b..819d19b10065f 100644
--- a/lld/test/COFF/arm64x-sameaddress.test
+++ b/lld/test/COFF/arm64x-sameaddress.test
@@ -3,16 +3,103 @@ RUN: split-file %s %t.dir && cd %t.dir
 
 RUN: llvm-mc -filetype=obj -triple=arm64ec-windows func-arm64ec.s -o func-arm64ec.obj
 RUN: llvm-mc -filetype=obj -triple=aarch64-windows func-arm64.s -o func-arm64.obj
+RUN: llvm-mc -filetype=obj -triple=arm64ec-windows ref-arm64ec.s -o ref-arm64ec.obj
+RUN: llvm-mc -filetype=obj -triple=aarch64-windows ref-arm64.s -o ref-arm64.obj
 RUN: llvm-mc -filetype=obj -triple=arm64ec-windows drectve.s -o drectve.obj
 RUN: llvm-mc -filetype=obj -triple=aarch64-windows drectve.s -o drectve-arm64.obj
 RUN: llvm-mc -filetype=obj -triple=arm64ec-windows %S/Inputs/loadconfig-arm64ec.s -o loadconfig-arm64ec.obj
 RUN: llvm-mc -filetype=obj -triple=aarch64-windows %S/Inputs/loadconfig-arm64.s -o loadconfig-arm64.obj
 
 RUN: lld-link -machine:arm64x -dll -noentry -out:out.dll loadconfig-arm64.obj loadconfig-arm64ec.obj \
-RUN:          func-arm64.obj func-arm64ec.obj drectve.obj
+RUN:          func-arm64.obj func-arm64ec.obj ref-arm64.obj ref-arm64ec.obj drectve.obj
+
+RUN: llvm-objdump -d out.dll | FileCheck --check-prefix=DISASM %s
+DISASM:      000000180001000 <.text>:
+DISASM-NEXT: 180001000: d2800020     mov     x0, #0x1                // =1
+DISASM-NEXT: 180001004: d65f03c0     ret
+DISASM-NEXT:                 ...
+DISASM-NEXT: 180002000: 00000019     udf     #0x19
+DISASM-NEXT: 180002004: d2800040     mov     x0, #0x2                // =2
+DISASM-NEXT: 180002008: d65f03c0     ret
+DISASM-NEXT: 18000200c: 0000000d     udf     #0xd
+DISASM-NEXT: 180002010: f0fffff0     adrp    x16, 0x180001000 <.text>
+DISASM-NEXT: 180002014: 91000210     add     x16, x16, #0x0
+DISASM-NEXT: 180002018: d61f0200     br      x16
+DISASM-NEXT: 18000201c: d2800060     mov     x0, #0x3                // =3
+DISASM-NEXT: 180002020: d65f03c0     ret
+
+RUN: llvm-readobj --hex-dump=.test out.dll | FileCheck --check-prefix=TESTSEC %s
+TESTSEC: 10200000 10200000 10200000
+
+RUN: llvm-readobj --coff-load-config out.dll | FileCheck --check-prefix=DYNRELOCS %s
+DYNRELOCS:      DynamicRelocations [
+DYNRELOCS-NEXT:   Version: 0x1
+DYNRELOCS-NEXT:   Arm64X [
+DYNRELOCS-NEXT:     Entry [
+DYNRELOCS-NEXT:       RVA: 0x7C
+DYNRELOCS-NEXT:       Type: VALUE
+DYNRELOCS-NEXT:       Size: 0x2
+DYNRELOCS-NEXT:       Value: 0x8664
+DYNRELOCS-NEXT:     ]
+DYNRELOCS-NEXT:     Entry [
+DYNRELOCS-NEXT:       RVA: 0x150
+DYNRELOCS-NEXT:       Type: VALUE
+DYNRELOCS-NEXT:       Size: 0x4
+DYNRELOCS-NEXT:       Value: 0x3150
+DYNRELOCS-NEXT:     ]
+DYNRELOCS-NEXT:     Entry [
+DYNRELOCS-NEXT:       RVA: 0x154
+DYNRELOCS-NEXT:       Type: VALUE
+DYNRELOCS-NEXT:       Size: 0x4
+DYNRELOCS-NEXT:       Value: 0x140
+DYNRELOCS-NEXT:     ]
+DYNRELOCS-NEXT:     Entry [
+DYNRELOCS-NEXT:       RVA: 0x2010
+DYNRELOCS-NEXT:       Type: VALUE
+DYNRELOCS-NEXT:       Size: 0x4
+DYNRELOCS-NEXT:       Value: 0x90000010
+DYNRELOCS-NEXT:     ]
+DYNRELOCS-NEXT:     Entry [
+DYNRELOCS-NEXT:       RVA: 0x2014
+DYNRELOCS-NEXT:       Type: VALUE
+DYNRELOCS-NEXT:       Size: 0x4
+DYNRELOCS-NEXT:       Value: 0x91001210
+DYNRELOCS-NEXT:     ]
+DYNRELOCS-NEXT:   ]
+DYNRELOCS-NEXT: ]
 
 RUN: lld-link -machine:arm64x -dll -noentry -out:out-cmd.dll loadconfig-arm64.obj loadconfig-arm64ec.obj \
-RUN:          func-arm64.obj func-arm64ec.obj -arm64xsameaddress:func
+RUN:          func-arm64.obj func-arm64ec.obj ref-arm64.obj ref-arm64ec.obj -arm64xsameaddress:func
+RUN: llvm-objdump -d out-cmd.dll | FileCheck --check-prefix=DISASM %s
+RUN: llvm-readobj --hex-dump=.test out-cmd.dll | FileCheck --check-prefix=TESTSEC %s
+RUN: llvm-readobj --coff-load-config out-cmd.dll | FileCheck --check-prefix=DYNRELOCS %s
+
+RUN: lld-link -machine:arm64x -dll -noentry -out:out-both.dll loadconfig-arm64.obj loadconfig-arm64ec.obj \
+RUN:          func-arm64.obj func-arm64ec.obj ref-arm64.obj ref-arm64ec.obj drectve.obj -arm64xsameaddress:func
+RUN: llvm-objdump -d out-both.dll | FileCheck --check-prefix=DISASM %s
+RUN: llvm-readobj --hex-dump=.test out-both.dll | FileCheck --check-prefix=TESTSEC %s
+RUN: llvm-readobj --coff-load-config out-both.dll | FileCheck --check-prefix=DYNRELOCS %s
+
+Check that if any of the sameaddress symbols is not alive, the thunk is not generated.
+
+RUN: lld-link -machine:arm64x -dll -noentry -out:out-live1.dll loadconfig-arm64.obj loadconfig-arm64ec.obj \
+RUN:          func-arm64.obj func-arm64ec.obj ref-arm64ec.obj drectve.obj
+RUN: llvm-objdump -d out-live1.dll | FileCheck --check-prefix=DISASM-LIVE1 %s
+DISASM-LIVE1:      0000000180001000 <.text>:
+DISASM-LIVE1-NEXT: 180001000: 00000009     udf     #0x9
+DISASM-LIVE1-NEXT: 180001004: d2800040     mov     x0, #0x2                // =2
+DISASM-LIVE1-NEXT: 180001008: d65f03c0     ret
+DISASM-LIVE1-NEXT: 18000100c: d2800060     mov     x0, #0x3                // =3
+DISASM-LIVE1-NEXT: 180001010: d65f03c0     ret
+DISASM-LIVE1-NOT:  br
+
+RUN: lld-link -machine:arm64x -dll -noentry -out:out-live2.dll loadconfig-arm64.obj loadconfig-arm64ec.obj \
+RUN:          func-arm64.obj func-arm64ec.obj ref-arm64.obj drectve.obj
+RUN: llvm-objdump -d out-live2.dll | FileCheck --check-prefix=DISASM-LIVE2 %s
+DISASM-LIVE2:      0000000180001000 <.text>:
+DISASM-LIVE2-NEXT: 180001000: d2800020     mov     x0, #0x1                // =1
+DISASM-LIVE2-NEXT: 180001004: d65f03c0     ret
+DISASM-LIVE2-NOT:  br
 
 RUN: lld-link -machine:arm64ec -dll -noentry -out:out-ec.dll loadconfig-arm64ec.obj func-arm64ec.obj drectve.obj
 
@@ -20,6 +107,10 @@ RUN: lld-link -machine:arm64x -dll -noentry -out:out-warn.dll loadconfig-arm64.o
 RUN:          func-arm64.obj func-arm64ec.obj drectve-arm64.obj 2>&1 | FileCheck --check-prefix=WARN %s
 WARN: lld-link: warning: -arm64xsameaddress: is not allowed in non-ARM64EC files (drectve-arm64.obj)
 
+RUN: lld-link -machine:arm64 -dll -noentry -out:out-warn2.dll loadconfig-arm64.obj \
+RUN:          func-arm64.obj -arm64xsameaddress:func 2>&1 | FileCheck --check-prefix=WARN2 %s
+WARN2: lld-link: warning: -arm64xsameaddress: is allowed only on EC targets
+
 #--- func-arm64.s
         .section .text,"xr",discard,func
         .globl func
@@ -27,6 +118,10 @@ func:
         mov x0, #1
         ret
 
+#--- ref-arm64.s
+        .section .test,"dr"
+        .rva func
+
 #--- func-arm64ec.s
         .section .text,"xr",discard,"#func"
         .globl "#func"
@@ -43,14 +138,16 @@ entry_thunk:
         mov x0, #3
         ret
 
-        .section .test,"dr"
-        .rva func
-
 	.section .hybmp$x,"yi"
 	.symidx "#func"
 	.symidx entry_thunk
 	.word 1
 
+#--- ref-arm64ec.s
+        .section .test,"dr"
+        .rva func
+        .rva "#func"
+
 #--- drectve.s
         .section .drectve, "yn"
         .ascii " -arm64xsameaddress:func"

>From 0a9034ba9e54a5fdf2212d4dba68c395a78bfaca Mon Sep 17 00:00:00 2001
From: Jacek Caban <jacek at codeweavers.com>
Date: Wed, 30 Jul 2025 17:56:10 +0200
Subject: [PATCH 4/4] Use ulittle32_t, move thunks insertion, remove redundant
 machine check

---
 lld/COFF/Chunks.cpp |  7 ++++---
 lld/COFF/Writer.cpp | 17 ++++++++---------
 2 files changed, 12 insertions(+), 12 deletions(-)

diff --git a/lld/COFF/Chunks.cpp b/lld/COFF/Chunks.cpp
index b9231b5d9bec5..39fc25047f3b1 100644
--- a/lld/COFF/Chunks.cpp
+++ b/lld/COFF/Chunks.cpp
@@ -882,9 +882,10 @@ void SameAddressThunkARM64EC::setDynamicRelocs(COFFLinkerContext &ctx) const {
   uint8_t buf[sizeof(arm64Thunk)];
   hybridView.setRVA(rva);
   hybridView.writeTo(buf);
-  ctx.dynamicRelocs->set(this, *reinterpret_cast<uint32_t *>(buf));
-  ctx.dynamicRelocs->set(Arm64XRelocVal(this, sizeof(uint32_t)),
-                         *reinterpret_cast<uint32_t *>(buf + sizeof(uint32_t)));
+  uint32_t addrp = *reinterpret_cast<ulittle32_t *>(buf);
+  uint32_t add = *reinterpret_cast<ulittle32_t *>(buf + sizeof(uint32_t));
+  ctx.dynamicRelocs->set(this, addrp);
+  ctx.dynamicRelocs->set(Arm64XRelocVal(this, sizeof(uint32_t)), add);
 }
 
 LocalImportChunk::LocalImportChunk(COFFLinkerContext &c, Defined *s)
diff --git a/lld/COFF/Writer.cpp b/lld/COFF/Writer.cpp
index 4c7a52eda151b..21ab9d17a26f9 100644
--- a/lld/COFF/Writer.cpp
+++ b/lld/COFF/Writer.cpp
@@ -1132,6 +1132,9 @@ void Writer::createSections() {
   if (hasIdata)
     locateImportTables();
 
+  for (auto thunk : ctx.symtab.sameAddressThunks)
+    wowthkSec->addChunk(thunk);
+
   // Then create an OutputSection for each section.
   // '$' and all following characters in input section names are
   // discarded when determining output section. So, .text$foo
@@ -2315,15 +2318,11 @@ void Writer::createECChunks() {
                                   entryPoints);
 
   for (auto thunk : ctx.symtab.sameAddressThunks) {
-    wowthkSec->insertChunkAtStart(thunk);
-    if (ctx.config.machine == ARM64X) {
-      // Relocation values are set later in setECSymbols.
-      ctx.dynamicRelocs->add(IMAGE_DVRT_ARM64X_FIXUP_TYPE_VALUE,
-                             sizeof(uint32_t), thunk);
-      ctx.dynamicRelocs->add(IMAGE_DVRT_ARM64X_FIXUP_TYPE_VALUE,
-                             sizeof(uint32_t),
-                             Arm64XRelocVal(thunk, sizeof(uint32_t)));
-    }
+    // Relocation values are set later in setECSymbols.
+    ctx.dynamicRelocs->add(IMAGE_DVRT_ARM64X_FIXUP_TYPE_VALUE, sizeof(uint32_t),
+                           thunk);
+    ctx.dynamicRelocs->add(IMAGE_DVRT_ARM64X_FIXUP_TYPE_VALUE, sizeof(uint32_t),
+                           Arm64XRelocVal(thunk, sizeof(uint32_t)));
   }
 }
 



More information about the llvm-commits mailing list