[lld] [LLD][COFF] Replace ARM64EC TLS symbols with native symbols when available (PR #212845)

Jacek Caban via llvm-commits llvm-commits at lists.llvm.org
Wed Jul 29 11:56:43 PDT 2026


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

Following MSVC, we don't emit separate TLS directories for EC and native views and instead use native one for both. CRT uses -arm64xsameaddress feature to ensure that it works well for callbacks. Additionally, linker needs to replace EC symbols with native ones to ensure that object files reference them as expected.

Even through EC symbols end up being unused, they still need to be present and defined, so the replacement happens after symbols are resolved. MSVC seems to have some dedicated logic to skip chunks defining EC _tls_used and _tls_index symbols. The logic seems incomplete; for example, if the same chunks defines more symbols, those symbols become equal to __ImageBase (0 RVA). Instead of trying to replicate that, I made sure that GC has a chance to clean up such sections.

>From 3578ed6b208301dfb1a165a82ee970939bd0f575 Mon Sep 17 00:00:00 2001
From: Jacek Caban <jacek at codeweavers.com>
Date: Tue, 28 Jul 2026 23:13:04 +0200
Subject: [PATCH] [LLD][COFF] Replace ARM64EC TLS symbols with native symbols
 when available.

Following MSVC, we don't emit separate TLS directories for EC and native views
and instead use native one for both. CRT uses -arm64xsameaddress feature to
ensure that it works well for callbacks. Additionally, linker needs to replace
EC symbols with native ones to ensure that object files reference them as
expected.

Even through EC symbols end up being unused, they still need to be present
and defined, so the replacement happens after symbols are resolved. MSVC
seems to have some dedicated logic to skip chunks defining EC _tls_used
and _tls_index symbols. The logic seems incomplete; for example, if the same
chunks defines more symbols, those symbols become equal to __ImageBase
(0 RVA). Instead of trying to replicate that, I made sure that GC has a chance
to clean up such sections.
---
 lld/COFF/Driver.cpp        |  21 ++++++--
 lld/test/COFF/arm64x-tls.s | 104 +++++++++++++++++++++++++++++++++++++
 2 files changed, 121 insertions(+), 4 deletions(-)
 create mode 100644 lld/test/COFF/arm64x-tls.s

diff --git a/lld/COFF/Driver.cpp b/lld/COFF/Driver.cpp
index 4eedefbf2ba5e..dc3fd46323032 100644
--- a/lld/COFF/Driver.cpp
+++ b/lld/COFF/Driver.cpp
@@ -2805,10 +2805,6 @@ void LinkerDriver::linkerMain(ArrayRef<const char *> argsArr) {
   ltoCompilationDone = true;
   ctx.forEachSymtab([](SymbolTable &symtab) { symtab.compileBitcodeFiles(); });
 
-  if (Defined *d =
-          dyn_cast_or_null<Defined>(ctx.symtab.findUnderscore("_tls_used")))
-    config->gcroot.push_back(d);
-
   // If -thinlto-index-only is given, we should create only "index
   // files" and not object files. Index file creation is already done
   // in addCombinedLTOObject, so we are done if that's the case.
@@ -2837,6 +2833,23 @@ void LinkerDriver::linkerMain(ArrayRef<const char *> argsArr) {
   if (errorCount())
     return;
 
+  if (ctx.hybridSymtab) {
+    // On ARM64X, use native _tls_used and _tls_index for both views.
+    auto maybeReplaceWithNative = [&](StringRef name) {
+      Symbol *nativeSym = ctx.hybridSymtab->findUnderscore(name);
+      if (!nativeSym || !nativeSym->getDefined())
+        return;
+      if (Symbol *ecSym = ctx.symtab.findUnderscore(name))
+        ecSym->replaceKeepingName(nativeSym, sizeof(SymbolUnion));
+    };
+    maybeReplaceWithNative("_tls_used");
+    maybeReplaceWithNative("_tls_index");
+  }
+
+  if (Defined *d =
+          dyn_cast_or_null<Defined>(ctx.symtab.findUnderscore("_tls_used")))
+    config->gcroot.push_back(d);
+
   ctx.forEachActiveSymtab([](SymbolTable &symtab) {
     symtab.initializeECThunks();
     symtab.initializeLoadConfig();
diff --git a/lld/test/COFF/arm64x-tls.s b/lld/test/COFF/arm64x-tls.s
new file mode 100644
index 0000000000000..21ff0ab26d2af
--- /dev/null
+++ b/lld/test/COFF/arm64x-tls.s
@@ -0,0 +1,104 @@
+// REQUIRES: aarch64
+// RUN: split-file %s %t.dir && cd %t.dir
+
+// RUN: llvm-mc -filetype=obj -triple=aarch64-windows aarch64.s -o aarch64.obj
+// RUN: llvm-mc -filetype=obj -triple=arm64ec-windows arm64ec.s -o arm64ec.obj
+// RUN: llvm-mc -filetype=obj -triple=aarch64-windows tls-aarch64.s -o tls-aarch64.obj
+// RUN: llvm-mc -filetype=obj -triple=arm64ec-windows tls-arm64ec.s -o tls-arm64ec.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: rm -f tls.lib
+// RUN: llvm-lib -machine:arm64x -out:tls.lib tls-aarch64.obj tls-arm64ec.obj loadconfig-arm64ec.obj loadconfig-arm64.obj
+// RUN: lld-link -machine:arm64x -dll -noentry aarch64.obj arm64ec.obj tls.lib -out:out.dll
+
+// Check that we're using native _tls_index and _tls_used for both views.
+
+// RUN: llvm-readobj --coff-tls-directory --hex-dump=.ec --hex-dump=.a64 out.dll | FileCheck %s
+// CHECK:      Format: COFF-ARM64X
+// CHECK-NEXT: Arch: aarch64
+// CHECK-NEXT: AddressSize: 64bit
+// CHECK-EMPTY:
+// CHECK-NEXT: Hex dump of section '.a64':
+// CHECK-NEXT: 0x180004000 00500000 04500000
+// CHECK-EMPTY:
+// CHECK-NEXT: Hex dump of section '.ec':
+// CHECK-NEXT: 0x180006000 00500000 04500000
+// CHECK-NEXT: TLSDirectory {
+// CHECK-NEXT:   StartAddressOfRawData: 0x0
+// CHECK-NEXT:   EndAddressOfRawData: 0x0
+// CHECK-NEXT:   AddressOfIndex: 0x180005000
+// CHECK-NEXT:   AddressOfCallBacks: 0x0
+// CHECK-NEXT:   SizeOfZeroFill: 0x0
+// CHECK-NEXT:   Characteristics [ (0x0)
+// CHECK-NEXT:   ]
+// CHECK-NEXT: }
+// CHECK-NEXT: HybridObject {
+// CHECK-NEXT:   Format: COFF-ARM64EC
+// CHECK-NEXT:   Arch: aarch64
+// CHECK-NEXT:   AddressSize: 64bit
+// CHECK-EMPTY:
+// CHECK-NEXT:   Hex dump of section '.a64':
+// CHECK-NEXT:   0x180004000 00500000 04500000
+// CHECK-EMPTY:
+// CHECK-NEXT:   Hex dump of section '.ec':
+// CHECK-NEXT:   0x180006000 00500000 04500000
+// CHECK-NEXT:   TLSDirectory {
+// CHECK-NEXT:     StartAddressOfRawData: 0x0
+// CHECK-NEXT:     EndAddressOfRawData: 0x0
+// CHECK-NEXT:     AddressOfIndex: 0x180005000
+// CHECK-NEXT:     AddressOfCallBacks: 0x0
+// CHECK-NEXT:     SizeOfZeroFill: 0x0
+// CHECK-NEXT:     Characteristics [ (0x0)
+// CHECK-NEXT:     ]
+// CHECK-NEXT:   }
+// CHECK-NEXT: }
+
+// Check that EC _tls_index and _tls_used pull the object file, but their chunks are collected by GC.
+// RUN: llvm-readobj --sections out.dll | FileCheck --check-prefix=DEFEC %s
+// DEFEC-NOT: defec
+// DEFEC:     .test
+// DEFEC-NOT: defec
+
+#--- tls-aarch64.s
+        .section .defa64,"dr",discard,_tls_index
+        .globl _tls_index
+_tls_index:
+        .long 0
+
+        .section .defa64,"dr",discard,_tls_used
+        .globl _tls_used
+_tls_used:
+        .xword 0
+        .xword 0
+        .xword _tls_index
+        .xword 0
+        .xword 0
+
+#--- aarch64.s
+        .section .a64,"dr"
+        .rva _tls_index
+        .rva _tls_used
+
+#--- tls-arm64ec.s
+        .section .defec,"dr",discard,_tls_index
+        .globl _tls_index
+_tls_index:
+        .long 0
+
+        .section .defec,"dr",discard,_tls_used
+        .globl _tls_used
+_tls_used:
+        .xword 1
+        .xword 1
+        .xword _tls_index
+        .xword 1
+        .xword 1
+
+        .section .test
+        .long 0
+
+#--- arm64ec.s
+        .section .ec,"dr"
+        .rva _tls_index
+        .rva _tls_used



More information about the llvm-commits mailing list