Skip to content

[LLD][COFF] Add support for hybrid exports on ARM64X #123724

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 21, 2025

Conversation

cjacek
Copy link
Contributor

@cjacek cjacek commented Jan 21, 2025

No description provided.

@cjacek
Copy link
Contributor Author

cjacek commented Jan 21, 2025

Depends on #123723.

@llvmbot
Copy link
Member

llvmbot commented Jan 21, 2025

@llvm/pr-subscribers-lld-coff

@llvm/pr-subscribers-platform-windows

Author: Jacek Caban (cjacek)

Changes

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

6 Files Affected:

  • (modified) lld/COFF/Chunks.cpp (+11-1)
  • (modified) lld/COFF/Chunks.h (+6-1)
  • (modified) lld/COFF/SymbolTable.h (+3)
  • (modified) lld/COFF/Writer.cpp (+83-21)
  • (modified) lld/test/COFF/arm64x-export.test (+108)
  • (modified) lld/test/COFF/pdata-arm64ec.test (+16-5)
diff --git a/lld/COFF/Chunks.cpp b/lld/COFF/Chunks.cpp
index ff3c89884c24df..2ef74cb4ce6259 100644
--- a/lld/COFF/Chunks.cpp
+++ b/lld/COFF/Chunks.cpp
@@ -1167,7 +1167,7 @@ uint32_t ImportThunkChunkARM64EC::extendRanges() {
 }
 
 uint64_t Arm64XRelocVal::get() const {
-  return (sym ? sym->getRVA() : 0) + value;
+  return (sym ? sym->getRVA() : 0) + (chunk ? chunk->getRVA() : 0) + value;
 }
 
 size_t Arm64XDynamicRelocEntry::getSize() const {
@@ -1230,6 +1230,16 @@ void DynamicRelocsChunk::finalize() {
   size = alignTo(size, sizeof(uint32_t));
 }
 
+// Set the reloc value. The reloc entry must be allocated beforehand.
+void DynamicRelocsChunk::set(uint32_t rva, Arm64XRelocVal value) {
+  Arm64XDynamicRelocEntry &entry =
+      *llvm::find_if(arm64xRelocs, [rva](const Arm64XDynamicRelocEntry &e) {
+        return e.offset.get() == rva;
+      });
+  assert(!entry.value.get());
+  entry.value = value;
+}
+
 void DynamicRelocsChunk::writeTo(uint8_t *buf) const {
   auto table = reinterpret_cast<coff_dynamic_reloc_table *>(buf);
   table->Version = 1;
diff --git a/lld/COFF/Chunks.h b/lld/COFF/Chunks.h
index 7ba58e336451fc..d6216efdd90bdd 100644
--- a/lld/COFF/Chunks.h
+++ b/lld/COFF/Chunks.h
@@ -840,10 +840,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)
+      : chunk(chunk), value(offset) {}
   uint64_t get() const;
 
 private:
   Defined *sym = nullptr;
+  Chunk *chunk = nullptr;
   uint64_t value;
 };
 
@@ -874,10 +877,12 @@ class DynamicRelocsChunk : public NonSectionChunk {
   void finalize();
 
   void add(llvm::COFF::Arm64XFixupType type, uint8_t size,
-           Arm64XRelocVal offset, Arm64XRelocVal value) {
+           Arm64XRelocVal offset, Arm64XRelocVal value = Arm64XRelocVal()) {
     arm64xRelocs.emplace_back(type, size, offset, value);
   }
 
+  void set(uint32_t rva, Arm64XRelocVal value);
+
 private:
   std::vector<Arm64XDynamicRelocEntry> arm64xRelocs;
   size_t size;
diff --git a/lld/COFF/SymbolTable.h b/lld/COFF/SymbolTable.h
index a0acf5db469032..e5b02ce5904c49 100644
--- a/lld/COFF/SymbolTable.h
+++ b/lld/COFF/SymbolTable.h
@@ -155,6 +155,9 @@ class SymbolTable {
   llvm::DenseSet<StringRef> directivesExports;
   bool hadExplicitExports;
 
+  Chunk *edataStart = nullptr;
+  Chunk *edataEnd = nullptr;
+
   void fixupExports();
   void assignExportOrdinals();
 
diff --git a/lld/COFF/Writer.cpp b/lld/COFF/Writer.cpp
index 3d95d219a493cd..bef2ced9f2957d 100644
--- a/lld/COFF/Writer.cpp
+++ b/lld/COFF/Writer.cpp
@@ -288,8 +288,6 @@ class Writer {
   IdataContents idata;
   Chunk *importTableStart = nullptr;
   uint64_t importTableSize = 0;
-  Chunk *edataStart = nullptr;
-  Chunk *edataEnd = nullptr;
   Chunk *iatStart = nullptr;
   uint64_t iatSize = 0;
   DelayLoadContents delayIdata;
@@ -1331,22 +1329,46 @@ void Writer::createExportTable() {
   if (!edataSec->chunks.empty()) {
     // Allow using a custom built export table from input object files, instead
     // of having the linker synthesize the tables.
-    if (ctx.symtab.hadExplicitExports)
-      Warn(ctx) << "literal .edata sections override exports";
-  } else if (!ctx.symtab.exports.empty()) {
-    std::vector<Chunk *> edataChunks;
-    createEdataChunks(ctx.symtab, edataChunks);
-    for (Chunk *c : edataChunks)
-      edataSec->addChunk(c);
-  }
-  if (!edataSec->chunks.empty()) {
-    edataStart = edataSec->chunks.front();
-    edataEnd = edataSec->chunks.back();
+    if (!ctx.hybridSymtab) {
+      ctx.symtab.edataStart = edataSec->chunks.front();
+      ctx.symtab.edataEnd = edataSec->chunks.back();
+    } else {
+      // On hybrid target, split EC and native chunks.
+      llvm::stable_sort(edataSec->chunks, [=](const Chunk *a, const Chunk *b) {
+        return (a->getMachine() != ARM64) < (b->getMachine() != ARM64);
+      });
+
+      for (auto chunk : edataSec->chunks) {
+        if (chunk->getMachine() != ARM64) {
+          ctx.hybridSymtab->edataStart = chunk;
+          ctx.hybridSymtab->edataEnd = edataSec->chunks.back();
+          break;
+        }
+
+        if (!ctx.symtab.edataStart)
+          ctx.symtab.edataStart = chunk;
+        ctx.symtab.edataEnd = chunk;
+      }
+    }
   }
-  // Warn on exported deleting destructor.
-  for (auto e : ctx.symtab.exports)
-    if (e.sym && e.sym->getName().starts_with("??_G"))
-      Warn(ctx) << "export of deleting dtor: " << e.sym;
+  ctx.forEachSymtab([&](SymbolTable &symtab) {
+    if (symtab.edataStart) {
+      if (symtab.hadExplicitExports)
+        Warn(ctx) << "literal .edata sections override exports";
+    } else if (!symtab.exports.empty()) {
+      std::vector<Chunk *> edataChunks;
+      createEdataChunks(symtab, edataChunks);
+      for (Chunk *c : edataChunks)
+        edataSec->addChunk(c);
+      symtab.edataStart = edataChunks.front();
+      symtab.edataEnd = edataChunks.back();
+    }
+
+    // Warn on exported deleting destructor.
+    for (auto e : symtab.exports)
+      if (e.sym && e.sym->getName().starts_with("??_G"))
+        Warn(ctx) << "export of deleting dtor: " << toString(ctx, *e.sym);
+  });
 }
 
 void Writer::removeUnusedSections() {
@@ -1819,10 +1841,11 @@ template <typename PEHeaderTy> void Writer::writeHeader() {
          dataDirOffset64 == buf - buffer->getBufferStart());
   auto *dir = reinterpret_cast<data_directory *>(buf);
   buf += sizeof(*dir) * numberOfDataDirectory;
-  if (edataStart) {
-    dir[EXPORT_TABLE].RelativeVirtualAddress = edataStart->getRVA();
-    dir[EXPORT_TABLE].Size =
-        edataEnd->getRVA() + edataEnd->getSize() - edataStart->getRVA();
+  if (ctx.symtab.edataStart) {
+    dir[EXPORT_TABLE].RelativeVirtualAddress = ctx.symtab.edataStart->getRVA();
+    dir[EXPORT_TABLE].Size = ctx.symtab.edataEnd->getRVA() +
+                             ctx.symtab.edataEnd->getSize() -
+                             ctx.symtab.edataStart->getRVA();
   }
   if (importTableStart) {
     dir[IMPORT_TABLE].RelativeVirtualAddress = importTableStart->getRVA();
@@ -2392,6 +2415,19 @@ void Writer::setECSymbols() {
       symtab->findUnderscore("__arm64x_native_entrypoint")
           ->replaceKeepingName(altEntrySym, sizeof(SymbolUnion));
     }
+
+    if (symtab->edataStart)
+      ctx.dynamicRelocs->set(
+          dataDirOffset64 + EXPORT_TABLE * sizeof(data_directory) +
+              offsetof(data_directory, Size),
+          symtab->edataEnd->getRVA() - symtab->edataStart->getRVA() +
+              symtab->edataEnd->getSize());
+    if (hybridPdata.first)
+      ctx.dynamicRelocs->set(
+          dataDirOffset64 + EXCEPTION_TABLE * sizeof(data_directory) +
+              offsetof(data_directory, Size),
+          hybridPdata.last->getRVA() - hybridPdata.first->getRVA() +
+              hybridPdata.last->getSize());
   }
 }
 
@@ -2644,6 +2680,32 @@ void Writer::createDynamicRelocs() {
       Warn(ctx) << "'__chpe_metadata' is missing for ARM64X target";
   }
 
+  if (ctx.symtab.edataStart != ctx.hybridSymtab->edataStart) {
+    ctx.dynamicRelocs->add(IMAGE_DVRT_ARM64X_FIXUP_TYPE_VALUE, sizeof(uint32_t),
+                           dataDirOffset64 +
+                               EXPORT_TABLE * sizeof(data_directory) +
+                               offsetof(data_directory, RelativeVirtualAddress),
+                           ctx.hybridSymtab->edataStart);
+    // The Size value is assigned after addresses are finalized.
+    ctx.dynamicRelocs->add(IMAGE_DVRT_ARM64X_FIXUP_TYPE_VALUE, sizeof(uint32_t),
+                           dataDirOffset64 +
+                               EXPORT_TABLE * sizeof(data_directory) +
+                               offsetof(data_directory, Size));
+  }
+
+  if (pdata.first != hybridPdata.first) {
+    ctx.dynamicRelocs->add(IMAGE_DVRT_ARM64X_FIXUP_TYPE_VALUE, sizeof(uint32_t),
+                           dataDirOffset64 +
+                               EXCEPTION_TABLE * sizeof(data_directory) +
+                               offsetof(data_directory, RelativeVirtualAddress),
+                           hybridPdata.first);
+    // The Size value is assigned after addresses are finalized.
+    ctx.dynamicRelocs->add(IMAGE_DVRT_ARM64X_FIXUP_TYPE_VALUE, sizeof(uint32_t),
+                           dataDirOffset64 +
+                               EXCEPTION_TABLE * sizeof(data_directory) +
+                               offsetof(data_directory, Size));
+  }
+
   // Set the hybrid load config to the EC load config.
   ctx.dynamicRelocs->add(IMAGE_DVRT_ARM64X_FIXUP_TYPE_VALUE, sizeof(uint32_t),
                          dataDirOffset64 +
diff --git a/lld/test/COFF/arm64x-export.test b/lld/test/COFF/arm64x-export.test
index e5d0307e570efd..526be633973581 100644
--- a/lld/test/COFF/arm64x-export.test
+++ b/lld/test/COFF/arm64x-export.test
@@ -5,6 +5,8 @@ RUN: llvm-mc -filetype=obj -triple=arm64ec-windows arm64ec-func.s -o arm64ec-fun
 RUN: llvm-mc -filetype=obj -triple=aarch64-windows arm64-func.s -o arm64-func.obj
 RUN: llvm-mc -filetype=obj -triple=arm64ec-windows func-drectve.s -o arm64ec-drectve.obj
 RUN: llvm-mc -filetype=obj -triple=aarch64-windows func-drectve.s -o arm64-drectve.obj
+RUN: llvm-mc -filetype=obj -triple=aarch64-windows edata.s -o arm64-edata.obj
+RUN: llvm-mc -filetype=obj -triple=arm64ec-windows edata.s -o arm64ec-edata.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
 
@@ -36,6 +38,15 @@ RUN: llvm-readobj --headers --coff-exports out-cmd.dll | FileCheck --check-prefi
 EXPORTS-EC:      ExportTableRVA: 0x0
 EXPORTS-EC-NEXT: ExportTableSize: 0x0
 EXPORTS-EC-NOT:  Name: func
+EXPORTS-EC:      HybridObject {
+EXPORTS-EC:        ExportTableRVA: 0x3{{.*}}
+EXPORTS-EC-NEXT:   ExportTableSize: 0x4{{.*}}
+EXPORTS-EC:        Export {
+EXPORTS-EC-NEXT:     Ordinal: 1
+EXPORTS-EC-NEXT:     Name: func
+EXPORTS-EC-NEXT:     RVA: 0x2000
+EXPORTS-EC-NEXT:   }
+EXPORTS-EC-NEXT: }
 
 # Export using the EC .drectve section.
 
@@ -44,6 +55,30 @@ RUN:          loadconfig-arm64.obj loadconfig-arm64ec.obj arm64ec-drectve.obj -n
 RUN: llvm-objdump -d out-drectve-ec.dll | FileCheck --check-prefix=DISASM-EC %s
 RUN: llvm-readobj --headers --coff-exports out-drectve-ec.dll | FileCheck --check-prefix=EXPORTS-EC %s
 
+# Export using the EC .edata section.
+
+RUN: lld-link -machine:arm64x -dll -out:out-edata-ec.dll arm64ec-func.obj arm64-func.obj \
+RUN:          loadconfig-arm64.obj loadconfig-arm64ec.obj arm64ec-edata.obj -noentry
+
+RUN: llvm-objdump -d out-edata-ec.dll | FileCheck --check-prefix=DISASM-EDATA-EC %s
+DISASM-EDATA-EC:      0000000180001000 <.text>:
+DISASM-EDATA-EC-NEXT: 180001000: 52800040     mov     w0, #0x2                // =2
+DISASM-EDATA-EC-NEXT: 180001004: d65f03c0     ret
+
+RUN: llvm-readobj --headers --coff-exports out-edata-ec.dll | FileCheck --check-prefix=EXPORTS-EDATA-EC %s
+EXPORTS-EDATA-EC:      ExportTableRVA: 0x0
+EXPORTS-EDATA-EC-NEXT: ExportTableSize: 0x0
+EXPORTS-EDATA-EC-NOT:  Name: func
+EXPORTS-EDATA-EC:      HybridObject {
+EXPORTS-EDATA-EC:        ExportTableRVA: 0x2{{.*}}
+EXPORTS-EDATA-EC-NEXT:   ExportTableSize: 0x4{{.*}}
+EXPORTS-EDATA-EC:        Export {
+EXPORTS-EDATA-EC-NEXT:     Ordinal: 1
+EXPORTS-EDATA-EC-NEXT:     Name: func
+EXPORTS-EDATA-EC-NEXT:     RVA: 0x1000
+EXPORTS-EDATA-EC-NEXT:   }
+EXPORTS-EDATA-EC-NEXT: }
+
 # Export using the native .drectve section.
 
 RUN: lld-link -machine:arm64x -dll -out:out-drectve-native.dll arm64ec-func.obj arm64-func.obj \
@@ -64,6 +99,17 @@ EXPORTS-NATIVE-NEXT:   Ordinal: 1
 EXPORTS-NATIVE-NEXT:   Name: func
 EXPORTS-NATIVE-NEXT:   RVA: 0x1000
 EXPORTS-NATIVE-NEXT: }
+EXPORTS-NATIVE:      HybridObject {
+EXPORTS-NATIVE:        ExportTableRVA: 0x0
+EXPORTS-NATIVE-NEXT:   ExportTableSize: 0x0
+EXPORTS-NATIVE-NOT:    Name: func
+
+# Export using the native .edata section.
+
+RUN: lld-link -machine:arm64x -dll -out:out-edata.dll arm64ec-func.obj arm64-func.obj \
+RUN:          loadconfig-arm64.obj loadconfig-arm64ec.obj arm64-edata.obj -noentry
+RUN: llvm-objdump -d out-edata.dll | FileCheck --check-prefix=DISASM-NATIVE %s
+RUN: llvm-readobj --headers --coff-exports out-edata.dll | FileCheck --check-prefix=EXPORTS-NATIVE %s
 
 # Export using both the native and EC .drectve sections.
 
@@ -99,6 +145,37 @@ EXPORTS-BOTH-NEXT:   Ordinal: 1
 EXPORTS-BOTH-NEXT:   Name: func
 EXPORTS-BOTH-NEXT:   RVA: 0x1000
 EXPORTS-BOTH-NEXT: }
+EXPORTS-BOTH:      HybridObject {
+EXPORTS-BOTH:        ExportTableRVA: 0x4{{.*}}
+EXPORTS-BOTH-NEXT:   ExportTableSize: 0x4{{.*}}
+EXPORTS-BOTH:        Export {
+EXPORTS-BOTH-NEXT:     Ordinal: 1
+EXPORTS-BOTH-NEXT:     Name: func
+EXPORTS-BOTH-NEXT:     RVA: 0x3000
+EXPORTS-BOTH-NEXT:   }
+EXPORTS-BOTH-NEXT: }
+
+# Export using both the native and EC .edata sections.
+
+RUN: lld-link -machine:arm64x -dll -out:out-edata-both.dll arm64ec-func.obj arm64-func.obj \
+RUN:          loadconfig-arm64.obj loadconfig-arm64ec.obj arm64-edata.obj arm64ec-edata.obj -noentry
+RUN: llvm-readobj --headers --coff-exports out-edata-both.dll | FileCheck --check-prefix=EXPORTS-EDATA-BOTH %s
+EXPORTS-EDATA-BOTH:      ExportTableRVA: 0x3{{.*}}
+EXPORTS-EDATA-BOTH-NEXT: ExportTableSize: 0x4{{.*}}
+EXPORTS-EDATA-BOTH:      Export {
+EXPORTS-EDATA-BOTH-NEXT:   Ordinal: 1
+EXPORTS-EDATA-BOTH-NEXT:   Name: func
+EXPORTS-EDATA-BOTH-NEXT:   RVA: 0x1000
+EXPORTS-EDATA-BOTH-NEXT: }
+EXPORTS-EDATA-BOTH:      HybridObject {
+EXPORTS-EDATA-BOTH:        ExportTableRVA: 0x3{{.*}}
+EXPORTS-EDATA-BOTH-NEXT:   ExportTableSize: 0x4{{.*}}
+EXPORTS-EDATA-BOTH:        Export {
+EXPORTS-EDATA-BOTH-NEXT:     Ordinal: 1
+EXPORTS-EDATA-BOTH-NEXT:     Name: func
+EXPORTS-EDATA-BOTH-NEXT:     RVA: 0x2000
+EXPORTS-EDATA-BOTH-NEXT:   }
+EXPORTS-EDATA-BOTH-NEXT: }
 
 #--- arm64-func.s
     .section .text,"xr",discard,func
@@ -119,3 +196,34 @@ func:
 #--- func-drectve.s
 .section .drectve
     .ascii "-export:func"
+
+#--- edata.s
+    .section .edata, "dr"
+    .align 4
+exports:
+    .long 0           // ExportFlags
+    .long 0           // TimeDateStamp
+    .long 0           // MajorVersion + MinorVersion
+    .rva name         // NameRVA
+    .long 1           // OrdinalBase
+    .long 1           // AddressTableEntries
+    .long 1           // NumberOfNamePointers
+    .rva functions    // ExportAddressTableRVA
+    .rva names        // NamePointerRVA
+    .rva nameordinals // OrdinalTableRVA
+
+names:
+    .rva funcname_func
+
+nameordinals:
+    .short 0
+
+functions:
+    .rva func
+    .long 0
+
+funcname_func:
+    .asciz "func"
+
+name:
+    .asciz "out-edata.dll"
diff --git a/lld/test/COFF/pdata-arm64ec.test b/lld/test/COFF/pdata-arm64ec.test
index 7f20c460dc1099..fbec797525f7f8 100644
--- a/lld/test/COFF/pdata-arm64ec.test
+++ b/lld/test/COFF/pdata-arm64ec.test
@@ -6,6 +6,7 @@ Test handlign of hybrid .pdata section on ARM64EC target.
 RUN: llvm-mc -filetype=obj -triple=arm64-windows arm64-func-sym.s -o arm64-func-sym.obj
 RUN: llvm-mc -filetype=obj -triple=arm64ec-windows arm64ec-func-sym.s -o arm64ec-func-sym.obj
 RUN: llvm-mc -filetype=obj -triple=x86_64-windows x86_64-func-sym.s -o x86_64-func-sym.obj
+RUN: llvm-mc -filetype=obj -triple=aarch64-windows %S/Inputs/loadconfig-arm64.s -o loadconfig-arm64.obj
 RUN: llvm-mc -filetype=obj -triple=arm64ec-windows %p/Inputs/loadconfig-arm64ec.s -o loadconfig-arm64ec.obj
 
 Only arm64ec code:
@@ -55,11 +56,21 @@ DATA3: 180005000 00100000 11000001 00200000 0e200000
 Mixed arm64x code:
 
 RUN: lld-link -out:test4.dll -machine:arm64x arm64-func-sym.obj arm64ec-func-sym.obj \
-RUN:          x86_64-func-sym.obj loadconfig-arm64ec.obj -dll -noentry
+RUN:          x86_64-func-sym.obj loadconfig-arm64.obj loadconfig-arm64ec.obj -dll -noentry
 
 RUN: llvm-readobj --headers test4.dll | FileCheck -check-prefix=DIR3 %s
-DIR3:      ExceptionTableRVA: 0x6000
-DIR3-NEXT: ExceptionTableSize: 0x10
+DIR3:      ImageOptionalHeader {
+DIR3:        DataDirectory {
+DIR3:          ExceptionTableRVA: 0x6000
+DIR3-NEXT:     ExceptionTableSize: 0x10
+DIR3:        }
+DIR3:      }
+DIR3:      HybridObject {
+DIR3:        ImageOptionalHeader {
+DIR3:          ExceptionTableRVA: 0x6010
+DIR3-NEXT:     ExceptionTableSize: 0xC
+DIR3:        }
+DIR3:      }
 
 RUN: llvm-objdump -s --section=.pdata test4.dll | FileCheck -check-prefix=DATA4 %s
 DATA4: 180006000 00100000 11000001 00200000 11000001  ......... ......
@@ -74,12 +85,12 @@ RUN: llvm-readobj --headers test5.dll | FileCheck -check-prefix=DIR2 %s
 RUN: llvm-objdump -s --section=.pdata test5.dll | FileCheck -check-prefix=DATA3 %s
 
 RUN: lld-link -out:test6.dll -machine:arm64x arm64ec-func-sym.obj x86_64-func-sym.obj \
-RUN:          arm64-func-sym.obj loadconfig-arm64ec.obj -dll -noentry
+RUN:          arm64-func-sym.obj loadconfig-arm64.obj loadconfig-arm64ec.obj -dll -noentry
 RUN: llvm-readobj --headers test6.dll | FileCheck -check-prefix=DIR3 %s
 RUN: llvm-objdump -s --section=.pdata test6.dll | FileCheck -check-prefix=DATA4 %s
 
 RUN: lld-link -out:test7.dll -machine:arm64x x86_64-func-sym.obj arm64ec-func-sym.obj \
-RUN:          arm64-func-sym.obj loadconfig-arm64ec.obj -dll -noentry
+RUN:          arm64-func-sym.obj loadconfig-arm64.obj loadconfig-arm64ec.obj -dll -noentry
 RUN: llvm-readobj --headers test7.dll | FileCheck -check-prefix=DIR3 %s
 RUN: llvm-objdump -s --section=.pdata test7.dll | FileCheck -check-prefix=DATA4 %s
 

@llvmbot
Copy link
Member

llvmbot commented Jan 21, 2025

@llvm/pr-subscribers-lld

Author: Jacek Caban (cjacek)

Changes

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

6 Files Affected:

  • (modified) lld/COFF/Chunks.cpp (+11-1)
  • (modified) lld/COFF/Chunks.h (+6-1)
  • (modified) lld/COFF/SymbolTable.h (+3)
  • (modified) lld/COFF/Writer.cpp (+83-21)
  • (modified) lld/test/COFF/arm64x-export.test (+108)
  • (modified) lld/test/COFF/pdata-arm64ec.test (+16-5)
diff --git a/lld/COFF/Chunks.cpp b/lld/COFF/Chunks.cpp
index ff3c89884c24df..2ef74cb4ce6259 100644
--- a/lld/COFF/Chunks.cpp
+++ b/lld/COFF/Chunks.cpp
@@ -1167,7 +1167,7 @@ uint32_t ImportThunkChunkARM64EC::extendRanges() {
 }
 
 uint64_t Arm64XRelocVal::get() const {
-  return (sym ? sym->getRVA() : 0) + value;
+  return (sym ? sym->getRVA() : 0) + (chunk ? chunk->getRVA() : 0) + value;
 }
 
 size_t Arm64XDynamicRelocEntry::getSize() const {
@@ -1230,6 +1230,16 @@ void DynamicRelocsChunk::finalize() {
   size = alignTo(size, sizeof(uint32_t));
 }
 
+// Set the reloc value. The reloc entry must be allocated beforehand.
+void DynamicRelocsChunk::set(uint32_t rva, Arm64XRelocVal value) {
+  Arm64XDynamicRelocEntry &entry =
+      *llvm::find_if(arm64xRelocs, [rva](const Arm64XDynamicRelocEntry &e) {
+        return e.offset.get() == rva;
+      });
+  assert(!entry.value.get());
+  entry.value = value;
+}
+
 void DynamicRelocsChunk::writeTo(uint8_t *buf) const {
   auto table = reinterpret_cast<coff_dynamic_reloc_table *>(buf);
   table->Version = 1;
diff --git a/lld/COFF/Chunks.h b/lld/COFF/Chunks.h
index 7ba58e336451fc..d6216efdd90bdd 100644
--- a/lld/COFF/Chunks.h
+++ b/lld/COFF/Chunks.h
@@ -840,10 +840,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)
+      : chunk(chunk), value(offset) {}
   uint64_t get() const;
 
 private:
   Defined *sym = nullptr;
+  Chunk *chunk = nullptr;
   uint64_t value;
 };
 
@@ -874,10 +877,12 @@ class DynamicRelocsChunk : public NonSectionChunk {
   void finalize();
 
   void add(llvm::COFF::Arm64XFixupType type, uint8_t size,
-           Arm64XRelocVal offset, Arm64XRelocVal value) {
+           Arm64XRelocVal offset, Arm64XRelocVal value = Arm64XRelocVal()) {
     arm64xRelocs.emplace_back(type, size, offset, value);
   }
 
+  void set(uint32_t rva, Arm64XRelocVal value);
+
 private:
   std::vector<Arm64XDynamicRelocEntry> arm64xRelocs;
   size_t size;
diff --git a/lld/COFF/SymbolTable.h b/lld/COFF/SymbolTable.h
index a0acf5db469032..e5b02ce5904c49 100644
--- a/lld/COFF/SymbolTable.h
+++ b/lld/COFF/SymbolTable.h
@@ -155,6 +155,9 @@ class SymbolTable {
   llvm::DenseSet<StringRef> directivesExports;
   bool hadExplicitExports;
 
+  Chunk *edataStart = nullptr;
+  Chunk *edataEnd = nullptr;
+
   void fixupExports();
   void assignExportOrdinals();
 
diff --git a/lld/COFF/Writer.cpp b/lld/COFF/Writer.cpp
index 3d95d219a493cd..bef2ced9f2957d 100644
--- a/lld/COFF/Writer.cpp
+++ b/lld/COFF/Writer.cpp
@@ -288,8 +288,6 @@ class Writer {
   IdataContents idata;
   Chunk *importTableStart = nullptr;
   uint64_t importTableSize = 0;
-  Chunk *edataStart = nullptr;
-  Chunk *edataEnd = nullptr;
   Chunk *iatStart = nullptr;
   uint64_t iatSize = 0;
   DelayLoadContents delayIdata;
@@ -1331,22 +1329,46 @@ void Writer::createExportTable() {
   if (!edataSec->chunks.empty()) {
     // Allow using a custom built export table from input object files, instead
     // of having the linker synthesize the tables.
-    if (ctx.symtab.hadExplicitExports)
-      Warn(ctx) << "literal .edata sections override exports";
-  } else if (!ctx.symtab.exports.empty()) {
-    std::vector<Chunk *> edataChunks;
-    createEdataChunks(ctx.symtab, edataChunks);
-    for (Chunk *c : edataChunks)
-      edataSec->addChunk(c);
-  }
-  if (!edataSec->chunks.empty()) {
-    edataStart = edataSec->chunks.front();
-    edataEnd = edataSec->chunks.back();
+    if (!ctx.hybridSymtab) {
+      ctx.symtab.edataStart = edataSec->chunks.front();
+      ctx.symtab.edataEnd = edataSec->chunks.back();
+    } else {
+      // On hybrid target, split EC and native chunks.
+      llvm::stable_sort(edataSec->chunks, [=](const Chunk *a, const Chunk *b) {
+        return (a->getMachine() != ARM64) < (b->getMachine() != ARM64);
+      });
+
+      for (auto chunk : edataSec->chunks) {
+        if (chunk->getMachine() != ARM64) {
+          ctx.hybridSymtab->edataStart = chunk;
+          ctx.hybridSymtab->edataEnd = edataSec->chunks.back();
+          break;
+        }
+
+        if (!ctx.symtab.edataStart)
+          ctx.symtab.edataStart = chunk;
+        ctx.symtab.edataEnd = chunk;
+      }
+    }
   }
-  // Warn on exported deleting destructor.
-  for (auto e : ctx.symtab.exports)
-    if (e.sym && e.sym->getName().starts_with("??_G"))
-      Warn(ctx) << "export of deleting dtor: " << e.sym;
+  ctx.forEachSymtab([&](SymbolTable &symtab) {
+    if (symtab.edataStart) {
+      if (symtab.hadExplicitExports)
+        Warn(ctx) << "literal .edata sections override exports";
+    } else if (!symtab.exports.empty()) {
+      std::vector<Chunk *> edataChunks;
+      createEdataChunks(symtab, edataChunks);
+      for (Chunk *c : edataChunks)
+        edataSec->addChunk(c);
+      symtab.edataStart = edataChunks.front();
+      symtab.edataEnd = edataChunks.back();
+    }
+
+    // Warn on exported deleting destructor.
+    for (auto e : symtab.exports)
+      if (e.sym && e.sym->getName().starts_with("??_G"))
+        Warn(ctx) << "export of deleting dtor: " << toString(ctx, *e.sym);
+  });
 }
 
 void Writer::removeUnusedSections() {
@@ -1819,10 +1841,11 @@ template <typename PEHeaderTy> void Writer::writeHeader() {
          dataDirOffset64 == buf - buffer->getBufferStart());
   auto *dir = reinterpret_cast<data_directory *>(buf);
   buf += sizeof(*dir) * numberOfDataDirectory;
-  if (edataStart) {
-    dir[EXPORT_TABLE].RelativeVirtualAddress = edataStart->getRVA();
-    dir[EXPORT_TABLE].Size =
-        edataEnd->getRVA() + edataEnd->getSize() - edataStart->getRVA();
+  if (ctx.symtab.edataStart) {
+    dir[EXPORT_TABLE].RelativeVirtualAddress = ctx.symtab.edataStart->getRVA();
+    dir[EXPORT_TABLE].Size = ctx.symtab.edataEnd->getRVA() +
+                             ctx.symtab.edataEnd->getSize() -
+                             ctx.symtab.edataStart->getRVA();
   }
   if (importTableStart) {
     dir[IMPORT_TABLE].RelativeVirtualAddress = importTableStart->getRVA();
@@ -2392,6 +2415,19 @@ void Writer::setECSymbols() {
       symtab->findUnderscore("__arm64x_native_entrypoint")
           ->replaceKeepingName(altEntrySym, sizeof(SymbolUnion));
     }
+
+    if (symtab->edataStart)
+      ctx.dynamicRelocs->set(
+          dataDirOffset64 + EXPORT_TABLE * sizeof(data_directory) +
+              offsetof(data_directory, Size),
+          symtab->edataEnd->getRVA() - symtab->edataStart->getRVA() +
+              symtab->edataEnd->getSize());
+    if (hybridPdata.first)
+      ctx.dynamicRelocs->set(
+          dataDirOffset64 + EXCEPTION_TABLE * sizeof(data_directory) +
+              offsetof(data_directory, Size),
+          hybridPdata.last->getRVA() - hybridPdata.first->getRVA() +
+              hybridPdata.last->getSize());
   }
 }
 
@@ -2644,6 +2680,32 @@ void Writer::createDynamicRelocs() {
       Warn(ctx) << "'__chpe_metadata' is missing for ARM64X target";
   }
 
+  if (ctx.symtab.edataStart != ctx.hybridSymtab->edataStart) {
+    ctx.dynamicRelocs->add(IMAGE_DVRT_ARM64X_FIXUP_TYPE_VALUE, sizeof(uint32_t),
+                           dataDirOffset64 +
+                               EXPORT_TABLE * sizeof(data_directory) +
+                               offsetof(data_directory, RelativeVirtualAddress),
+                           ctx.hybridSymtab->edataStart);
+    // The Size value is assigned after addresses are finalized.
+    ctx.dynamicRelocs->add(IMAGE_DVRT_ARM64X_FIXUP_TYPE_VALUE, sizeof(uint32_t),
+                           dataDirOffset64 +
+                               EXPORT_TABLE * sizeof(data_directory) +
+                               offsetof(data_directory, Size));
+  }
+
+  if (pdata.first != hybridPdata.first) {
+    ctx.dynamicRelocs->add(IMAGE_DVRT_ARM64X_FIXUP_TYPE_VALUE, sizeof(uint32_t),
+                           dataDirOffset64 +
+                               EXCEPTION_TABLE * sizeof(data_directory) +
+                               offsetof(data_directory, RelativeVirtualAddress),
+                           hybridPdata.first);
+    // The Size value is assigned after addresses are finalized.
+    ctx.dynamicRelocs->add(IMAGE_DVRT_ARM64X_FIXUP_TYPE_VALUE, sizeof(uint32_t),
+                           dataDirOffset64 +
+                               EXCEPTION_TABLE * sizeof(data_directory) +
+                               offsetof(data_directory, Size));
+  }
+
   // Set the hybrid load config to the EC load config.
   ctx.dynamicRelocs->add(IMAGE_DVRT_ARM64X_FIXUP_TYPE_VALUE, sizeof(uint32_t),
                          dataDirOffset64 +
diff --git a/lld/test/COFF/arm64x-export.test b/lld/test/COFF/arm64x-export.test
index e5d0307e570efd..526be633973581 100644
--- a/lld/test/COFF/arm64x-export.test
+++ b/lld/test/COFF/arm64x-export.test
@@ -5,6 +5,8 @@ RUN: llvm-mc -filetype=obj -triple=arm64ec-windows arm64ec-func.s -o arm64ec-fun
 RUN: llvm-mc -filetype=obj -triple=aarch64-windows arm64-func.s -o arm64-func.obj
 RUN: llvm-mc -filetype=obj -triple=arm64ec-windows func-drectve.s -o arm64ec-drectve.obj
 RUN: llvm-mc -filetype=obj -triple=aarch64-windows func-drectve.s -o arm64-drectve.obj
+RUN: llvm-mc -filetype=obj -triple=aarch64-windows edata.s -o arm64-edata.obj
+RUN: llvm-mc -filetype=obj -triple=arm64ec-windows edata.s -o arm64ec-edata.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
 
@@ -36,6 +38,15 @@ RUN: llvm-readobj --headers --coff-exports out-cmd.dll | FileCheck --check-prefi
 EXPORTS-EC:      ExportTableRVA: 0x0
 EXPORTS-EC-NEXT: ExportTableSize: 0x0
 EXPORTS-EC-NOT:  Name: func
+EXPORTS-EC:      HybridObject {
+EXPORTS-EC:        ExportTableRVA: 0x3{{.*}}
+EXPORTS-EC-NEXT:   ExportTableSize: 0x4{{.*}}
+EXPORTS-EC:        Export {
+EXPORTS-EC-NEXT:     Ordinal: 1
+EXPORTS-EC-NEXT:     Name: func
+EXPORTS-EC-NEXT:     RVA: 0x2000
+EXPORTS-EC-NEXT:   }
+EXPORTS-EC-NEXT: }
 
 # Export using the EC .drectve section.
 
@@ -44,6 +55,30 @@ RUN:          loadconfig-arm64.obj loadconfig-arm64ec.obj arm64ec-drectve.obj -n
 RUN: llvm-objdump -d out-drectve-ec.dll | FileCheck --check-prefix=DISASM-EC %s
 RUN: llvm-readobj --headers --coff-exports out-drectve-ec.dll | FileCheck --check-prefix=EXPORTS-EC %s
 
+# Export using the EC .edata section.
+
+RUN: lld-link -machine:arm64x -dll -out:out-edata-ec.dll arm64ec-func.obj arm64-func.obj \
+RUN:          loadconfig-arm64.obj loadconfig-arm64ec.obj arm64ec-edata.obj -noentry
+
+RUN: llvm-objdump -d out-edata-ec.dll | FileCheck --check-prefix=DISASM-EDATA-EC %s
+DISASM-EDATA-EC:      0000000180001000 <.text>:
+DISASM-EDATA-EC-NEXT: 180001000: 52800040     mov     w0, #0x2                // =2
+DISASM-EDATA-EC-NEXT: 180001004: d65f03c0     ret
+
+RUN: llvm-readobj --headers --coff-exports out-edata-ec.dll | FileCheck --check-prefix=EXPORTS-EDATA-EC %s
+EXPORTS-EDATA-EC:      ExportTableRVA: 0x0
+EXPORTS-EDATA-EC-NEXT: ExportTableSize: 0x0
+EXPORTS-EDATA-EC-NOT:  Name: func
+EXPORTS-EDATA-EC:      HybridObject {
+EXPORTS-EDATA-EC:        ExportTableRVA: 0x2{{.*}}
+EXPORTS-EDATA-EC-NEXT:   ExportTableSize: 0x4{{.*}}
+EXPORTS-EDATA-EC:        Export {
+EXPORTS-EDATA-EC-NEXT:     Ordinal: 1
+EXPORTS-EDATA-EC-NEXT:     Name: func
+EXPORTS-EDATA-EC-NEXT:     RVA: 0x1000
+EXPORTS-EDATA-EC-NEXT:   }
+EXPORTS-EDATA-EC-NEXT: }
+
 # Export using the native .drectve section.
 
 RUN: lld-link -machine:arm64x -dll -out:out-drectve-native.dll arm64ec-func.obj arm64-func.obj \
@@ -64,6 +99,17 @@ EXPORTS-NATIVE-NEXT:   Ordinal: 1
 EXPORTS-NATIVE-NEXT:   Name: func
 EXPORTS-NATIVE-NEXT:   RVA: 0x1000
 EXPORTS-NATIVE-NEXT: }
+EXPORTS-NATIVE:      HybridObject {
+EXPORTS-NATIVE:        ExportTableRVA: 0x0
+EXPORTS-NATIVE-NEXT:   ExportTableSize: 0x0
+EXPORTS-NATIVE-NOT:    Name: func
+
+# Export using the native .edata section.
+
+RUN: lld-link -machine:arm64x -dll -out:out-edata.dll arm64ec-func.obj arm64-func.obj \
+RUN:          loadconfig-arm64.obj loadconfig-arm64ec.obj arm64-edata.obj -noentry
+RUN: llvm-objdump -d out-edata.dll | FileCheck --check-prefix=DISASM-NATIVE %s
+RUN: llvm-readobj --headers --coff-exports out-edata.dll | FileCheck --check-prefix=EXPORTS-NATIVE %s
 
 # Export using both the native and EC .drectve sections.
 
@@ -99,6 +145,37 @@ EXPORTS-BOTH-NEXT:   Ordinal: 1
 EXPORTS-BOTH-NEXT:   Name: func
 EXPORTS-BOTH-NEXT:   RVA: 0x1000
 EXPORTS-BOTH-NEXT: }
+EXPORTS-BOTH:      HybridObject {
+EXPORTS-BOTH:        ExportTableRVA: 0x4{{.*}}
+EXPORTS-BOTH-NEXT:   ExportTableSize: 0x4{{.*}}
+EXPORTS-BOTH:        Export {
+EXPORTS-BOTH-NEXT:     Ordinal: 1
+EXPORTS-BOTH-NEXT:     Name: func
+EXPORTS-BOTH-NEXT:     RVA: 0x3000
+EXPORTS-BOTH-NEXT:   }
+EXPORTS-BOTH-NEXT: }
+
+# Export using both the native and EC .edata sections.
+
+RUN: lld-link -machine:arm64x -dll -out:out-edata-both.dll arm64ec-func.obj arm64-func.obj \
+RUN:          loadconfig-arm64.obj loadconfig-arm64ec.obj arm64-edata.obj arm64ec-edata.obj -noentry
+RUN: llvm-readobj --headers --coff-exports out-edata-both.dll | FileCheck --check-prefix=EXPORTS-EDATA-BOTH %s
+EXPORTS-EDATA-BOTH:      ExportTableRVA: 0x3{{.*}}
+EXPORTS-EDATA-BOTH-NEXT: ExportTableSize: 0x4{{.*}}
+EXPORTS-EDATA-BOTH:      Export {
+EXPORTS-EDATA-BOTH-NEXT:   Ordinal: 1
+EXPORTS-EDATA-BOTH-NEXT:   Name: func
+EXPORTS-EDATA-BOTH-NEXT:   RVA: 0x1000
+EXPORTS-EDATA-BOTH-NEXT: }
+EXPORTS-EDATA-BOTH:      HybridObject {
+EXPORTS-EDATA-BOTH:        ExportTableRVA: 0x3{{.*}}
+EXPORTS-EDATA-BOTH-NEXT:   ExportTableSize: 0x4{{.*}}
+EXPORTS-EDATA-BOTH:        Export {
+EXPORTS-EDATA-BOTH-NEXT:     Ordinal: 1
+EXPORTS-EDATA-BOTH-NEXT:     Name: func
+EXPORTS-EDATA-BOTH-NEXT:     RVA: 0x2000
+EXPORTS-EDATA-BOTH-NEXT:   }
+EXPORTS-EDATA-BOTH-NEXT: }
 
 #--- arm64-func.s
     .section .text,"xr",discard,func
@@ -119,3 +196,34 @@ func:
 #--- func-drectve.s
 .section .drectve
     .ascii "-export:func"
+
+#--- edata.s
+    .section .edata, "dr"
+    .align 4
+exports:
+    .long 0           // ExportFlags
+    .long 0           // TimeDateStamp
+    .long 0           // MajorVersion + MinorVersion
+    .rva name         // NameRVA
+    .long 1           // OrdinalBase
+    .long 1           // AddressTableEntries
+    .long 1           // NumberOfNamePointers
+    .rva functions    // ExportAddressTableRVA
+    .rva names        // NamePointerRVA
+    .rva nameordinals // OrdinalTableRVA
+
+names:
+    .rva funcname_func
+
+nameordinals:
+    .short 0
+
+functions:
+    .rva func
+    .long 0
+
+funcname_func:
+    .asciz "func"
+
+name:
+    .asciz "out-edata.dll"
diff --git a/lld/test/COFF/pdata-arm64ec.test b/lld/test/COFF/pdata-arm64ec.test
index 7f20c460dc1099..fbec797525f7f8 100644
--- a/lld/test/COFF/pdata-arm64ec.test
+++ b/lld/test/COFF/pdata-arm64ec.test
@@ -6,6 +6,7 @@ Test handlign of hybrid .pdata section on ARM64EC target.
 RUN: llvm-mc -filetype=obj -triple=arm64-windows arm64-func-sym.s -o arm64-func-sym.obj
 RUN: llvm-mc -filetype=obj -triple=arm64ec-windows arm64ec-func-sym.s -o arm64ec-func-sym.obj
 RUN: llvm-mc -filetype=obj -triple=x86_64-windows x86_64-func-sym.s -o x86_64-func-sym.obj
+RUN: llvm-mc -filetype=obj -triple=aarch64-windows %S/Inputs/loadconfig-arm64.s -o loadconfig-arm64.obj
 RUN: llvm-mc -filetype=obj -triple=arm64ec-windows %p/Inputs/loadconfig-arm64ec.s -o loadconfig-arm64ec.obj
 
 Only arm64ec code:
@@ -55,11 +56,21 @@ DATA3: 180005000 00100000 11000001 00200000 0e200000
 Mixed arm64x code:
 
 RUN: lld-link -out:test4.dll -machine:arm64x arm64-func-sym.obj arm64ec-func-sym.obj \
-RUN:          x86_64-func-sym.obj loadconfig-arm64ec.obj -dll -noentry
+RUN:          x86_64-func-sym.obj loadconfig-arm64.obj loadconfig-arm64ec.obj -dll -noentry
 
 RUN: llvm-readobj --headers test4.dll | FileCheck -check-prefix=DIR3 %s
-DIR3:      ExceptionTableRVA: 0x6000
-DIR3-NEXT: ExceptionTableSize: 0x10
+DIR3:      ImageOptionalHeader {
+DIR3:        DataDirectory {
+DIR3:          ExceptionTableRVA: 0x6000
+DIR3-NEXT:     ExceptionTableSize: 0x10
+DIR3:        }
+DIR3:      }
+DIR3:      HybridObject {
+DIR3:        ImageOptionalHeader {
+DIR3:          ExceptionTableRVA: 0x6010
+DIR3-NEXT:     ExceptionTableSize: 0xC
+DIR3:        }
+DIR3:      }
 
 RUN: llvm-objdump -s --section=.pdata test4.dll | FileCheck -check-prefix=DATA4 %s
 DATA4: 180006000 00100000 11000001 00200000 11000001  ......... ......
@@ -74,12 +85,12 @@ RUN: llvm-readobj --headers test5.dll | FileCheck -check-prefix=DIR2 %s
 RUN: llvm-objdump -s --section=.pdata test5.dll | FileCheck -check-prefix=DATA3 %s
 
 RUN: lld-link -out:test6.dll -machine:arm64x arm64ec-func-sym.obj x86_64-func-sym.obj \
-RUN:          arm64-func-sym.obj loadconfig-arm64ec.obj -dll -noentry
+RUN:          arm64-func-sym.obj loadconfig-arm64.obj loadconfig-arm64ec.obj -dll -noentry
 RUN: llvm-readobj --headers test6.dll | FileCheck -check-prefix=DIR3 %s
 RUN: llvm-objdump -s --section=.pdata test6.dll | FileCheck -check-prefix=DATA4 %s
 
 RUN: lld-link -out:test7.dll -machine:arm64x x86_64-func-sym.obj arm64ec-func-sym.obj \
-RUN:          arm64-func-sym.obj loadconfig-arm64ec.obj -dll -noentry
+RUN:          arm64-func-sym.obj loadconfig-arm64.obj loadconfig-arm64ec.obj -dll -noentry
 RUN: llvm-readobj --headers test7.dll | FileCheck -check-prefix=DIR3 %s
 RUN: llvm-objdump -s --section=.pdata test7.dll | FileCheck -check-prefix=DATA4 %s
 

Copy link
Member

@mstorsjo mstorsjo left a comment

Choose a reason for hiding this comment

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

LGTM

@cjacek cjacek merged commit 671ec34 into llvm:main Jan 21, 2025
5 of 7 checks passed
@cjacek cjacek deleted the arm64x-exports branch January 21, 2025 21:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants