https://github.com/cjacek created 
https://github.com/llvm/llvm-project/pull/202740

This is a draft of an alternative solution for 
https://discourse.llvm.org/t/rfc-multi-architecture-coff-object-files-for-arm64x/90723
 and 
https://discourse.llvm.org/t/rfc-introduce-a-wholearchive-marker-embedded-in-archives-themselves/90721.
 Unlike the previous attempt, this approach does not alter the archive format. 
Instead, it introduces the `.llvm.arm64x` section, similar to how `.llvm.lto` 
is used for "fat LTO" object files. This allows aware tools to leverage the 
additional capability while ensuring unaware tools simply ignore the section.

On the Clang side, this is achieved by compiling separately for both the ARM64 
and ARM64EC targets and merging the results via an additional llvm-objcopy job. 
lld-link then selects the correct view of the file depending on the target 
architecture. Additionally, the archive writer splits the object file into 
separate members so that they are properly reflected in the symbol map. An 
advantage of such treatment in archives is that the resulting static library 
can be used seamlessly by the MSVC linker, as it no longer relies on the 
`.llvm.arm64x` section.

>From ebc5a77ce0fd36fa7a6ff6bc0bb3850fc9b66425 Mon Sep 17 00:00:00 2001
From: Jacek Caban <[email protected]>
Date: Fri, 22 May 2026 22:39:11 +0200
Subject: [PATCH 1/6] [Object][COFF] Introduce the .llvm.arm64x section

Introduce a new extension section allowing the embedding of ARM64EC
object files inside native ARM64 object files. Its content consists
of an entire, valid ARM64EC COFF object file.
---
 llvm/include/llvm/Object/COFF.h               |   3 +
 llvm/lib/Object/COFFObjectFile.cpp            |  22 ++++
 .../llvm-readobj/COFF/arm64x-hybridobj.yaml   | 102 ++++++++++++++++++
 llvm/tools/llvm-readobj/llvm-readobj.cpp      |  11 +-
 4 files changed, 135 insertions(+), 3 deletions(-)
 create mode 100644 llvm/test/tools/llvm-readobj/COFF/arm64x-hybridobj.yaml

diff --git a/llvm/include/llvm/Object/COFF.h b/llvm/include/llvm/Object/COFF.h
index ed2addbc69487..ac8c6abd7e027 100644
--- a/llvm/include/llvm/Object/COFF.h
+++ b/llvm/include/llvm/Object/COFF.h
@@ -877,6 +877,8 @@ struct coff_dynamic_relocation64_v2 {
   support::ulittle32_t Flags;
 };
 
+static constexpr StringLiteral kArm64XSectionName = ".llvm.arm64x";
+
 class LLVM_ABI COFFObjectFile : public ObjectFile {
 private:
   COFFObjectFile(MemoryBufferRef Object);
@@ -1100,6 +1102,7 @@ class LLVM_ABI COFFObjectFile : public ObjectFile {
     return SubtargetFeatures();
   }
   std::unique_ptr<MemoryBuffer> getHybridObjectView() const;
+  std::optional<MemoryBufferRef> getHybridObjectSection() const;
 
   import_directory_iterator import_directory_begin() const;
   import_directory_iterator import_directory_end() const;
diff --git a/llvm/lib/Object/COFFObjectFile.cpp 
b/llvm/lib/Object/COFFObjectFile.cpp
index 14a2343811a38..e321cd8918527 100644
--- a/llvm/lib/Object/COFFObjectFile.cpp
+++ b/llvm/lib/Object/COFFObjectFile.cpp
@@ -1560,6 +1560,28 @@ std::unique_ptr<MemoryBuffer> 
COFFObjectFile::getHybridObjectView() const {
   return HybridView;
 }
 
+std::optional<MemoryBufferRef> COFFObjectFile::getHybridObjectSection() const {
+  if (getDOSHeader() || getMachine() != COFF::IMAGE_FILE_MACHINE_ARM64)
+    return std::nullopt;
+
+  // Search the .llvm.arm64x section, which may be used to embed a full ARM64EC
+  // object file into an ARM64 object file.
+  for (const SectionRef &S : sections()) {
+    Expected<StringRef> Name = S.getName();
+    if (errorToBool(Name.takeError()))
+      return std::nullopt;
+
+    if (*Name == kArm64XSectionName) {
+      Expected<StringRef> ContentsOrErr = S.getContents();
+      if (errorToBool(ContentsOrErr.takeError()))
+        return std::nullopt;
+      return MemoryBufferRef(*ContentsOrErr, getFileName());
+    }
+  }
+
+  return std::nullopt;
+}
+
 bool ImportDirectoryEntryRef::
 operator==(const ImportDirectoryEntryRef &Other) const {
   return ImportTable == Other.ImportTable && Index == Other.Index;
diff --git a/llvm/test/tools/llvm-readobj/COFF/arm64x-hybridobj.yaml 
b/llvm/test/tools/llvm-readobj/COFF/arm64x-hybridobj.yaml
new file mode 100644
index 0000000000000..bd5d07e23f51f
--- /dev/null
+++ b/llvm/test/tools/llvm-readobj/COFF/arm64x-hybridobj.yaml
@@ -0,0 +1,102 @@
+# RUN: yaml2obj -DMACHINE=IMAGE_FILE_MACHINE_ARM64 %s -o %t-aarch64.o
+# RUN: yaml2obj -DMACHINE=IMAGE_FILE_MACHINE_ARM64EC %s -o %t-arm64ec.o
+# RUN: llvm-objcopy --add-section=.llvm.arm64x=%t-arm64ec.o 
--set-section-flags=.llvm.arm64x=debug %t-aarch64.o %t.o
+# RUN: llvm-readobj --sections %t.o | FileCheck %s
+
+# CHECK:      Format: COFF-ARM64
+# CHECK-NEXT: Arch: aarch64
+# CHECK-NEXT: AddressSize: 64bit
+# CHECK-NEXT: Sections [
+# CHECK-NEXT:   Section {
+# CHECK-NEXT:     Number: 1
+# CHECK-NEXT:     Name: .data (2E 64 61 74 61 00 00 00)
+# CHECK-NEXT:     VirtualSize: 0x0
+# CHECK-NEXT:     VirtualAddress: 0x0
+# CHECK-NEXT:     RawDataSize: 4
+# CHECK-NEXT:     PointerToRawData: 0x64
+# CHECK-NEXT:     PointerToRelocations: 0x0
+# CHECK-NEXT:     PointerToLineNumbers: 0x0
+# CHECK-NEXT:     RelocationCount: 0
+# CHECK-NEXT:     LineNumberCount: 0
+# CHECK-NEXT:     Characteristics [ (0xC0300040)
+# CHECK-NEXT:       IMAGE_SCN_ALIGN_4BYTES (0x300000)
+# CHECK-NEXT:       IMAGE_SCN_CNT_INITIALIZED_DATA (0x40)
+# CHECK-NEXT:       IMAGE_SCN_MEM_READ (0x40000000)
+# CHECK-NEXT:       IMAGE_SCN_MEM_WRITE (0x80000000)
+# CHECK-NEXT:     ]
+# CHECK-NEXT:   }
+# CHECK-NEXT:   Section {
+# CHECK-NEXT:     Number: 2
+# CHECK-NEXT:     Name: .llvm.arm64x (2F 34 00 00 00 00 00 00)
+# CHECK-NEXT:     VirtualSize: 0x7A
+# CHECK-NEXT:     VirtualAddress: 0x0
+# CHECK-NEXT:     RawDataSize: 122
+# CHECK-NEXT:     PointerToRawData: 0x68
+# CHECK-NEXT:     PointerToRelocations: 0x0
+# CHECK-NEXT:     PointerToLineNumbers: 0x0
+# CHECK-NEXT:     RelocationCount: 0
+# CHECK-NEXT:     LineNumberCount: 0
+# CHECK-NEXT:     Characteristics [ (0xC2000040)
+# CHECK-NEXT:       IMAGE_SCN_CNT_INITIALIZED_DATA (0x40)
+# CHECK-NEXT:       IMAGE_SCN_MEM_DISCARDABLE (0x2000000)
+# CHECK-NEXT:       IMAGE_SCN_MEM_READ (0x40000000)
+# CHECK-NEXT:       IMAGE_SCN_MEM_WRITE (0x80000000)
+# CHECK-NEXT:     ]
+# CHECK-NEXT:   }
+# CHECK-NEXT: ]
+# CHECK-NEXT: HybridObject {
+# CHECK:        Format: COFF-ARM64EC
+# CHECK-NEXT:   Arch: aarch64
+# CHECK-NEXT:   AddressSize: 64bit
+# CHECK-NEXT:   Sections [
+# CHECK-NEXT:     Section {
+# CHECK-NEXT:       Number: 1
+# CHECK-NEXT:       Name: .data (2E 64 61 74 61 00 00 00)
+# CHECK-NEXT:       VirtualSize: 0x0
+# CHECK-NEXT:       VirtualAddress: 0x0
+# CHECK-NEXT:       RawDataSize: 4
+# CHECK-NEXT:       PointerToRawData: 0x3C
+# CHECK-NEXT:       PointerToRelocations: 0x0
+# CHECK-NEXT:       PointerToLineNumbers: 0x0
+# CHECK-NEXT:       RelocationCount: 0
+# CHECK-NEXT:       LineNumberCount: 0
+# CHECK-NEXT:       Characteristics [ (0xC0300040)
+# CHECK-NEXT:         IMAGE_SCN_ALIGN_4BYTES (0x300000)
+# CHECK-NEXT:         IMAGE_SCN_CNT_INITIALIZED_DATA (0x40)
+# CHECK-NEXT:         IMAGE_SCN_MEM_READ (0x40000000)
+# CHECK-NEXT:         IMAGE_SCN_MEM_WRITE (0x80000000)
+# CHECK-NEXT:       ]
+# CHECK-NEXT:     }
+# CHECK-NEXT:   ]
+# CHECK-NEXT: }
+
+--- !COFF
+header:
+  Machine:         [[MACHINE]]
+  Characteristics: [  ]
+sections:
+  - Name:            .data
+    Characteristics: [ IMAGE_SCN_CNT_INITIALIZED_DATA, IMAGE_SCN_MEM_READ, 
IMAGE_SCN_MEM_WRITE ]
+    Alignment:       4
+    SectionData:     '00000000'
+    SizeOfRawData:   4
+symbols:
+  - Name:            .data
+    Value:           0
+    SectionNumber:   1
+    SimpleType:      IMAGE_SYM_TYPE_NULL
+    ComplexType:     IMAGE_SYM_DTYPE_NULL
+    StorageClass:    IMAGE_SYM_CLASS_STATIC
+    SectionDefinition:
+      Length:          4
+      NumberOfRelocations: 0
+      NumberOfLinenumbers: 0
+      CheckSum:        0
+      Number:          2
+  - Name:            sym
+    Value:           0
+    SectionNumber:   1
+    SimpleType:      IMAGE_SYM_TYPE_NULL
+    ComplexType:     IMAGE_SYM_DTYPE_NULL
+    StorageClass:    IMAGE_SYM_CLASS_EXTERNAL
+...
diff --git a/llvm/tools/llvm-readobj/llvm-readobj.cpp 
b/llvm/tools/llvm-readobj/llvm-readobj.cpp
index fa56e3e48e58c..cfb51c18b8484 100644
--- a/llvm/tools/llvm-readobj/llvm-readobj.cpp
+++ b/llvm/tools/llvm-readobj/llvm-readobj.cpp
@@ -604,11 +604,16 @@ static void dumpCOFFObject(COFFObjectFile *Obj, 
ScopedPrinter &Writer) {
   dumpObject(*Obj, Writer);
 
   // Dump a hybrid object when available.
-  std::unique_ptr<MemoryBuffer> HybridView = Obj->getHybridObjectView();
-  if (!HybridView)
+  MemoryBufferRef HybridView;
+  std::unique_ptr<MemoryBuffer> HybridViewBuf;
+  if (std::optional<MemoryBufferRef> HybridSec = Obj->getHybridObjectSection())
+    HybridView = *HybridSec;
+  else if ((HybridViewBuf = Obj->getHybridObjectView()))
+    HybridView = HybridViewBuf->getMemBufferRef();
+  else
     return;
   Expected<std::unique_ptr<COFFObjectFile>> HybridObjOrErr =
-      COFFObjectFile::create(*HybridView);
+      COFFObjectFile::create(HybridView);
   if (!HybridObjOrErr)
     reportError(HybridObjOrErr.takeError(), Obj->getFileName().str());
   DictScope D(Writer, "HybridObject");

>From 168d83b4d476a49757b8ea3bd06e4e3aa7a449ad Mon Sep 17 00:00:00 2001
From: Jacek Caban <[email protected]>
Date: Tue, 26 May 2026 16:45:40 +0200
Subject: [PATCH 2/6] [Object][ArchiveWriter] Factor out MemberData allocation

Factor out MemberData allocation in preparation for handling hybrid
object files, which may require creating additional archive members.
---
 llvm/lib/Object/ArchiveWriter.cpp | 64 ++++++++++++++-----------------
 1 file changed, 29 insertions(+), 35 deletions(-)

diff --git a/llvm/lib/Object/ArchiveWriter.cpp 
b/llvm/lib/Object/ArchiveWriter.cpp
index 4610fb4303274..584a5c2c597e8 100644
--- a/llvm/lib/Object/ArchiveWriter.cpp
+++ b/llvm/lib/Object/ArchiveWriter.cpp
@@ -338,6 +338,7 @@ struct MemberData {
   StringRef Padding;
   uint64_t PreHeadPadSize = 0;
   std::unique_ptr<SymbolicFile> SymFile = nullptr;
+  const NewArchiveMember *NewMember = nullptr;
 };
 } // namespace
 
@@ -783,7 +784,6 @@ computeMemberData(raw_ostream &StringTable, raw_ostream 
&SymNames,
                   LLVMContext &Context, ArrayRef<NewArchiveMember> NewMembers,
                   std::optional<bool> IsEC, function_ref<void(Error)> Warn) {
   static char PaddingData[8] = {'\n', '\n', '\n', '\n', '\n', '\n', '\n', 
'\n'};
-  uint64_t MemHeadPadSize = 0;
   uint64_t Pos =
       isAIXBigArchive(Kind) ? sizeof(object::BigArchive::FixLenHdr) : 0;
 
@@ -845,17 +845,18 @@ computeMemberData(raw_ostream &StringTable, raw_ostream 
&SymNames,
       Entry.second = Entry.second > 1 ? 1 : 0;
   }
 
-  std::vector<std::unique_ptr<SymbolicFile>> SymFiles;
+  for (const NewArchiveMember &M : NewMembers) {
+    MemberData &D = Ret.emplace_back();
+    D.NewMember = &M;
 
-  if (NeedSymbols != SymtabWritingMode::NoSymtab || isAIXBigArchive(Kind)) {
-    for (const NewArchiveMember &M : NewMembers) {
+    if (NeedSymbols != SymtabWritingMode::NoSymtab || isAIXBigArchive(Kind)) {
       Expected<std::unique_ptr<SymbolicFile>> SymFileOrErr = getSymbolicFile(
           M.Buf->getMemBufferRef(), Context, Kind, [&](Error Err) {
             Warn(createFileError(M.MemberName, std::move(Err)));
           });
       if (!SymFileOrErr)
         return createFileError(M.MemberName, SymFileOrErr.takeError());
-      SymFiles.push_back(std::move(*SymFileOrErr));
+      D.SymFile = std::move(*SymFileOrErr);
     }
   }
 
@@ -868,13 +869,13 @@ computeMemberData(raw_ostream &StringTable, raw_ostream 
&SymNames,
       // AMD64). This may be a single ARM64EC object, but may also be separate
       // ARM64 and AMD64 objects.
       bool HaveArm64 = false, HaveEC = false;
-      for (std::unique_ptr<SymbolicFile> &SymFile : SymFiles) {
-        if (!SymFile)
+      for (const MemberData &D : Ret) {
+        if (!D.SymFile)
           continue;
         if (!HaveArm64)
-          HaveArm64 = isAnyArm64COFF(*SymFile);
+          HaveArm64 = isAnyArm64COFF(*D.SymFile);
         if (!HaveEC)
-          HaveEC = isECObject(*SymFile);
+          HaveEC = isECObject(*D.SymFile);
         if (HaveArm64 && HaveEC) {
           SymMap->UseECMap = true;
           break;
@@ -888,23 +889,23 @@ computeMemberData(raw_ostream &StringTable, raw_ostream 
&SymNames,
   uint64_t PrevOffset = 0;
   uint64_t NextMemHeadPadSize = 0;
 
-  for (uint32_t Index = 0; Index < NewMembers.size(); ++Index) {
-    const NewArchiveMember *M = &NewMembers[Index];
-    std::string Header;
-    raw_string_ostream Out(Header);
+  for (uint32_t Index = 0; Index < Ret.size(); ++Index) {
+    MemberData &D = Ret[Index];
+    const NewArchiveMember *M = D.NewMember;
+    raw_string_ostream Out(D.Header);
 
     MemoryBufferRef Buf = M->Buf->getMemBufferRef();
-    StringRef Data = Thin ? "" : Buf.getBuffer();
+    D.Data = Thin ? "" : Buf.getBuffer();
 
     // ld64 expects the members to be 8-byte aligned for 64-bit content and at
     // least 4-byte aligned for 32-bit content.  Opt for the larger encoding
     // uniformly.  This matches the behaviour with cctools and ensures that 
ld64
     // is happy with archives that we generate.
     unsigned MemberPadding =
-        isDarwin(Kind) ? offsetToAlignment(Data.size(), Align(8)) : 0;
+        isDarwin(Kind) ? offsetToAlignment(D.Data.size(), Align(8)) : 0;
     unsigned TailPadding =
-        offsetToAlignment(Data.size() + MemberPadding, Align(2));
-    StringRef Padding = StringRef(PaddingData, MemberPadding + TailPadding);
+        offsetToAlignment(D.Data.size() + MemberPadding, Align(2));
+    D.Padding = StringRef(PaddingData, MemberPadding + TailPadding);
 
     sys::TimePoint<std::chrono::seconds> ModTime;
     if (UniqueTimestamps)
@@ -921,36 +922,32 @@ computeMemberData(raw_ostream &StringTable, raw_ostream 
&SymNames,
           std::move(StringMsg), object::object_error::parse_failed);
     }
 
-    std::unique_ptr<SymbolicFile> CurSymFile;
-    if (!SymFiles.empty())
-      CurSymFile = std::move(SymFiles[Index]);
-
     // In the big archive file format, we need to calculate and include the 
next
     // member offset and previous member offset in the file member header.
     if (isAIXBigArchive(Kind)) {
       uint64_t OffsetToMemData = Pos + sizeof(object::BigArMemHdrType) +
                                  alignTo(M->MemberName.size(), 2);
 
-      if (M == NewMembers.begin())
+      if (Index == 0)
         NextMemHeadPadSize =
             alignToPowerOf2(OffsetToMemData,
-                            getMemberAlignment(CurSymFile.get())) -
+                            getMemberAlignment(D.SymFile.get())) -
             OffsetToMemData;
 
-      MemHeadPadSize = NextMemHeadPadSize;
-      Pos += MemHeadPadSize;
+      D.PreHeadPadSize = NextMemHeadPadSize;
+      Pos += D.PreHeadPadSize;
       uint64_t NextOffset = Pos + sizeof(object::BigArMemHdrType) +
                             alignTo(M->MemberName.size(), 2) + alignTo(Size, 
2);
 
       // If there is another member file after this, we need to calculate the
       // padding before the header.
-      if (Index + 1 != SymFiles.size()) {
+      if (Index + 1 != Ret.size()) {
         uint64_t OffsetToNextMemData =
             NextOffset + sizeof(object::BigArMemHdrType) +
-            alignTo(NewMembers[Index + 1].MemberName.size(), 2);
+            alignTo(Ret[Index + 1].NewMember->MemberName.size(), 2);
         NextMemHeadPadSize =
             alignToPowerOf2(OffsetToNextMemData,
-                            getMemberAlignment(SymFiles[Index + 1].get())) -
+                            getMemberAlignment(Ret[Index + 1].SymFile.get())) -
             OffsetToNextMemData;
         NextOffset += NextMemHeadPadSize;
       }
@@ -962,20 +959,17 @@ computeMemberData(raw_ostream &StringTable, raw_ostream 
&SymNames,
                         ModTime, Size);
     }
 
-    std::vector<unsigned> Symbols;
     if (NeedSymbols != SymtabWritingMode::NoSymtab) {
       Expected<std::vector<unsigned>> SymbolsOrErr =
-          getSymbols(CurSymFile.get(), Index + 1, SymNames, SymMap);
+          getSymbols(D.SymFile.get(), Index + 1, SymNames, SymMap);
       if (!SymbolsOrErr)
         return createFileError(M->MemberName, SymbolsOrErr.takeError());
-      Symbols = std::move(*SymbolsOrErr);
-      if (CurSymFile)
+      D.Symbols = std::move(*SymbolsOrErr);
+      if (D.SymFile)
         HasObject = true;
     }
 
-    Pos += Header.size() + Data.size() + Padding.size();
-    Ret.push_back({std::move(Symbols), std::move(Header), Data, Padding,
-                   MemHeadPadSize, std::move(CurSymFile)});
+    Pos += D.Header.size() + D.Data.size() + D.Padding.size();
   }
   // If there are no symbols, emit an empty symbol table, to satisfy Solaris
   // tools, older versions of which expect a symbol table in a non-empty

>From 5041437ce6e49274a9f5fffcfc0ffd71d0d87f9b Mon Sep 17 00:00:00 2001
From: Jacek Caban <[email protected]>
Date: Wed, 27 May 2026 16:07:54 +0200
Subject: [PATCH 3/6] [Archive][COFF] Split hybrid COFF files when adding them
 to an archive

Create a separate member for the embedded hybrid object so that it's
properly reflected in the archive's symbol map and can be correctly
processed by tools unaware of multi-arch object files. Prefix the
embedded member name with 'llvm.arm64x/' to avoid name collisions.

We could also strip the .llvm.arm64x section from the original file.
I have a draft patch doing that I plan as a follow-up. However, that
is mostly an optimization, as the original file remains a valid ARM64
file regardless.
---
 llvm/lib/Object/ArchiveWriter.cpp             | 55 +++++++++++++------
 llvm/test/tools/llvm-ar/arm64x-hybridobj.yaml | 50 +++++++++++++++++
 .../test/tools/llvm-lib/arm64x-hybridobj.yaml | 49 +++++++++++++++++
 3 files changed, 138 insertions(+), 16 deletions(-)
 create mode 100644 llvm/test/tools/llvm-ar/arm64x-hybridobj.yaml
 create mode 100644 llvm/test/tools/llvm-lib/arm64x-hybridobj.yaml

diff --git a/llvm/lib/Object/ArchiveWriter.cpp 
b/llvm/lib/Object/ArchiveWriter.cpp
index 584a5c2c597e8..cc873fa8cb68a 100644
--- a/llvm/lib/Object/ArchiveWriter.cpp
+++ b/llvm/lib/Object/ArchiveWriter.cpp
@@ -301,24 +301,24 @@ static bool is64BitKind(object::Archive::Kind Kind) {
 static void
 printMemberHeader(raw_ostream &Out, uint64_t Pos, raw_ostream &StringTable,
                   StringMap<uint64_t> &MemberNames, object::Archive::Kind Kind,
-                  bool Thin, const NewArchiveMember &M,
+                  bool Thin, const NewArchiveMember &M, StringRef MemberName,
                   sys::TimePoint<std::chrono::seconds> ModTime, uint64_t Size) 
{
   if (isBSDLike(Kind))
-    return printBSDMemberHeader(Out, Pos, M.MemberName, ModTime, M.UID, M.GID,
+    return printBSDMemberHeader(Out, Pos, MemberName, ModTime, M.UID, M.GID,
                                 M.Perms, Size);
-  if (!useStringTable(Thin, M.MemberName))
-    return printGNUSmallMemberHeader(Out, M.MemberName, ModTime, M.UID, M.GID,
+  if (!useStringTable(Thin, MemberName))
+    return printGNUSmallMemberHeader(Out, MemberName, ModTime, M.UID, M.GID,
                                      M.Perms, Size);
   Out << '/';
   uint64_t NamePos;
   if (Thin) {
     NamePos = StringTable.tell();
-    StringTable << M.MemberName << "/\n";
+    StringTable << MemberName << "/\n";
   } else {
-    auto Insertion = MemberNames.insert({M.MemberName, uint64_t(0)});
+    auto Insertion = MemberNames.insert({MemberName, uint64_t(0)});
     if (Insertion.second) {
       Insertion.first->second = StringTable.tell();
-      StringTable << M.MemberName;
+      StringTable << MemberName;
       if (isCOFFArchive(Kind))
         StringTable << '\0';
       else
@@ -339,6 +339,7 @@ struct MemberData {
   uint64_t PreHeadPadSize = 0;
   std::unique_ptr<SymbolicFile> SymFile = nullptr;
   const NewArchiveMember *NewMember = nullptr;
+  std::string HybridName = "";
 };
 } // namespace
 
@@ -857,6 +858,25 @@ computeMemberData(raw_ostream &StringTable, raw_ostream 
&SymNames,
       if (!SymFileOrErr)
         return createFileError(M.MemberName, SymFileOrErr.takeError());
       D.SymFile = std::move(*SymFileOrErr);
+
+      if (SymMap && D.SymFile.get()) {
+        auto COFFObj = dyn_cast<COFFObjectFile>(D.SymFile.get());
+        std::optional<MemoryBufferRef> HybridView;
+        if (COFFObj && (HybridView = COFFObj->getHybridObjectSection())) {
+          // Create a separate archive member for the hybrid ARM64X object.
+          MemberData &HybridData = Ret.emplace_back();
+          HybridData.NewMember = &M;
+
+          SymFileOrErr =
+              getSymbolicFile(*HybridView, Context, Kind, [&](Error Err) {
+                Warn(createFileError(M.MemberName, std::move(Err)));
+              });
+          if (!SymFileOrErr)
+            return createFileError(M.MemberName, SymFileOrErr.takeError());
+          HybridData.SymFile = std::move(*SymFileOrErr);
+          HybridData.HybridName = ("llvm.arm64x/" + M.MemberName).str();
+        }
+      }
     }
   }
 
@@ -894,7 +914,8 @@ computeMemberData(raw_ostream &StringTable, raw_ostream 
&SymNames,
     const NewArchiveMember *M = D.NewMember;
     raw_string_ostream Out(D.Header);
 
-    MemoryBufferRef Buf = M->Buf->getMemBufferRef();
+    MemoryBufferRef Buf =
+        D.SymFile ? D.SymFile->getMemoryBufferRef() : 
M->Buf->getMemBufferRef();
     D.Data = Thin ? "" : Buf.getBuffer();
 
     // ld64 expects the members to be 8-byte aligned for 64-bit content and at
@@ -907,17 +928,19 @@ computeMemberData(raw_ostream &StringTable, raw_ostream 
&SymNames,
         offsetToAlignment(D.Data.size() + MemberPadding, Align(2));
     D.Padding = StringRef(PaddingData, MemberPadding + TailPadding);
 
+    StringRef MemberName = D.HybridName.size() ? D.HybridName : M->MemberName;
+
     sys::TimePoint<std::chrono::seconds> ModTime;
     if (UniqueTimestamps)
       // Increment timestamp for each file of a given name.
-      ModTime = sys::toTimePoint(FilenameCount[M->MemberName]++);
+      ModTime = sys::toTimePoint(FilenameCount[MemberName]++);
     else
       ModTime = M->ModTime;
 
     uint64_t Size = Buf.getBufferSize() + MemberPadding;
     if (Size > object::Archive::MaxMemberSize) {
       std::string StringMsg =
-          "File " + M->MemberName.str() + " exceeds size limit";
+          "File " + MemberName.str() + " exceeds size limit";
       return make_error<object::GenericBinaryError>(
           std::move(StringMsg), object::object_error::parse_failed);
     }
@@ -925,8 +948,8 @@ computeMemberData(raw_ostream &StringTable, raw_ostream 
&SymNames,
     // In the big archive file format, we need to calculate and include the 
next
     // member offset and previous member offset in the file member header.
     if (isAIXBigArchive(Kind)) {
-      uint64_t OffsetToMemData = Pos + sizeof(object::BigArMemHdrType) +
-                                 alignTo(M->MemberName.size(), 2);
+      uint64_t OffsetToMemData =
+          Pos + sizeof(object::BigArMemHdrType) + alignTo(MemberName.size(), 
2);
 
       if (Index == 0)
         NextMemHeadPadSize =
@@ -937,7 +960,7 @@ computeMemberData(raw_ostream &StringTable, raw_ostream 
&SymNames,
       D.PreHeadPadSize = NextMemHeadPadSize;
       Pos += D.PreHeadPadSize;
       uint64_t NextOffset = Pos + sizeof(object::BigArMemHdrType) +
-                            alignTo(M->MemberName.size(), 2) + alignTo(Size, 
2);
+                            alignTo(MemberName.size(), 2) + alignTo(Size, 2);
 
       // If there is another member file after this, we need to calculate the
       // padding before the header.
@@ -951,19 +974,19 @@ computeMemberData(raw_ostream &StringTable, raw_ostream 
&SymNames,
             OffsetToNextMemData;
         NextOffset += NextMemHeadPadSize;
       }
-      printBigArchiveMemberHeader(Out, M->MemberName, ModTime, M->UID, M->GID,
+      printBigArchiveMemberHeader(Out, MemberName, ModTime, M->UID, M->GID,
                                   M->Perms, Size, PrevOffset, NextOffset);
       PrevOffset = Pos;
     } else {
       printMemberHeader(Out, Pos, StringTable, MemberNames, Kind, Thin, *M,
-                        ModTime, Size);
+                        MemberName, ModTime, Size);
     }
 
     if (NeedSymbols != SymtabWritingMode::NoSymtab) {
       Expected<std::vector<unsigned>> SymbolsOrErr =
           getSymbols(D.SymFile.get(), Index + 1, SymNames, SymMap);
       if (!SymbolsOrErr)
-        return createFileError(M->MemberName, SymbolsOrErr.takeError());
+        return createFileError(MemberName, SymbolsOrErr.takeError());
       D.Symbols = std::move(*SymbolsOrErr);
       if (D.SymFile)
         HasObject = true;
diff --git a/llvm/test/tools/llvm-ar/arm64x-hybridobj.yaml 
b/llvm/test/tools/llvm-ar/arm64x-hybridobj.yaml
new file mode 100644
index 0000000000000..884489e6701a4
--- /dev/null
+++ b/llvm/test/tools/llvm-ar/arm64x-hybridobj.yaml
@@ -0,0 +1,50 @@
+# RUN: yaml2obj -DMACHINE=IMAGE_FILE_MACHINE_ARM64 %s -o %t-aarch64.o
+# RUN: yaml2obj -DMACHINE=IMAGE_FILE_MACHINE_ARM64EC %s -o %t-arm64ec.o
+# RUN: llvm-objcopy --add-section=.llvm.arm64x=%t-arm64ec.o 
--set-section-flags=.llvm.arm64x=debug %t-aarch64.o %t.o
+# RUN: rm -f %t.lib
+# RUN: llvm-ar rc %t.lib %t.o
+# RUN: llvm-nm --print-armap %t.lib | FileCheck %s
+
+# CHECK:      Archive map
+# CHECK-NEXT: sym in arm64x-hybridobj.yaml.tmp.o
+# CHECK-EMPTY:
+# CHECK-NEXT: Archive EC map
+# CHECK-NEXT: sym in llvm.arm64x/arm64x-hybridobj.yaml.tmp.o
+# CHECK-EMPTY:
+# CHECK-EMPTY:
+# CHECK-NEXT: arm64x-hybridobj.yaml.tmp.o:
+# CHECK-NEXT: 00000000 D sym
+# CHECK-EMPTY:
+# CHECK-NEXT: llvm.arm64x/arm64x-hybridobj.yaml.tmp.o:
+# CHECK-NEXT: 00000000 D sym
+
+--- !COFF
+header:
+  Machine:         [[MACHINE]]
+  Characteristics: [  ]
+sections:
+  - Name:            .data
+    Characteristics: [ IMAGE_SCN_CNT_INITIALIZED_DATA, IMAGE_SCN_MEM_READ, 
IMAGE_SCN_MEM_WRITE ]
+    Alignment:       4
+    SectionData:     '00000000'
+    SizeOfRawData:   4
+symbols:
+  - Name:            .data
+    Value:           0
+    SectionNumber:   1
+    SimpleType:      IMAGE_SYM_TYPE_NULL
+    ComplexType:     IMAGE_SYM_DTYPE_NULL
+    StorageClass:    IMAGE_SYM_CLASS_STATIC
+    SectionDefinition:
+      Length:          4
+      NumberOfRelocations: 0
+      NumberOfLinenumbers: 0
+      CheckSum:        0
+      Number:          2
+  - Name:            sym
+    Value:           0
+    SectionNumber:   1
+    SimpleType:      IMAGE_SYM_TYPE_NULL
+    ComplexType:     IMAGE_SYM_DTYPE_NULL
+    StorageClass:    IMAGE_SYM_CLASS_EXTERNAL
+...
diff --git a/llvm/test/tools/llvm-lib/arm64x-hybridobj.yaml 
b/llvm/test/tools/llvm-lib/arm64x-hybridobj.yaml
new file mode 100644
index 0000000000000..b893cc70d1c19
--- /dev/null
+++ b/llvm/test/tools/llvm-lib/arm64x-hybridobj.yaml
@@ -0,0 +1,49 @@
+# RUN: yaml2obj -DMACHINE=IMAGE_FILE_MACHINE_ARM64 %s -o %t-aarch64.o
+# RUN: yaml2obj -DMACHINE=IMAGE_FILE_MACHINE_ARM64EC %s -o %t-arm64ec.o
+# RUN: llvm-objcopy --add-section=.llvm.arm64x=%t-arm64ec.o 
--set-section-flags=.llvm.arm64x=debug %t-aarch64.o %t.o
+# RUN: llvm-lib -machine:arm64x -out:%t.lib %t.o
+# RUN: llvm-nm --print-armap %t.lib | FileCheck %s
+
+# CHECK:      Archive map
+# CHECK-NEXT: sym in {{.*}}/arm64x-hybridobj.yaml.tmp.o
+# CHECK-EMPTY:
+# CHECK-NEXT: Archive EC map
+# CHECK-NEXT: sym in llvm.arm64x/{{.*}}/arm64x-hybridobj.yaml.tmp.o
+# CHECK-EMPTY:
+# CHECK-EMPTY:
+# CHECK-NEXT: {{.*}}/arm64x-hybridobj.yaml.tmp.o:
+# CHECK-NEXT: 00000000 D sym
+# CHECK-EMPTY:
+# CHECK-NEXT: llvm.arm64x/{{.*}}/arm64x-hybridobj.yaml.tmp.o:
+# CHECK-NEXT: 00000000 D sym
+
+--- !COFF
+header:
+  Machine:         [[MACHINE]]
+  Characteristics: [  ]
+sections:
+  - Name:            .data
+    Characteristics: [ IMAGE_SCN_CNT_INITIALIZED_DATA, IMAGE_SCN_MEM_READ, 
IMAGE_SCN_MEM_WRITE ]
+    Alignment:       4
+    SectionData:     '00000000'
+    SizeOfRawData:   4
+symbols:
+  - Name:            .data
+    Value:           0
+    SectionNumber:   1
+    SimpleType:      IMAGE_SYM_TYPE_NULL
+    ComplexType:     IMAGE_SYM_DTYPE_NULL
+    StorageClass:    IMAGE_SYM_CLASS_STATIC
+    SectionDefinition:
+      Length:          4
+      NumberOfRelocations: 0
+      NumberOfLinenumbers: 0
+      CheckSum:        0
+      Number:          2
+  - Name:            sym
+    Value:           0
+    SectionNumber:   1
+    SimpleType:      IMAGE_SYM_TYPE_NULL
+    ComplexType:     IMAGE_SYM_DTYPE_NULL
+    StorageClass:    IMAGE_SYM_CLASS_EXTERNAL
+...

>From b729297aa7b3cc8d7b086e395e3b3199bccbd655 Mon Sep 17 00:00:00 2001
From: Jacek Caban <[email protected]>
Date: Mon, 8 Jun 2026 17:59:55 +0200
Subject: [PATCH 4/6] [LLD][COFF] Factor out addObjectFile

Avoid parsing the input COFF file twice by creating it earlier and
replacing findBitcodeInMemBuffer with findBitcodeInObject. It's also
a preparation for handling hybrid ARM64X object files.
---
 lld/COFF/Driver.cpp     | 28 +++++++++++++++++-----------
 lld/COFF/Driver.h       |  4 ++++
 lld/COFF/InputFiles.cpp | 12 +++++++++---
 lld/COFF/InputFiles.h   |  9 ++++++++-
 4 files changed, 38 insertions(+), 15 deletions(-)

diff --git a/lld/COFF/Driver.cpp b/lld/COFF/Driver.cpp
index 024cb2c95cd20..34b8c85116984 100644
--- a/lld/COFF/Driver.cpp
+++ b/lld/COFF/Driver.cpp
@@ -262,21 +262,27 @@ MemoryBufferRef 
LinkerDriver::takeBuffer(std::unique_ptr<MemoryBuffer> mb) {
   return mbref;
 }
 
-static InputFile *tryCreateFatLTOFile(COFFLinkerContext &ctx,
-                                      MemoryBufferRef mb, StringRef 
archiveName,
-                                      uint64_t offsetInArchive, bool lazy) {
+InputFile *LinkerDriver::addObjectFile(COFFLinkerContext &ctx,
+                                       MemoryBufferRef mb,
+                                       StringRef archiveName,
+                                       uint64_t offsetInArchive, bool lazy) {
+  COFFObjectFile *coffObj = ObjFile::createCOFFObject(ctx, mb);
+  InputFile *obj = nullptr;
+
   if (ctx.config.fatLTOObjects) {
     Expected<MemoryBufferRef> fatLTOData =
-        IRObjectFile::findBitcodeInMemBuffer(mb);
+        IRObjectFile::findBitcodeInObject(*coffObj);
 
     if (!errorToBool(fatLTOData.takeError())) {
-      return BitcodeFile::create(ctx, *fatLTOData, archiveName, 
offsetInArchive,
-                                 lazy);
+      obj = BitcodeFile::create(ctx, *fatLTOData, archiveName, offsetInArchive,
+                                lazy);
     }
   }
 
-  InputFile *obj = ObjFile::create(ctx, mb, lazy);
+  if (!obj)
+    obj = ObjFile::create(ctx, coffObj, lazy);
   obj->parentName = archiveName;
+  addFile(obj);
   return obj;
 }
 
@@ -323,7 +329,7 @@ void LinkerDriver::addBuffer(std::unique_ptr<MemoryBuffer> 
mb,
     addFile(BitcodeFile::create(ctx, mbref, "", 0, lazy));
     break;
   case file_magic::coff_object: {
-    addFile(tryCreateFatLTOFile(ctx, mbref, "", 0, lazy));
+    addObjectFile(ctx, mbref, "", 0, lazy);
     break;
   }
   case file_magic::coff_import_library:
@@ -437,9 +443,11 @@ void LinkerDriver::addArchiveBuffer(MemoryBufferRef mb, 
StringRef symName,
 
   InputFile *obj;
   if (magic == file_magic::coff_object) {
-    obj = tryCreateFatLTOFile(ctx, mb, parentName, offsetInArchive, lazy);
+    obj = addObjectFile(ctx, mb, parentName, offsetInArchive, lazy);
   } else if (magic == file_magic::bitcode) {
     obj = BitcodeFile::create(ctx, mb, parentName, offsetInArchive, lazy);
+    obj->parentName = parentName;
+    addFile(obj);
   } else if (magic == file_magic::coff_cl_gl_object) {
     Err(ctx) << mb.getBufferIdentifier()
              << ": is not a native COFF file. Recompile without /GL?";
@@ -449,8 +457,6 @@ void LinkerDriver::addArchiveBuffer(MemoryBufferRef mb, 
StringRef symName,
     return;
   }
 
-  obj->parentName = parentName;
-  addFile(obj);
   Log(ctx) << "Loaded " << obj << " for " << symName;
 }
 
diff --git a/lld/COFF/Driver.h b/lld/COFF/Driver.h
index e7a7acebc6e4c..7eea9aee279a1 100644
--- a/lld/COFF/Driver.h
+++ b/lld/COFF/Driver.h
@@ -125,6 +125,10 @@ class LinkerDriver {
 
   bool isDecorated(StringRef sym);
 
+  InputFile *addObjectFile(COFFLinkerContext &ctx, MemoryBufferRef mb,
+                           StringRef archiveName, uint64_t offsetInArchive,
+                           bool lazy);
+
   std::string getMapFile(const llvm::opt::InputArgList &args,
                          llvm::opt::OptSpecifier os,
                          llvm::opt::OptSpecifier osFile);
diff --git a/lld/COFF/InputFiles.cpp b/lld/COFF/InputFiles.cpp
index 3821c9366c56a..a9113f8260576 100644
--- a/lld/COFF/InputFiles.cpp
+++ b/lld/COFF/InputFiles.cpp
@@ -278,7 +278,8 @@ ObjFile::ObjFile(SymbolTable &symtab, COFFObjectFile 
*coffObj, bool lazy)
     : InputFile(symtab, ObjectKind, coffObj->getMemoryBufferRef(), lazy),
       coffObj(coffObj) {}
 
-ObjFile *ObjFile::create(COFFLinkerContext &ctx, MemoryBufferRef m, bool lazy) 
{
+COFFObjectFile *ObjFile::createCOFFObject(COFFLinkerContext &ctx,
+                                          MemoryBufferRef m) {
   // Parse a memory buffer as a COFF file.
   Expected<std::unique_ptr<Binary>> bin = createBinary(m);
   if (!bin)
@@ -289,8 +290,13 @@ ObjFile *ObjFile::create(COFFLinkerContext &ctx, 
MemoryBufferRef m, bool lazy) {
     Fatal(ctx) << m.getBufferIdentifier() << " is not a COFF file";
 
   bin->release();
-  return make<ObjFile>(ctx.getSymtab(MachineTypes(obj->getMachine())), obj,
-                       lazy);
+  return obj;
+}
+
+ObjFile *ObjFile::create(COFFLinkerContext &ctx, COFFObjectFile *coffObj,
+                         bool lazy) {
+  return make<ObjFile>(ctx.getSymtab(MachineTypes(coffObj->getMachine())),
+                       coffObj, lazy);
 }
 
 void ObjFile::parseLazy() {
diff --git a/lld/COFF/InputFiles.h b/lld/COFF/InputFiles.h
index ce8bc6705e489..b9cf3bd0339c4 100644
--- a/lld/COFF/InputFiles.h
+++ b/lld/COFF/InputFiles.h
@@ -136,10 +136,17 @@ class ArchiveFile : public InputFile {
 // .obj or .o file. This may be a member of an archive file.
 class ObjFile : public InputFile {
 public:
-  static ObjFile *create(COFFLinkerContext &ctx, MemoryBufferRef mb,
+  static ObjFile *create(COFFLinkerContext &ctx, COFFObjectFile *coffObj,
                          bool lazy = false);
+  static ObjFile *create(COFFLinkerContext &ctx, MemoryBufferRef mb,
+                         bool lazy = false) {
+    return ObjFile::create(ctx, ObjFile::createCOFFObject(ctx, mb), lazy);
+  }
   explicit ObjFile(SymbolTable &symtab, COFFObjectFile *coffObj, bool lazy);
 
+  static COFFObjectFile *createCOFFObject(COFFLinkerContext &ctx,
+                                          MemoryBufferRef mb);
+
   static bool classof(const InputFile *f) { return f->kind() == ObjectKind; }
   void parse() override;
   void parseLazy();

>From 86f66bb282aaffc4af0cf49092849ec0a553bc24 Mon Sep 17 00:00:00 2001
From: Jacek Caban <[email protected]>
Date: Mon, 8 Jun 2026 18:02:40 +0200
Subject: [PATCH 5/6] [LLD][COFF] Add support for multi-atch ARM64X object
 files

---
 lld/COFF/Driver.cpp              | 21 ++++++++--
 lld/COFF/Driver.h                |  4 +-
 lld/test/COFF/arm64x-hybridobj.s | 71 ++++++++++++++++++++++++++++++++
 3 files changed, 90 insertions(+), 6 deletions(-)
 create mode 100644 lld/test/COFF/arm64x-hybridobj.s

diff --git a/lld/COFF/Driver.cpp b/lld/COFF/Driver.cpp
index 34b8c85116984..aa3b880b2418a 100644
--- a/lld/COFF/Driver.cpp
+++ b/lld/COFF/Driver.cpp
@@ -265,10 +265,21 @@ MemoryBufferRef 
LinkerDriver::takeBuffer(std::unique_ptr<MemoryBuffer> mb) {
 InputFile *LinkerDriver::addObjectFile(COFFLinkerContext &ctx,
                                        MemoryBufferRef mb,
                                        StringRef archiveName,
-                                       uint64_t offsetInArchive, bool lazy) {
+                                       uint64_t offsetInArchive, bool lazy,
+                                       bool addHybrid) {
   COFFObjectFile *coffObj = ObjFile::createCOFFObject(ctx, mb);
   InputFile *obj = nullptr;
 
+  if (addHybrid && ctx.symtab.isEC()) {
+    if (std::optional<MemoryBufferRef> hybridView =
+            coffObj->getHybridObjectSection()) {
+      InputFile *hybridObj = addObjectFile(ctx, *hybridView, archiveName,
+                                           offsetInArchive, lazy, false);
+      if (ctx.config.machine != ARM64X)
+        return hybridObj;
+    }
+  }
+
   if (ctx.config.fatLTOObjects) {
     Expected<MemoryBufferRef> fatLTOData =
         IRObjectFile::findBitcodeInObject(*coffObj);
@@ -432,7 +443,8 @@ void LinkerDriver::enqueuePath(StringRef path, bool lazy, 
InputOpt inputOpt) {
 
 void LinkerDriver::addArchiveBuffer(MemoryBufferRef mb, StringRef symName,
                                     StringRef parentName,
-                                    uint64_t offsetInArchive, bool lazy) {
+                                    uint64_t offsetInArchive, bool lazy,
+                                    bool isThin) {
   file_magic magic = identify_magic(mb.getBuffer());
   if (magic == file_magic::coff_import_library) {
     InputFile *imp = make<ImportFile>(ctx, mb);
@@ -443,7 +455,7 @@ void LinkerDriver::addArchiveBuffer(MemoryBufferRef mb, 
StringRef symName,
 
   InputFile *obj;
   if (magic == file_magic::coff_object) {
-    obj = addObjectFile(ctx, mb, parentName, offsetInArchive, lazy);
+    obj = addObjectFile(ctx, mb, parentName, offsetInArchive, lazy, isThin);
   } else if (magic == file_magic::bitcode) {
     obj = BitcodeFile::create(ctx, mb, parentName, offsetInArchive, lazy);
     obj->parentName = parentName;
@@ -466,7 +478,8 @@ void LinkerDriver::addThinArchiveBuffer(MemoryBufferRef mb, 
StringRef symName,
   // the original filename is used as the buffer identifier. This is
   // useful for DTLTO, where having the member identifier be the actual
   // path on disk enables distribution of bitcode files during ThinLTO.
-  addArchiveBuffer(mb, symName, /*parentName=*/"", /*OffsetInArchive=*/0, 
lazy);
+  addArchiveBuffer(mb, symName, /*parentName=*/"", /*OffsetInArchive=*/0, lazy,
+                   true);
 }
 
 void LinkerDriver::enqueueArchiveMember(const Archive::Child &c,
diff --git a/lld/COFF/Driver.h b/lld/COFF/Driver.h
index 7eea9aee279a1..572ed85eed35e 100644
--- a/lld/COFF/Driver.h
+++ b/lld/COFF/Driver.h
@@ -127,7 +127,7 @@ class LinkerDriver {
 
   InputFile *addObjectFile(COFFLinkerContext &ctx, MemoryBufferRef mb,
                            StringRef archiveName, uint64_t offsetInArchive,
-                           bool lazy);
+                           bool lazy, bool addHybrid = true);
 
   std::string getMapFile(const llvm::opt::InputArgList &args,
                          llvm::opt::OptSpecifier os,
@@ -183,7 +183,7 @@ class LinkerDriver {
                  bool lazy);
   void addArchiveBuffer(MemoryBufferRef mbref, StringRef symName,
                         StringRef parentName, uint64_t offsetInArchive,
-                        bool lazy);
+                        bool lazy, bool isThin = false);
   void addThinArchiveBuffer(MemoryBufferRef mbref, StringRef symName,
                             bool lazy);
 
diff --git a/lld/test/COFF/arm64x-hybridobj.s b/lld/test/COFF/arm64x-hybridobj.s
new file mode 100644
index 0000000000000..8742e8fd03b8d
--- /dev/null
+++ b/lld/test/COFF/arm64x-hybridobj.s
@@ -0,0 +1,71 @@
+// REQUIRES: aarch64
+// RUN: split-file %s %t.dir && cd %t.dir
+
+// RUN: llvm-mc -filetype=obj -triple=aarch64-windows sym.s -o sym-arm64.obj
+// RUN: llvm-mc -filetype=obj -triple=arm64ec-windows sym.s -o sym-arm64ec.obj
+// RUN: llvm-objcopy --add-section=.llvm.arm64x=sym-arm64ec.obj 
--set-section-flags=.llvm.arm64x=debug \
+// RUN:              sym-arm64.obj sym.obj
+
+// RUN: llvm-mc -filetype=obj -triple=aarch64-windows ref.s -o ref-arm64.obj
+// RUN: llvm-mc -filetype=obj -triple=arm64ec-windows ref.s -o ref-arm64ec.obj
+// RUN: llvm-objcopy --add-section=.llvm.arm64x=ref-arm64ec.obj 
--set-section-flags=.llvm.arm64x=debug \
+// RUN:              ref-arm64.obj ref.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: llvm-objcopy --add-section=.llvm.arm64x=loadconfig-arm64ec.obj 
--set-section-flags=.llvm.arm64x=debug \
+// RUN:              loadconfig-arm64.obj loadconfig.obj
+
+// RUN: lld-link -machine:arm64x -dll -noentry -out:out.dll sym.obj 
loadconfig.obj
+// RUN: llvm-readobj --coff-exports out.dll | FileCheck %s
+
+// RUN: lld-link -machine:arm64ec -dll -noentry -out:out-ec.dll sym.obj 
loadconfig.obj
+// RUN: lld-link -machine:arm64 -dll -noentry -out:out-native.dll sym.obj 
loadconfig.obj
+
+// RUN: llvm-ar cr sym.lib sym.obj
+// RUN: lld-link -machine:arm64x -dll -noentry -out:out2.dll ref.obj sym.lib 
loadconfig.obj
+// RUN: llvm-readobj --coff-exports out2.dll | FileCheck %s
+// RUN: lld-link -machine:arm64ec -dll -noentry -out:out-ec2.dll sym.obj 
loadconfig.obj
+// RUN: lld-link -machine:arm64 -dll -noentry -out:out-native2.dll sym.obj 
loadconfig.obj
+
+// RUN: llvm-ar cr --thin sym-thin.lib sym.obj
+// RUN: lld-link -machine:arm64x -dll -noentry -out:out3.dll ref.obj 
sym-thin.lib loadconfig.obj
+// RUN: llvm-readobj --coff-exports out3.dll | FileCheck %s
+
+// RUN: lld-link -machine:arm64x -dll -noentry -out:out4.dll ref.obj 
-start-lib sym.obj loadconfig.obj -end-lib
+// RUN: llvm-readobj --coff-exports out4.dll | FileCheck %s
+
+// RUN: lld-link -machine:arm64x -dll -noentry -out:out4.dll 
-wholearchive:sym-thin.lib loadconfig.obj
+// RUN: llvm-readobj --coff-exports out4.dll | FileCheck %s
+
+// CHECK:      Format: COFF-ARM64X
+// CHECK-NEXT: Arch: aarch64
+// CHECK-NEXT: AddressSize: 64bit
+// CHECK-NEXT: Export {
+// CHECK-NEXT:   Ordinal: 1
+// CHECK-NEXT:   Name: sym
+// CHECK-NEXT:   RVA: 0x4004
+// CHECK-NEXT: }
+// CHECK-NEXT: HybridObject {
+// CHECK-NEXT:   Format: COFF-ARM64EC
+// CHECK-NEXT:   Arch: aarch64
+// CHECK-NEXT:   AddressSize: 64bit
+// CHECK-NEXT:   Export {
+// CHECK-NEXT:     Ordinal: 1
+// CHECK-NEXT:     Name: sym
+// CHECK-NEXT:     RVA: 0x4000
+// CHECK-NEXT:   }
+// CHECK-NEXT: }
+
+#--- sym.s
+        .section .sym,"dr"
+        .globl sym
+sym:
+        .long 0
+
+        .section .drectve
+        .ascii "-export:sym"
+
+#--- ref.s
+        .data
+        .rva sym

>From 0bbe2fbbfa3a46743902e0c8fabf267abf78b8d7 Mon Sep 17 00:00:00 2001
From: Jacek Caban <[email protected]>
Date: Wed, 1 Apr 2026 16:18:07 +0200
Subject: [PATCH 6/6] [clang][ARM64X] Support compiling both native and EC
 objects with -marm64x

When -marm64x is used during the assembly phase, construct jobs for both
native and EC targets and merge their outputs using llvm-objcopy.
---
 clang/include/clang/Driver/Action.h    |  2 +-
 clang/include/clang/Options/Options.td |  2 +-
 clang/lib/Driver/Action.cpp            |  4 +--
 clang/lib/Driver/Driver.cpp            | 37 +++++++++++++++++++-------
 clang/lib/Driver/ToolChain.cpp         |  4 ++-
 clang/lib/Driver/ToolChains/MSVC.cpp   | 11 ++++++++
 clang/lib/Driver/ToolChains/MSVC.h     |  4 +++
 clang/lib/Driver/ToolChains/MinGW.cpp  | 29 ++++++++++++++++++++
 clang/lib/Driver/ToolChains/MinGW.h    | 14 ++++++++++
 clang/test/Driver/arm64x.c             |  6 +++++
 clang/test/Driver/msvc-link.c          |  4 +--
 11 files changed, 101 insertions(+), 16 deletions(-)
 create mode 100644 clang/test/Driver/arm64x.c

diff --git a/clang/include/clang/Driver/Action.h 
b/clang/include/clang/Driver/Action.h
index 67937b00f6bcf..edad2e64e8d62 100644
--- a/clang/include/clang/Driver/Action.h
+++ b/clang/include/clang/Driver/Action.h
@@ -695,7 +695,7 @@ class ObjcopyJobAction : public JobAction {
   void anchor() override;
 
 public:
-  ObjcopyJobAction(Action *Input, types::ID Type);
+  ObjcopyJobAction(ActionList &Inputs, types::ID Type);
 
   static bool classof(const Action *A) {
     return A->getKind() == ObjcopyJobClass;
diff --git a/clang/include/clang/Options/Options.td 
b/clang/include/clang/Options/Options.td
index 4fd892e58df86..23a27c4db6985 100644
--- a/clang/include/clang/Options/Options.td
+++ b/clang/include/clang/Options/Options.td
@@ -5410,7 +5410,7 @@ def municode : Joined<["-"], "municode">, Group<m_Group>;
 def mthreads : Joined<["-"], "mthreads">, Group<m_Group>;
 def marm64x : Joined<["-"], "marm64x">, Group<m_Group>,
   Visibility<[ClangOption, CLOption]>,
-  HelpText<"Link as a hybrid ARM64X image">;
+  HelpText<"Build as a hybrid ARM64X image">;
 def mguard_EQ : Joined<["-"], "mguard=">, Group<m_Group>,
   HelpText<"Enable or disable Control Flow Guard checks and guard tables 
emission">,
   Values<"none,cf,cf-nochecks">;
diff --git a/clang/lib/Driver/Action.cpp b/clang/lib/Driver/Action.cpp
index 72a42a6f957ee..01045a97930f8 100644
--- a/clang/lib/Driver/Action.cpp
+++ b/clang/lib/Driver/Action.cpp
@@ -472,5 +472,5 @@ BinaryTranslatorJobAction::BinaryTranslatorJobAction(Action 
*Input,
 
 void ObjcopyJobAction::anchor() {}
 
-ObjcopyJobAction::ObjcopyJobAction(Action *Input, types::ID Type)
-    : JobAction(ObjcopyJobClass, Input, Type) {}
+ObjcopyJobAction::ObjcopyJobAction(ActionList &Inputs, types::ID Type)
+    : JobAction(ObjcopyJobClass, Inputs, Type) {}
diff --git a/clang/lib/Driver/Driver.cpp b/clang/lib/Driver/Driver.cpp
index c2a2cd80b94b2..6ca9639276341 100644
--- a/clang/lib/Driver/Driver.cpp
+++ b/clang/lib/Driver/Driver.cpp
@@ -618,10 +618,9 @@ static void setZosTargetVersion(const Driver &D, 
llvm::Triple &Target,
 ///
 /// This routine provides the logic to compute a target triple from various
 /// args passed to the driver and the default triple string.
-static llvm::Triple computeTargetTriple(const Driver &D,
-                                        StringRef TargetTriple,
+static llvm::Triple computeTargetTriple(const Driver &D, StringRef 
TargetTriple,
                                         const ArgList &Args,
-                                        StringRef DarwinArchName = "") {
+                                        StringRef ArchName = "") {
   // FIXME: Already done in Compilation *Driver::BuildCompilation
   if (const Arg *A = Args.getLastArg(options::OPT_target))
     TargetTriple = A->getValue();
@@ -637,9 +636,8 @@ static llvm::Triple computeTargetTriple(const Driver &D,
   // Handle Apple-specific options available here.
   if (Target.isOSBinFormatMachO()) {
     // If an explicit Darwin arch name is given, that trumps all.
-    if (!DarwinArchName.empty()) {
-      tools::darwin::setTripleTypeForMachOArchName(Target, DarwinArchName,
-                                                   Args);
+    if (!ArchName.empty()) {
+      tools::darwin::setTripleTypeForMachOArchName(Target, ArchName, Args);
       return Target;
     }
 
@@ -648,6 +646,9 @@ static llvm::Triple computeTargetTriple(const Driver &D,
       StringRef ArchName = A->getValue();
       tools::darwin::setTripleTypeForMachOArchName(Target, ArchName, Args);
     }
+  } else if (!ArchName.empty()) {
+    Target.setArchName(ArchName);
+    return Target;
   }
 
   // Handle pseudo-target flags '-mlittle-endian'/'-EL' and
@@ -700,6 +701,11 @@ static llvm::Triple computeTargetTriple(const Driver &D,
     D.Diag(diag::err_drv_unsupported_opt_for_target)
         << A->getAsString(Args) << Target.str();
 
+  // The `-marm64x` flag is only valid for Windows targets.
+  if (Args.hasArgNoClaim(options::OPT_marm64x) && !Target.isOSWindows())
+    D.Diag(diag::err_drv_unsupported_opt_for_target)
+        << "-marm64x" << Target.str();
+
   // Handle pseudo-target flags '-m64', '-mx32', '-m32' and '-m16'.
   Arg *A = Args.getLastArg(options::OPT_m64, options::OPT_mx32,
                            options::OPT_m32, options::OPT_m16,
@@ -4808,9 +4814,11 @@ void Driver::BuildActions(Compilation &C, DerivedArgList 
&Args,
     if (TC.requiresObjcopy(Args)) {
       Action *LastAction = Actions.back();
       // llvm-objcopy expects an unvalidated DXIL container (TY_OBJECT).
-      if (LastAction->getType() == types::TY_Object)
+      if (LastAction->getType() == types::TY_Object) {
+        ActionList ObjcopyActions({LastAction});
         Actions.push_back(
-            C.MakeAction<ObjcopyJobAction>(LastAction, types::TY_Object));
+            C.MakeAction<ObjcopyJobAction>(ObjcopyActions, types::TY_Object));
+      }
     }
 
     // Call validator when -Vd not in Args.
@@ -5471,6 +5479,16 @@ Action *Driver::ConstructPhaseAction(
     return C.MakeAction<BackendJobAction>(Input, types::TY_PP_Asm);
   }
   case phases::Assemble:
+    // When -marm64x is used, construct jobs for the EC and native targets and
+    // merge them into an archive with llvm-objcopy.
+    if (Args.hasArg(options::OPT_marm64x)) {
+      Action *Act =
+          C.MakeAction<AssembleJobAction>(std::move(Input), types::TY_Object);
+      ActionList Inputs;
+      Inputs.push_back(C.MakeAction<BindArchAction>(Act, "arm64ec"));
+      Inputs.push_back(C.MakeAction<BindArchAction>(Act, "aarch64"));
+      return C.MakeAction<ObjcopyJobAction>(Inputs, types::TY_Object);
+    }
     return C.MakeAction<AssembleJobAction>(std::move(Input), types::TY_Object);
   }
 
@@ -5565,7 +5583,8 @@ void Driver::BuildJobs(Compilation &C) const {
     BuildJobsForAction(C, A, &C.getDefaultToolChain(),
                        /*BoundArch*/ StringRef(),
                        /*AtTopLevel*/ true,
-                       /*MultipleArchs*/ ArchNames.size() > 1,
+                       /*MultipleArchs*/ ArchNames.size() > 1 ||
+                           C.getArgs().hasArgNoClaim(options::OPT_marm64x),
                        /*LinkingOutput*/ LinkingOutput, CachedResults,
                        /*TargetDeviceOffloadKind*/ Action::OFK_None);
   }
diff --git a/clang/lib/Driver/ToolChain.cpp b/clang/lib/Driver/ToolChain.cpp
index ccfc022f79427..eb1878594a779 100644
--- a/clang/lib/Driver/ToolChain.cpp
+++ b/clang/lib/Driver/ToolChain.cpp
@@ -776,9 +776,11 @@ Tool *ToolChain::getTool(Action::ActionClass AC) const {
   case Action::VerifyDebugInfoJobClass:
   case Action::BinaryAnalyzeJobClass:
   case Action::BinaryTranslatorJobClass:
-  case Action::ObjcopyJobClass:
     llvm_unreachable("Invalid tool kind.");
 
+  case Action::ObjcopyJobClass:
+    return nullptr;
+
   case Action::CompileJobClass:
   case Action::PrecompileJobClass:
   case Action::PreprocessJobClass:
diff --git a/clang/lib/Driver/ToolChains/MSVC.cpp 
b/clang/lib/Driver/ToolChains/MSVC.cpp
index 8141f9f132421..ba8fdb9060b5c 100644
--- a/clang/lib/Driver/ToolChains/MSVC.cpp
+++ b/clang/lib/Driver/ToolChains/MSVC.cpp
@@ -525,6 +525,17 @@ MSVCToolChain::MSVCToolChain(const Driver &D, const 
llvm::Triple &Triple,
       llvm::findVCToolChainViaRegistry(VCToolChainPath, VSLayout);
 }
 
+Tool *MSVCToolChain::getTool(Action::ActionClass AC) const {
+  switch (AC) {
+  case Action::ObjcopyJobClass:
+    if (!LLVMObjcopy)
+      LLVMObjcopy.reset(new tools::MinGW::LLVMObjcopy(*this));
+    return LLVMObjcopy.get();
+  default:
+    return ToolChain::getTool(AC);
+  }
+}
+
 Tool *MSVCToolChain::buildLinker() const {
   return new tools::visualstudio::Linker(*this);
 }
diff --git a/clang/lib/Driver/ToolChains/MSVC.h 
b/clang/lib/Driver/ToolChains/MSVC.h
index dcf4c0b488171..f231dd5824a6e 100644
--- a/clang/lib/Driver/ToolChains/MSVC.h
+++ b/clang/lib/Driver/ToolChains/MSVC.h
@@ -9,6 +9,7 @@
 #ifndef LLVM_CLANG_LIB_DRIVER_TOOLCHAINS_MSVC_H
 #define LLVM_CLANG_LIB_DRIVER_TOOLCHAINS_MSVC_H
 
+#include "MinGW.h"
 #include "clang/Driver/Compilation.h"
 #include "clang/Driver/CudaInstallationDetector.h"
 #include "clang/Driver/LazyDetector.h"
@@ -137,8 +138,10 @@ class LLVM_LIBRARY_VISIBILITY MSVCToolChain : public 
ToolChain {
                                      const Twine &subfolder2 = "",
                                      const Twine &subfolder3 = "") const;
 
+  Tool *getTool(Action::ActionClass AC) const override;
   Tool *buildLinker() const override;
   Tool *buildAssembler() const override;
+
 private:
   std::optional<llvm::StringRef> WinSdkDir, WinSdkVersion, WinSysRoot;
   std::string VCToolChainPath;
@@ -146,6 +149,7 @@ class LLVM_LIBRARY_VISIBILITY MSVCToolChain : public 
ToolChain {
   LazyDetector<CudaInstallationDetector> CudaInstallation;
   LazyDetector<RocmInstallationDetector> RocmInstallation;
   LazyDetector<SYCLInstallationDetector> SYCLInstallation;
+  mutable std::unique_ptr<tools::MinGW::LLVMObjcopy> LLVMObjcopy;
 };
 
 } // end namespace toolchains
diff --git a/clang/lib/Driver/ToolChains/MinGW.cpp 
b/clang/lib/Driver/ToolChains/MinGW.cpp
index e3f8cb8292fc8..f91b292f1037c 100644
--- a/clang/lib/Driver/ToolChains/MinGW.cpp
+++ b/clang/lib/Driver/ToolChains/MinGW.cpp
@@ -387,6 +387,31 @@ void tools::MinGW::Linker::ConstructJob(Compilation &C, 
const JobAction &JA,
                                          Exec, CmdArgs, Inputs, Output));
 }
 
+void tools::MinGW::LLVMObjcopy::ConstructJob(Compilation &C,
+                                             const JobAction &JA,
+                                             const InputInfo &Output,
+                                             const InputInfoList &Inputs,
+                                             const ArgList &Args,
+                                             const char *LinkingOutput) const {
+
+  std::string ObjcopyPath = getToolChain().GetProgramPath("llvm-objcopy");
+  const char *Exec = Args.MakeArgString(ObjcopyPath);
+
+  // Assume llvm-objcopy is only used for hybrid ARM64X object files.
+  assert(Inputs.size() == 2 && "Expected 2 inputs.");
+  // Embed the hybrid object in the .llvm.arm64x section.
+  ArgStringList CmdArgs;
+  CmdArgs.push_back(Args.MakeArgString("--add-section=.llvm.arm64x=" +
+                                       Twine(Inputs[0].getFilename())));
+  // Mark the .llvm.arm64x section as discardable.
+  CmdArgs.push_back("--set-section-flags=.llvm.arm64x=debug");
+  CmdArgs.push_back(Inputs[1].getFilename());
+  CmdArgs.push_back(Output.getFilename());
+
+  C.addCommand(std::make_unique<Command>(JA, *this, 
ResponseFileSupport::None(),
+                                         Exec, CmdArgs, Inputs, Output));
+}
+
 static bool isCrossCompiling(const llvm::Triple &T, bool RequireArchMatch) {
   llvm::Triple HostTriple(llvm::Triple::normalize(LLVM_HOST_TRIPLE));
   if (HostTriple.getOS() != llvm::Triple::Win32)
@@ -575,6 +600,10 @@ Tool *toolchains::MinGW::getTool(Action::ActionClass AC) 
const {
     if (!Compiler)
       Compiler.reset(new tools::gcc::Compiler(*this));
     return Compiler.get();
+  case Action::ObjcopyJobClass:
+    if (!LLVMObjcopy)
+      LLVMObjcopy.reset(new tools::MinGW::LLVMObjcopy(*this));
+    return LLVMObjcopy.get();
   default:
     return ToolChain::getTool(AC);
   }
diff --git a/clang/lib/Driver/ToolChains/MinGW.h 
b/clang/lib/Driver/ToolChains/MinGW.h
index ddf9dc500960b..8c54609afddaa 100644
--- a/clang/lib/Driver/ToolChains/MinGW.h
+++ b/clang/lib/Driver/ToolChains/MinGW.h
@@ -52,6 +52,19 @@ class LLVM_LIBRARY_VISIBILITY Linker final : public Tool {
   void AddLibGCC(const llvm::opt::ArgList &Args,
                  llvm::opt::ArgStringList &CmdArgs) const;
 };
+
+class LLVM_LIBRARY_VISIBILITY LLVMObjcopy : public Tool {
+public:
+  LLVMObjcopy(const ToolChain &TC)
+      : Tool("MinGW::LLVMObjcopy", "llvm-objcopy", TC) {}
+
+  bool hasIntegratedCPP() const override { return false; }
+
+  void ConstructJob(Compilation &C, const JobAction &JA,
+                    const InputInfo &Output, const InputInfoList &Inputs,
+                    const llvm::opt::ArgList &TCArgs,
+                    const char *LinkingOutput) const override;
+};
 } // end namespace MinGW
 } // end namespace tools
 
@@ -117,6 +130,7 @@ class LLVM_LIBRARY_VISIBILITY MinGW : public ToolChain {
   std::string TripleDirName;
   mutable std::unique_ptr<tools::gcc::Preprocessor> Preprocessor;
   mutable std::unique_ptr<tools::gcc::Compiler> Compiler;
+  mutable std::unique_ptr<tools::MinGW::LLVMObjcopy> LLVMObjcopy;
   void findGccLibDir(const llvm::Triple &LiteralTriple);
 
   bool NativeLLVMSupport;
diff --git a/clang/test/Driver/arm64x.c b/clang/test/Driver/arm64x.c
new file mode 100644
index 0000000000000..fc55703208187
--- /dev/null
+++ b/clang/test/Driver/arm64x.c
@@ -0,0 +1,6 @@
+// RUN: %clang -c -marm64x  --target=arm64ec-pc-windows-msvc -### %s 2>&1 | 
FileCheck %s
+// RUN: %clang -c -marm64x  --target=arm64ec-pc-windows-gnu -### %s 2>&1 | 
FileCheck %s
+
+// CHECK:      "-cc1" "-triple" "arm64ec-pc-windows-{{.*}}" "-emit-obj"
+// CHECK-NEXT: "-cc1" "-triple" "aarch64-pc-windows-{{.*}}" "-emit-obj"
+// CHECK-NEXT: llvm-objcopy" 
"--add-section=.llvm.arm64x={{.*}}arm64x-arm64ec-{{.*}}.o" 
"--set-section-flags=.llvm.arm64x=debug" "{{.*}}arm64x-aarch64-{{.*}}.o" 
"arm64x.o"
diff --git a/clang/test/Driver/msvc-link.c b/clang/test/Driver/msvc-link.c
index 5cb1653bf4db9..19027ff3e80ff 100644
--- a/clang/test/Driver/msvc-link.c
+++ b/clang/test/Driver/msvc-link.c
@@ -46,9 +46,9 @@
 // ARM64X: "-machine:arm64x"
 
 // RUN: not %clang --target=x86_64-linux-gnu -marm64x -### %s 2>&1 | FileCheck 
--check-prefix=HYBRID-ERR %s
-// HYBRID-ERR: error: unsupported option '-marm64x' for target 
'x86_64-linux-gnu'
+// HYBRID-ERR: error: unsupported option '-marm64x' for target 
'x86_64-unknown-linux-gnu'
 
-// RUN: %clang -c -marm64x  --target=arm64ec-pc-windows-msvc -fuse-ld=link 
-### %s 2>&1 | \
+// RUN: %clang -S -marm64x  --target=arm64ec-pc-windows-msvc -fuse-ld=link 
-### %s 2>&1 | \
 // RUN:        FileCheck --check-prefix=HYBRID-WARN %s
 // HYBRID-WARN: warning: argument unused during compilation: '-marm64x' 
[-Wunused-command-line-argument]
 

_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to