Skip to content

[lldb][ARM] Support thread local variables on ARM Linux - #181315

Merged
igorkudrin merged 11 commits into
llvm:mainfrom
igorkudrin:lldb-arm-tls
Mar 2, 2026
Merged

igorkudrin merged 11 commits into
llvm:mainfrom
igorkudrin:lldb-arm-tls

Conversation

@igorkudrin

@igorkudrin igorkudrin commented Feb 13, 2026 •

Copy link
Copy Markdown
Contributor

Currently, DynamicLoaderPOSIXDYLD::GetThreadLocalData() only supports the TLS memory layout where the thread pointer register points to the start of the pthread structure, and the address of the DTV pointer can be calculated by adding the offset of the dtv field to tp. On ARM (and AArch64), the thread pointer points directly to dtv. The patch improves detection of the actual memory layout in the method and adjusts the calculations for the new case, thus adding support for thread-local variables on ARM Linux.

@llvmbot

llvmbot commented Feb 13, 2026

Copy link
Copy Markdown
Member

@llvm/pr-subscribers-lldb

Author: Igor Kudrin (igorkudrin)

Changes

This implements reading the thread pointer register (TPIDRURO) on ARM Linux and improves DynamicLoaderPOSIXDYLD::GetThreadLocalData() to support the TLS memory layout where the thread pointer register points directly to the DTV pointer.


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

12 Files Affected:

  • (modified) lldb/include/lldb/Host/linux/Ptrace.h (+7-1)
  • (modified) lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DYLDRendezvous.cpp (+4-1)
  • (modified) lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DYLDRendezvous.h (+2-1)
  • (modified) lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp (+32-6)
  • (modified) lldb/source/Plugins/Process/Linux/NativeRegisterContextLinux_arm.cpp (+38-3)
  • (modified) lldb/source/Plugins/Process/Linux/NativeRegisterContextLinux_arm.h (+9)
  • (modified) lldb/source/Plugins/Process/Utility/RegisterContextPOSIX_arm.cpp (+7)
  • (modified) lldb/source/Plugins/Process/Utility/RegisterContextPOSIX_arm.h (+2)
  • (modified) lldb/source/Plugins/Process/Utility/RegisterInfoPOSIX_arm.cpp (+23-6)
  • (modified) lldb/source/Plugins/Process/Utility/RegisterInfoPOSIX_arm.h (+4-2)
  • (modified) lldb/source/Plugins/Process/Utility/RegisterInfos_arm.h (+16)
  • (modified) lldb/test/API/lang/c/tls_globals/TestTlsGlobals.py (+1-1)
diff --git a/lldb/include/lldb/Host/linux/Ptrace.h b/lldb/include/lldb/Host/linux/Ptrace.h
index aabd3fd4fc557..233ef962782c7 100644
--- a/lldb/include/lldb/Host/linux/Ptrace.h
+++ b/lldb/include/lldb/Host/linux/Ptrace.h
@@ -38,9 +38,15 @@ typedef int __ptrace_request;
 #ifndef PTRACE_SETREGSET
 #define PTRACE_SETREGSET 0x4205
 #endif
+
 #ifndef PTRACE_GET_THREAD_AREA
+#ifdef __arm__
+#define PTRACE_GET_THREAD_AREA 22
+#else
 #define PTRACE_GET_THREAD_AREA 25
-#endif
+#endif // __arm__
+#endif // PTRACE_GET_THREAD_AREA
+
 #ifndef PTRACE_ARCH_PRCTL
 #define PTRACE_ARCH_PRCTL 30
 #endif
diff --git a/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DYLDRendezvous.cpp b/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DYLDRendezvous.cpp
index 1a9c4593b1b4f..f9c29b95185dc 100644
--- a/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DYLDRendezvous.cpp
+++ b/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DYLDRendezvous.cpp
@@ -714,7 +714,8 @@ bool DYLDRendezvous::FindMetadata(const char *name, PThreadField field,
     return false;
 
   Address address = list[0].symbol->GetAddress();
-  address.SetOffset(address.GetOffset() + field * sizeof(uint32_t));
+  int field_num = (field == eStructSize) ? 0 : field;
+  address.SetOffset(address.GetOffset() + field_num * sizeof(uint32_t));
 
   // Read from target memory as this allows us to try process memory and
   // fallback to reading from read only sections from the object files. Here we
@@ -737,6 +738,8 @@ const DYLDRendezvous::ThreadInfo &DYLDRendezvous::GetThreadInfo() {
   if (!m_thread_info.valid) {
     bool ok = true;
 
+    ok &= FindMetadata("_thread_db_sizeof_pthread", eStructSize,
+                       m_thread_info.pthread_size);
     ok &= FindMetadata("_thread_db_pthread_dtvp", eOffset,
                        m_thread_info.dtv_offset);
     ok &=
diff --git a/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DYLDRendezvous.h b/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DYLDRendezvous.h
index b8bdf78fbdfad..d13b8250cc314 100644
--- a/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DYLDRendezvous.h
+++ b/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DYLDRendezvous.h
@@ -133,6 +133,7 @@ class DYLDRendezvous {
   // the per-thread state.
   struct ThreadInfo {
     bool valid;             // whether we read valid metadata
+    uint32_t pthread_size;  // size of struct pthread
     uint32_t dtv_offset;    // offset of DTV pointer within pthread
     uint32_t dtv_slot_size; // size of one DTV slot
     uint32_t modid_offset;  // offset of module ID within link_map
@@ -345,7 +346,7 @@ class DYLDRendezvous {
   /// supplied by the runtime linker.
   bool TakeSnapshot(SOEntryList &entry_list);
 
-  enum PThreadField { eSize, eNElem, eOffset };
+  enum PThreadField { eSize, eNElem, eOffset, eStructSize };
 
   bool FindMetadata(const char *name, PThreadField field, uint32_t &value);
 
diff --git a/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp b/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp
index 1d814f93484d8..270339dd74111 100644
--- a/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp
+++ b/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp
@@ -843,10 +843,11 @@ DynamicLoaderPOSIXDYLD::GetThreadLocalData(const lldb::ModuleSP module_sp,
   LLDB_LOGF(log,
             "GetThreadLocalData info: link_map=0x%" PRIx64
             ", thread info metadata: "
-            "modid_offset=0x%" PRIx32 ", dtv_offset=0x%" PRIx32
-            ", tls_offset=0x%" PRIx32 ", dtv_slot_size=%" PRIx32 "\n",
-            link_map, metadata.modid_offset, metadata.dtv_offset,
-            metadata.tls_offset, metadata.dtv_slot_size);
+            "modid_offset=0x%" PRIx32 ", pthread_size=0x%" PRIx32
+            ", dtv_offset=0x%" PRIx32 ", tls_offset=0x%" PRIx32
+            ", dtv_slot_size=%" PRIx32 "\n",
+            link_map, metadata.modid_offset, metadata.pthread_size,
+            metadata.dtv_offset, metadata.tls_offset, metadata.dtv_slot_size);
 
   // Get the thread pointer.
   addr_t tp = thread->GetThreadPointer();
@@ -865,8 +866,33 @@ DynamicLoaderPOSIXDYLD::GetThreadLocalData(const lldb::ModuleSP module_sp,
   }
 
   // Lookup the DTV structure for this thread.
-  addr_t dtv_ptr = tp + metadata.dtv_offset;
-  addr_t dtv = ReadPointer(dtv_ptr);
+  addr_t dtv_ptr = LLDB_INVALID_ADDRESS;
+  if (metadata.dtv_offset < metadata.pthread_size) {
+    // The DTV pointer field lies within `pthread`. This indicates that `libc`
+    // placed `tcbhead_t header`, which contains the `dtv` field, inside
+    // `pthread`, so, for this architecture, `TLS_TCB_AT_TP` is set to `1` and
+    // `TLS_DTV_AT_TP` is `0`. This corresponds to the "Variant II" memory
+    // layout described in Ulrich Drepper's ELF TLS document
+    // (https://akkadia.org/drepper/tls.pdf). The thread pointer points to the
+    // start of `pthread`, and the address of the `dtv` field can be calculated
+    // by adding its offset.
+    dtv_ptr = tp + metadata.dtv_offset;
+  } else if (metadata.dtv_offset == metadata.pthread_size) {
+    // The DTV pointer field is located right after `pthread`. This means that,
+    // for this architecture, `TLS_DTV_AT_TP` is set to `1` in `libc`, which may
+    // correspond to the "Version I" memory layout, in which the thread pointer
+    // points directly to the `dtv` field. However, for different architectures,
+    // the position of the `dtv` field relative to the thread pointer may vary,
+    // so the following calculations must be adjusted for each platform.
+    //
+    // On AArch64 and ARM, `tp` is known to point directly to `dtv`.
+    const llvm::Triple &triple = module_sp->GetArchitecture().GetTriple();
+    if (triple.isAArch64() || triple.isARM()) {
+      dtv_ptr = tp;
+    }
+  }
+  addr_t dtv = (dtv_ptr != LLDB_INVALID_ADDRESS) ? ReadPointer(dtv_ptr)
+                                                 : LLDB_INVALID_ADDRESS;
   if (dtv == LLDB_INVALID_ADDRESS) {
     LLDB_LOGF(log, "GetThreadLocalData error: fail to read dtv");
     return LLDB_INVALID_ADDRESS;
diff --git a/lldb/source/Plugins/Process/Linux/NativeRegisterContextLinux_arm.cpp b/lldb/source/Plugins/Process/Linux/NativeRegisterContextLinux_arm.cpp
index c1bc6a3f036bf..70b957b0285fe 100644
--- a/lldb/source/Plugins/Process/Linux/NativeRegisterContextLinux_arm.cpp
+++ b/lldb/source/Plugins/Process/Linux/NativeRegisterContextLinux_arm.cpp
@@ -25,9 +25,10 @@
 
 #if defined(__arm64__) || defined(__aarch64__)
 #include "NativeRegisterContextLinux_arm64dbreg.h"
+#endif
+
 #include "lldb/Host/linux/Ptrace.h"
 #include <asm/ptrace.h>
-#endif
 
 #define REG_CONTEXT_SIZE (GetGPRSize() + sizeof(m_fpr))
 
@@ -68,8 +69,9 @@ NativeRegisterContextLinux::DetermineArchitecture(lldb::tid_t tid) {
 
 NativeRegisterContextLinux_arm::NativeRegisterContextLinux_arm(
     const ArchSpec &target_arch, NativeThreadProtocol &native_thread)
-    : NativeRegisterContextRegisterInfo(native_thread,
-                                        new RegisterInfoPOSIX_arm(target_arch)),
+    : NativeRegisterContextRegisterInfo(
+          native_thread,
+          new RegisterInfoPOSIX_arm(target_arch, /*has_tls_reg=*/true)),
       NativeRegisterContextLinux(native_thread) {
   assert(target_arch.GetMachine() == llvm::Triple::arm);
 
@@ -77,6 +79,7 @@ NativeRegisterContextLinux_arm::NativeRegisterContextLinux_arm(
   ::memset(&m_gpr_arm, 0, sizeof(m_gpr_arm));
   ::memset(&m_hwp_regs, 0, sizeof(m_hwp_regs));
   ::memset(&m_hbp_regs, 0, sizeof(m_hbp_regs));
+  m_tpidr = 0;
 
   // 16 is just a maximum value, query hardware for actual watchpoint count
   m_max_hwp_supported = 16;
@@ -120,6 +123,11 @@ NativeRegisterContextLinux_arm::ReadRegister(const RegisterInfo *reg_info,
     error = ReadFPR();
     if (error.Fail())
       return error;
+  } else if (IsTLS(reg)) {
+    error = ReadTLS();
+    if (error.Success())
+      reg_value.SetUInt32(m_tpidr);
+    return error;
   } else {
     uint32_t full_reg = reg;
     bool is_subreg = reg_info->invalidate_regs &&
@@ -199,6 +207,10 @@ NativeRegisterContextLinux_arm::WriteRegister(const RegisterInfo *reg_info,
     return WriteFPR();
   }
 
+  if (IsTLS(reg_index))
+    return Status::FromErrorString(
+        "writing to a thread pointer register is not implemented");
+
   return Status::FromErrorString(
       "failed - register wasn't recognized to be a GPR or an FPR, "
       "write strategy unknown");
@@ -283,6 +295,13 @@ bool NativeRegisterContextLinux_arm::IsFPR(unsigned reg) const {
   return false;
 }
 
+bool NativeRegisterContextLinux_arm::IsTLS(unsigned reg) const {
+  if (GetRegisterInfo().GetRegisterSetFromRegisterIndex(reg) ==
+      RegisterInfoPOSIX_arm::TLSRegSet)
+    return true;
+  return false;
+}
+
 llvm::Error NativeRegisterContextLinux_arm::ReadHardwareDebugInfo() {
   if (!m_refresh_hwdebug_info)
     return llvm::Error::success();
@@ -480,4 +499,20 @@ Status NativeRegisterContextLinux_arm::WriteFPR() {
 #endif // __arm__
 }
 
+Status NativeRegisterContextLinux_arm::ReadTLS() {
+#ifdef __arm__
+  return NativeProcessLinux::PtraceWrapper(PTRACE_GET_THREAD_AREA,
+                                           m_thread.GetID(), nullptr,
+                                           GetTLSBuffer(), GetTLSSize());
+#else  // __aarch64__
+  Status error;
+
+  struct iovec ioVec;
+  ioVec.iov_base = GetTLSBuffer();
+  ioVec.iov_len = GetTLSSize();
+
+  return ReadRegisterSet(&ioVec, GetTLSSize(), NT_ARM_TLS);
+#endif // __arm__
+}
+
 #endif // defined(__arm__) || defined(__arm64__) || defined(__aarch64__)
diff --git a/lldb/source/Plugins/Process/Linux/NativeRegisterContextLinux_arm.h b/lldb/source/Plugins/Process/Linux/NativeRegisterContextLinux_arm.h
index cf36859b16ad4..2c292ad19b966 100644
--- a/lldb/source/Plugins/Process/Linux/NativeRegisterContextLinux_arm.h
+++ b/lldb/source/Plugins/Process/Linux/NativeRegisterContextLinux_arm.h
@@ -58,15 +58,22 @@ class NativeRegisterContextLinux_arm : public NativeRegisterContextLinux,
 
   Status WriteFPR() override;
 
+  Status ReadTLS();
+
   void *GetGPRBuffer() override { return &m_gpr_arm; }
 
   void *GetFPRBuffer() override { return &m_fpr; }
 
   size_t GetFPRSize() override { return sizeof(m_fpr); }
 
+  void *GetTLSBuffer() { return &m_tpidr; }
+
+  size_t GetTLSSize() { return sizeof(m_tpidr); }
+
 private:
   uint32_t m_gpr_arm[k_num_gpr_registers_arm];
   RegisterInfoPOSIX_arm::FPU m_fpr;
+  uint32_t m_tpidr;
 
   bool m_refresh_hwdebug_info;
 
@@ -74,6 +81,8 @@ class NativeRegisterContextLinux_arm : public NativeRegisterContextLinux,
 
   bool IsFPR(unsigned reg) const;
 
+  bool IsTLS(unsigned reg) const;
+
   llvm::Error ReadHardwareDebugInfo() override;
 
   llvm::Error WriteHardwareDebugRegs(DREGType hwbType) override;
diff --git a/lldb/source/Plugins/Process/Utility/RegisterContextPOSIX_arm.cpp b/lldb/source/Plugins/Process/Utility/RegisterContextPOSIX_arm.cpp
index 684176bccdf0c..2d8f5a30fcd56 100644
--- a/lldb/source/Plugins/Process/Utility/RegisterContextPOSIX_arm.cpp
+++ b/lldb/source/Plugins/Process/Utility/RegisterContextPOSIX_arm.cpp
@@ -39,6 +39,13 @@ bool RegisterContextPOSIX_arm::IsFPR(unsigned reg) {
   return false;
 }
 
+bool RegisterContextPOSIX_arm::IsTLS(unsigned reg) {
+  if (m_register_info_up->GetRegisterSetFromRegisterIndex(reg) ==
+      RegisterInfoPOSIX_arm::TLSRegSet)
+    return true;
+  return false;
+}
+
 RegisterContextPOSIX_arm::RegisterContextPOSIX_arm(
     lldb_private::Thread &thread,
     std::unique_ptr<RegisterInfoPOSIX_arm> register_info)
diff --git a/lldb/source/Plugins/Process/Utility/RegisterContextPOSIX_arm.h b/lldb/source/Plugins/Process/Utility/RegisterContextPOSIX_arm.h
index 099c37d46f496..ecd927e64cf70 100644
--- a/lldb/source/Plugins/Process/Utility/RegisterContextPOSIX_arm.h
+++ b/lldb/source/Plugins/Process/Utility/RegisterContextPOSIX_arm.h
@@ -51,6 +51,8 @@ class RegisterContextPOSIX_arm : public lldb_private::RegisterContext {
 
   bool IsFPR(unsigned reg);
 
+  bool IsTLS(unsigned reg);
+
   size_t GetFPUSize() { return sizeof(RegisterInfoPOSIX_arm::FPU); }
 
   virtual bool ReadGPR() = 0;
diff --git a/lldb/source/Plugins/Process/Utility/RegisterInfoPOSIX_arm.cpp b/lldb/source/Plugins/Process/Utility/RegisterInfoPOSIX_arm.cpp
index d47647422ae21..3a3846ace1aaf 100644
--- a/lldb/source/Plugins/Process/Utility/RegisterInfoPOSIX_arm.cpp
+++ b/lldb/source/Plugins/Process/Utility/RegisterInfoPOSIX_arm.cpp
@@ -75,7 +75,9 @@ GetRegisterInfoCount(const lldb_private::ArchSpec &target_arch) {
 enum {
   k_num_gpr_registers = gpr_cpsr - gpr_r0 + 1,
   k_num_fpr_registers = fpu_q15 - fpu_s0 + 1,
-  k_num_register_sets = 2
+  k_num_tls_registers = 1,
+  k_num_register_sets_without_tls = 2,
+  k_num_register_sets_with_tls = 3
 };
 
 // arm general purpose registers.
@@ -142,18 +144,30 @@ static_assert(((sizeof g_fpu_regnums_arm / sizeof g_fpu_regnums_arm[0]) - 1) ==
                   k_num_fpr_registers,
               "g_fpu_regnums_arm has wrong number of register infos");
 
+// arm thread local storage registers.
+static const uint32_t g_tls_regnums_arm[] = {
+    tls_tpidruro,
+    LLDB_INVALID_REGNUM // register sets need to end with this flag
+};
+static_assert(((sizeof g_tls_regnums_arm / sizeof g_tls_regnums_arm[0]) - 1) ==
+                  k_num_tls_registers,
+              "g_tls_regnums_arm has wrong number of register infos");
+
 // Register sets for arm.
-static const RegisterSet g_reg_sets_arm[k_num_register_sets] = {
+static const RegisterSet g_reg_sets_arm[k_num_register_sets_with_tls] = {
     {"General Purpose Registers", "gpr", k_num_gpr_registers,
      g_gpr_regnums_arm},
     {"Floating Point Registers", "fpu", k_num_fpr_registers,
-     g_fpu_regnums_arm}};
+     g_fpu_regnums_arm},
+    {"Thread Local Storage Registers", "tls", k_num_tls_registers,
+     g_tls_regnums_arm}};
 
 RegisterInfoPOSIX_arm::RegisterInfoPOSIX_arm(
-    const lldb_private::ArchSpec &target_arch)
+    const lldb_private::ArchSpec &target_arch, bool has_tls_reg)
     : lldb_private::RegisterInfoAndSetInterface(target_arch),
       m_register_info_p(GetRegisterInfoPtr(target_arch)),
-      m_register_info_count(GetRegisterInfoCount(target_arch)) {}
+      m_register_info_count(GetRegisterInfoCount(target_arch)),
+      m_has_tls_reg(has_tls_reg) {}
 
 size_t RegisterInfoPOSIX_arm::GetGPRSize() const {
   return sizeof(struct RegisterInfoPOSIX_arm::GPR);
@@ -169,7 +183,8 @@ RegisterInfoPOSIX_arm::GetRegisterInfo() const {
 }
 
 size_t RegisterInfoPOSIX_arm::GetRegisterSetCount() const {
-  return k_num_register_sets;
+  return m_has_tls_reg ? k_num_register_sets_with_tls
+                       : k_num_register_sets_without_tls;
 }
 
 size_t RegisterInfoPOSIX_arm::GetRegisterSetFromRegisterIndex(
@@ -178,6 +193,8 @@ size_t RegisterInfoPOSIX_arm::GetRegisterSetFromRegisterIndex(
     return GPRegSet;
   if (reg_index <= fpu_q15)
     return FPRegSet;
+  if (reg_index == tls_tpidruro && m_has_tls_reg)
+    return TLSRegSet;
   return LLDB_INVALID_REGNUM;
 }
 
diff --git a/lldb/source/Plugins/Process/Utility/RegisterInfoPOSIX_arm.h b/lldb/source/Plugins/Process/Utility/RegisterInfoPOSIX_arm.h
index db155d757ca8c..327e9f746cab0 100644
--- a/lldb/source/Plugins/Process/Utility/RegisterInfoPOSIX_arm.h
+++ b/lldb/source/Plugins/Process/Utility/RegisterInfoPOSIX_arm.h
@@ -15,7 +15,7 @@
 
 class RegisterInfoPOSIX_arm : public lldb_private::RegisterInfoAndSetInterface {
 public:
-  enum { GPRegSet = 0, FPRegSet};
+  enum { GPRegSet = 0, FPRegSet, TLSRegSet };
 
   struct GPR {
     uint32_t r[16]; // R0-R15
@@ -47,7 +47,8 @@ class RegisterInfoPOSIX_arm : public lldb_private::RegisterInfoAndSetInterface {
     uint32_t wcr[16];
   };
 
-  RegisterInfoPOSIX_arm(const lldb_private::ArchSpec &target_arch);
+  RegisterInfoPOSIX_arm(const lldb_private::ArchSpec &target_arch,
+                        bool has_tls_reg = false);
 
   size_t GetGPRSize() const override;
 
@@ -67,6 +68,7 @@ class RegisterInfoPOSIX_arm : public lldb_private::RegisterInfoAndSetInterface {
 private:
   const lldb_private::RegisterInfo *m_register_info_p;
   uint32_t m_register_info_count;
+  bool m_has_tls_reg;
 };
 
 #endif // LLDB_SOURCE_PLUGINS_PROCESS_UTILITY_REGISTERINFOPOSIX_ARM_H
diff --git a/lldb/source/Plugins/Process/Utility/RegisterInfos_arm.h b/lldb/source/Plugins/Process/Utility/RegisterInfos_arm.h
index f535ca40c4030..afaada679cb79 100644
--- a/lldb/source/Plugins/Process/Utility/RegisterInfos_arm.h
+++ b/lldb/source/Plugins/Process/Utility/RegisterInfos_arm.h
@@ -146,6 +146,8 @@ enum {
   fpu_q14,
   fpu_q15,
 
+  tls_tpidruro,
+
   exc_exception,
   exc_fsr,
   exc_far,
@@ -682,6 +684,20 @@ static RegisterInfo g_register_infos_arm[] = {
     FPU_QREG(q14, 56),
     FPU_QREG(q15, 60),
 
+    {
+        "tpidruro",
+        nullptr,
+        4,
+        0,
+        eEncodingUint,
+        eFormatHex,
+        {LLDB_INVALID_REGNUM, LLDB_INVALID_REGNUM, LLDB_REGNUM_GENERIC_TP,
+         LLDB_INVALID_REGNUM, tls_tpidruro},
+        nullptr,
+        nullptr,
+        nullptr,
+    },
+
     {
         "exception",
         nullptr,
diff --git a/lldb/test/API/lang/c/tls_globals/TestTlsGlobals.py b/lldb/test/API/lang/c/tls_globals/TestTlsGlobals.py
index ed696bca54ab4..28d5212cbc7dc 100644
--- a/lldb/test/API/lang/c/tls_globals/TestTlsGlobals.py
+++ b/lldb/test/API/lang/c/tls_globals/TestTlsGlobals.py
@@ -38,7 +38,7 @@ def setUp(self):
     # TLS works differently on Windows, this would need to be implemented
     # separately.
     @skipIfWindows
-    @skipIf(oslist=["linux"], archs=["arm$", "aarch64"])
+    @skipIf(oslist=["linux"], archs=["aarch64"])
     @skipIf(oslist=no_match([lldbplatformutil.getDarwinOSTriples(), "linux"]))
     @expectedFailureIf(lldbplatformutil.xcode15LinkerBug())
     def test(self):

@github-actions

github-actions Bot commented Feb 13, 2026 •

Copy link
Copy Markdown

✅ With the latest revision this PR passed the C/C++ code formatter.

This implements reading the thread pointer register (`TPIDRURO`) on ARM
Linux and improves `DynamicLoaderPOSIXDYLD::GetThreadLocalData()` to
support the TLS memory layout where the thread pointer register points
directly to the DTV pointer.

@jeffreytan81 jeffreytan81 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for adding this.
Is it expected that aarch64 test is still not working? If so, can you documenting the remaining work required to get it working and if you plan to address it next?

Comment thread lldb/include/lldb/Host/linux/Ptrace.h
Comment thread lldb/test/API/lang/c/tls_globals/TestTlsGlobals.py

Address address = list[0].symbol->GetAddress();
address.SetOffset(address.GetOffset() + field * sizeof(uint32_t));
int field_num = (field == eStructSize) ? 0 : field;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you add a comment explaining this change?

Comment thread lldb/include/lldb/Host/linux/Ptrace.h
Comment thread lldb/source/Plugins/Process/Linux/NativeRegisterContextLinux_arm.cpp Outdated
Comment thread lldb/source/Plugins/Process/Utility/RegisterContextPOSIX_arm.cpp Outdated
Comment thread lldb/source/Plugins/Process/Utility/RegisterInfos_arm.h
Comment thread lldb/test/API/lang/c/tls_globals/TestTlsGlobals.py
Comment thread lldb/source/Plugins/Process/Utility/RegisterInfoPOSIX_arm.h
Comment thread lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DYLDRendezvous.h
Comment thread lldb/source/Plugins/Process/Utility/RegisterInfos_arm.h

@igorkudrin igorkudrin left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I've just realised that I forgot to specify a distinct offset for the new register. Fixing this now with an additional "fixup" commit.

Comment thread lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DYLDRendezvous.h
Comment thread lldb/source/Plugins/Process/Utility/RegisterInfos_arm.h
@DavidSpickett

Copy link
Copy Markdown
Contributor

I've just realised that I forgot to specify a distinct offset for the new register.

Sorry to bring this up so late but this reminds me we need tests for the register access. Please split this PR so that adding the new register is its own PR, and this is just the dyld parts.

My AArch64 TLS register commits for reference:
7e22921 43ad521 6239227

You've implemented the register as read only, which is fine. So you don't need to test what happens with an expression, just check that a write from lldb fails. You can likely use the existing Arm core file and read whatever random value happens to be in there. For a live test, I suggest a program with a thread local, and reading the TLS register using inline asm into a local variable that LLDB can read. Then compare that to what you see in LLDB via. read register.

You can put the live process test in a new folder in https://github.com/llvm/llvm-project/tree/main/lldb/test/API/linux. The core file tests are in lldb/test/API/functionalities/postmortem/elf-core/TestLinuxCore.py.

Each needs a release note, I suggest:

  • On Arm Linux, the tpidruro register can now be read. Writing to this register is not supported.
  • Thread local variables are now supported on Arm Linux if the program being debugged is using glibc.

@DavidSpickett

Copy link
Copy Markdown
Contributor

And to be clear - you don't need to annotate the tests with any "only if glibc" stuff. We don't actively support running the test suite on Linux with anything else.

My concern with other C libraries is what individual users see rather than people who build and test lldb. If those folks need musl support than as you say, they can work on that.

@igorkudrin

Copy link
Copy Markdown
Contributor Author

I've just realised that I forgot to specify a distinct offset for the new register.

Sorry to bring this up so late but this reminds me we need tests for the register access. Please split this PR so that adding the new register is its own PR, and this is just the dyld parts.

I've created #182438; will update this PR when the other one is merged.

My AArch64 TLS register commits for reference: 7e22921 43ad521 6239227

Thanks for the references!

You've implemented the register as read only, which is fine. So you don't need to test what happens with an expression, just check that a write from lldb fails. You can likely use the existing Arm core file and read whatever random value happens to be in there.

As far as I can see, NT_ARM_TLS notes are not generated for Arm32 binaries, so this feature will be missing for core files.

Each needs a release note, I suggest:

  • On Arm Linux, the tpidruro register can now be read. Writing to this register is not supported.
  • Thread local variables are now supported on Arm Linux if the program being debugged is using glibc.

Thanks!

And to be clear - you don't need to annotate the tests with any "only if glibc" stuff. We don't actively support running the test suite on Linux with anything else.

My concern with other C libraries is what individual users see rather than people who build and test lldb. If those folks need musl support than as you say, they can work on that.

Such users will be able to read the TP register, but because DynamicLoaderPOSIXDYLD::GetThreadLocalData() will not get the valid metadata and return LLDB_INVALID_ADDRESS, thread local variables will still display the same "optimized out" message. At least, their debugging experience won't worsen.

@DavidSpickett

Copy link
Copy Markdown
Contributor

As far as I can see, NT_ARM_TLS notes are not generated for Arm32 binaries, so this feature will be missing for core files.

Yep, kernel sources confirm that.

At least, their debugging experience won't worsen.

Great, no problem then.

@DavidSpickett DavidSpickett left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

@jeffreytan81 any more comments? (note that adding the new register was split into another PR, which has already landed)

@igorkudrin
igorkudrin merged commit a4d7866 into llvm:main Mar 2, 2026
12 checks passed
@igorkudrin
igorkudrin deleted the lldb-arm-tls branch March 2, 2026 20:50
sahas3 pushed a commit to sahas3/llvm-project that referenced this pull request Mar 4, 2026
Currently, `DynamicLoaderPOSIXDYLD::GetThreadLocalData()` only supports
the TLS memory layout where the thread pointer register points to the
start of the `pthread` structure, and the address of the DTV pointer can
be calculated by adding the offset of the `dtv` field to `tp`. On ARM
(and AArch64), the thread pointer points directly to `dtv`. The patch
improves the detection of the actual memory layout in the method and
adjusts the calculations for the new case, thus adding support for
thread-local variables on ARM Linux.
sujianIBM pushed a commit to sujianIBM/llvm-project that referenced this pull request Mar 5, 2026
Currently, `DynamicLoaderPOSIXDYLD::GetThreadLocalData()` only supports
the TLS memory layout where the thread pointer register points to the
start of the `pthread` structure, and the address of the DTV pointer can
be calculated by adding the offset of the `dtv` field to `tp`. On ARM
(and AArch64), the thread pointer points directly to `dtv`. The patch
improves the detection of the actual memory layout in the method and
adjusts the calculations for the new case, thus adding support for
thread-local variables on ARM Linux.
felipepiovezan pushed a commit to felipepiovezan/llvm-project that referenced this pull request Jul 10, 2026
Currently, `DynamicLoaderPOSIXDYLD::GetThreadLocalData()` only supports
the TLS memory layout where the thread pointer register points to the
start of the `pthread` structure, and the address of the DTV pointer can
be calculated by adding the offset of the `dtv` field to `tp`. On ARM
(and AArch64), the thread pointer points directly to `dtv`. The patch
improves the detection of the actual memory layout in the method and
adjusts the calculations for the new case, thus adding support for
thread-local variables on ARM Linux.
felipepiovezan pushed a commit to felipepiovezan/llvm-project that referenced this pull request Jul 13, 2026
Currently, `DynamicLoaderPOSIXDYLD::GetThreadLocalData()` only supports
the TLS memory layout where the thread pointer register points to the
start of the `pthread` structure, and the address of the DTV pointer can
be calculated by adding the offset of the `dtv` field to `tp`. On ARM
(and AArch64), the thread pointer points directly to `dtv`. The patch
improves the detection of the actual memory layout in the method and
adjusts the calculations for the new case, thus adding support for
thread-local variables on ARM Linux.
felipepiovezan pushed a commit to felipepiovezan/llvm-project that referenced this pull request Jul 20, 2026
Currently, `DynamicLoaderPOSIXDYLD::GetThreadLocalData()` only supports
the TLS memory layout where the thread pointer register points to the
start of the `pthread` structure, and the address of the DTV pointer can
be calculated by adding the offset of the `dtv` field to `tp`. On ARM
(and AArch64), the thread pointer points directly to `dtv`. The patch
improves the detection of the actual memory layout in the method and
adjusts the calculations for the new case, thus adding support for
thread-local variables on ARM Linux.
felipepiovezan pushed a commit to felipepiovezan/llvm-project that referenced this pull request Jul 20, 2026
Currently, `DynamicLoaderPOSIXDYLD::GetThreadLocalData()` only supports
the TLS memory layout where the thread pointer register points to the
start of the `pthread` structure, and the address of the DTV pointer can
be calculated by adding the offset of the `dtv` field to `tp`. On ARM
(and AArch64), the thread pointer points directly to `dtv`. The patch
improves the detection of the actual memory layout in the method and
adjusts the calculations for the new case, thus adding support for
thread-local variables on ARM Linux.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants