[PATCH v1 2/2] Add TLS support for shared libraries in AIX

Aditya Vidyadhar Kamath <[email protected]> Mon, 3 Aug 2026 16:29:12 +0530
Newsgroups gmane.comp.gdb.patches
Message-ID <[email protected]>
From: Aditya Kamath <[email protected]>

This patch adds support to debug thread local variables defined in shared libraries in AIX.

Sample debug output of this patch is as below
Thread 3 hit Breakpoint 1, thread_runner (arg=0x2) at tls_main.c:36
36        volatile int bp_here = 0; (void)bp_here;

$3 = 20
$4 = 40
thread 2: my_tls_var=20  lib_tls_var=40
[Thread 1 (tid 101646715) (id 1) exited]
[Thread 515 (tid 88015327) (id 3) exited]
[Inferior 1 (process 21758254) exited normally]

where lib_tls_var=40 is a variable from a thread library.

For initial-exec (R_TLS_IE): read the runtime TP-relative offset from the
.loader section TOC slot written by the AIX loader, returning it with bit 0
set as a sentinel so get_thread_local_address() uses it directly without
re-adding the static symbol value.

For global-dynamic/local-dynamic (R_TLSM): scan the .loader section for the
R_TLSM relocation, read the 8-byte mod_id from the inferior's TOC, and
dereference it as a TP-relative pointer to the per-thread block, then add the
intra-module variable offset.

Local-exec (main executable) continues to use the existing tp + offset path.

This patch implements the same as mentioned above.
---
 gdb/rs6000-aix-tdep.c | 205 ++++++++++++++++++++++++++++++++++++------
 1 file changed, 179 insertions(+), 26 deletions(-)

diff --git a/gdb/rs6000-aix-tdep.c b/gdb/rs6000-aix-tdep.c
index aa68654ff41..db81c91524c 100644
--- a/gdb/rs6000-aix-tdep.c
+++ b/gdb/rs6000-aix-tdep.c
@@ -40,6 +40,11 @@
 #include "trad-frame.h"
 #include "frame-unwind.h"
 #include "inferior.h"
+#include "bfd.h"
+#include "coff/internal.h"
+#include "libcoff.h"
+#include "coff/xcoff.h"
+#include "libxcoff.h"
 
 /* If the kernel has to deliver a signal, it pushes a sigcontext
    structure on the stack and then calls the signal handler, passing
@@ -69,6 +74,9 @@
 /* Minimum possible text address in AIX.  */
 #define AIX_TEXT_SEGMENT_BASE 0x10000000
 
+/* R_TLSM relocation type stored in the low byte of internal_ldrel.l_rtype.  */
+#define XCOFF_R_TLSM 0x24
+
 struct rs6000_aix_reg_vrreg_offset
 {
   int vr0_offset;
@@ -1358,45 +1366,165 @@ rs6000_aix_core_xfer_shared_libraries_aix (struct gdbarch *gdbarch,
 
 /* For AIX, use the rs6000_aix_fetch_tls_load_module_address gdbarch method.
 
-   Thread-local variables accessed via a simple TP-relative offset (the Local
-   Exec and Initial Exec TLS models) are only valid for the main executable and
-   for shared libraries that were pulled in at program startup.  Variables
-   belonging to a library loaded later via dlopen() have their storage
-   allocated dynamically; their addresses must be resolved through the DTV
-   (Dynamic Thread Vector) and cannot be computed with a plain TP offset.
+   AIX 64-bit TLS defines four access models, they are:
+
+   local-exec:
+     The TLS variable belongs to the main executable.
+
+   initial-exec:
+     The TLS variable may be in the main executable or a shared library
+     that was present at program startup (i.e. not dlopen'd).
+
+   local-dynamic:
+     The TLS variable is in the same module but it may be a shared library.
+
+   global-dynamic:
+     The general case for shared libraries.  The compiler generates a
+     per-module TOC entry pair:
+       TOC[mod_entry]  -- filled with "mod_id" by the R_TLSM loader relocation
+       TOC[off_entry]  -- filled with the intra-module variable offset by R_TLS
+     mod_id is a signed TP-relative offset to the per-thread block pointer
+     for this module's TLS storage:
+       per_thread_block_ptr = *(thread_pointer + mod_id)
+       variable_addr        = per_thread_block_ptr + xcoff_symbol_value
 
-   Until full DTV-based lookup is implemented, reject any objfile that is a
-   shared library (OBJF_SHARED) which has been dlopen'd and only
-   allow the main executable (OBJF_MAINLINE).  Returning 0 here causes
-   rs6000_aix_get_thread_local_address() to be called with lm_addr == 0,
-   which it treats as the signal to use the TP-relative path.  */
+   In practice, local-exec and initial-exec both resolve via a plain
+   TP-relative offset (lm_addr == 0).  local-dynamic and global-dynamic
+   both require the mod_id indirection (lm_addr != 0) and are handled
+   in rs6000_aix_fetch_tls_load_module_address ().
+
+   To obtain mod_id for a shared library we scan the library's loader
+   section for the first R_TLSM relocation, read the virtual address of the
+   TOC entry it targets, apply the objfile's data-section relocation to get
+   the runtime address, and then read the 8-byte mod_id from the inferior.  */
 
 static CORE_ADDR
 rs6000_aix_fetch_tls_load_module_address (struct objfile *objfile)
 {
-  /* TLS variables from shared libraries cannot be directly fetched
-     via the thread pointer if they were loaded by dlopen().  */
-  if (objfile->flags & OBJF_SHARED)
+  /* Main executable so return 0.  */
+  if (!(objfile->flags & OBJF_SHARED))
+    return 0;
+
+  bfd *abfd = objfile->obfd.get ();
+  if (abfd == nullptr)
     throw_error (TLS_GENERIC_ERROR,
-		 _("TLS lookup via thread pointer is not supported for "
-		   "shared library \"%s\"; full DTV-based lookup is not "
-		   "yet implemented for AIX"),
+		 _("Cannot resolve TLS for \"%s\": no BFD"),
 		 objfile_name (objfile));
 
-  return 0;
+  /* Read the .loader section, which contains the header, symbol table,
+     and relocation table for the shared library.  */
+  asection *loader_sec = bfd_get_section_by_name (abfd, ".loader");
+  if (loader_sec == nullptr)
+    throw_error (TLS_GENERIC_ERROR,
+		 _("Cannot resolve TLS for \"%s\": no .loader section"),
+		 objfile_name (objfile));
+
+  bfd_size_type loader_size = bfd_section_size (loader_sec);
+  gdb::byte_vector loader_buf (loader_size);
+  if (!bfd_get_section_contents (abfd, loader_sec, loader_buf.data (),
+				 0, loader_size))
+    throw_error (TLS_GENERIC_ERROR,
+		 _("Cannot resolve TLS for \"%s\": cannot read .loader section"),
+		 objfile_name (objfile));
+
+  /* Parse the loader header to find the relocation table offset and count.  */
+  struct internal_ldhdr ldhdr;
+  bfd_xcoff_swap_ldhdr_in (abfd, loader_buf.data (), &ldhdr);
+
+  bfd_vma reloc_start = bfd_xcoff_loader_reloc_offset (abfd, &ldhdr);
+  bfd_size_type reloc_size = bfd_xcoff_ldrelsz (abfd);
+
+  if (reloc_start + ldhdr.l_nreloc * reloc_size > loader_size)
+    throw_error (TLS_GENERIC_ERROR,
+		 _("Cannot resolve TLS for \"%s\": .loader section truncated"),
+		 objfile_name (objfile));
+
+  /* Scan loader relocations for TLS reloc types.
+     l_rtype is a 16-bit field: high byte = reloc size, low byte = reloc type
+
+     R_TLS_IE (0x21) = initial-exec: the TOC slot holds the complete
+       TP-relative address of the variable written at load time by the
+       AIX loader.  For shared libraries this value differs from the
+       static XCOFF symbol value.  We must read the runtime TOC slot.
+       Bit 0 of lm_addr is set as a sentinel to distinguish this path
+
+     R_TLSM (0x24) = global-dynamic / local-dynamic: the TOC slot holds
+       mod_id, a signed TP-relative offset to the per-thread block pointer.
+       get_thread_local_address() then dereferences it and adds offset.  */
+
+  const gdb_byte *reloc_ptr = loader_buf.data () + reloc_start;
+  for (size_t i = 0; i < ldhdr.l_nreloc; i++, reloc_ptr += reloc_size)
+    {
+      struct internal_ldrel ldrel;
+      bfd_xcoff_swap_ldrel_in (abfd, reloc_ptr, &ldrel);
+
+      int rtype = ldrel.l_rtype & 0xff;
+      if (rtype != R_TLS_IE && rtype != XCOFF_R_TLSM)
+	continue;
+
+      /* ldrel.l_vaddr is the static (pre-relocation) address of the TOC
+	 entry the loader fills with the TLS value.  Apply the .data section
+	 relocation offset to get the runtime TOC slot address.  */
+      CORE_ADDR toc_addr = ldrel.l_vaddr + objfile->data_section_offset ();
+
+      /* Read the 8-byte value the AIX loader wrote into the TOC slot.  */
+      gdb_byte buf[8];
+      if (target_read_memory (toc_addr, buf, sizeof buf) != 0)
+	throw_error (TLS_GENERIC_ERROR,
+		     _("Cannot resolve TLS for \"%s\": "
+		       "failed to read TLS TOC slot from inferior"),
+		     objfile_name (objfile));
+
+      CORE_ADDR toc_val
+	= extract_signed_integer (buf, sizeof buf, BFD_ENDIAN_BIG);
+
+      /* A zero or positive value means the loader has not yet written the
+	 TLS offset (storage not yet allocated).  */
+      if ((LONGEST) toc_val >= 0)
+	throw_error (TLS_NOT_ALLOCATED_YET_ERROR,
+		     _("TLS storage not yet allocated for \"%s\""),
+		     objfile_name (objfile));
+
+      if (rtype == R_TLS_IE)
+	{
+	  /* initial-exec: return runtime TP-relative offset with bit 0 set
+	     so get_thread_local_address() uses it directly without adding
+	     the symbol offset (which is the static .tdata offset, not the
+	     runtime TP offset for a shared library).  */
+	  return (CORE_ADDR) toc_val | 1;
+	}
+
+      /* R_TLSM: return mod_id as-is (bit 0 is always 0).  */
+      return (CORE_ADDR) toc_val;
+    }
+
+  throw_error (TLS_GENERIC_ERROR,
+	       _("Cannot resolve TLS for \"%s\": "
+		 "no TLS relocation (R_TLS_IE or R_TLSM) found "
+		 "in .loader section"),
+	       objfile_name (objfile));
 }
 
 /* Use the AIX get_thread_local_address gdbarch function.
 
-   On 64-bit AIX the thread pointer is in R13.  For the Local Exec TLS model
-   (used by the main executable) the XCOFF symbol value is a signed
-   TP-relative offset, so the per-thread address is:
+   On 64-bit AIX the thread pointer is in R13.
+
+   lm_addr == 0  (local-exec, main executable):
+     The XCOFF symbol value is a signed TP-relative offset baked in at
+     link time:
+       address = tp + (int64_t) offset
 
-     address = thread_pointer + (int64_t) offset
+   lm_addr has bit 0 set  (initial-exec, shared library):
+     The full runtime TP-relative offset was read from the R_TLS_IE TOC
+     slot by rs6000_aix_fetch_tls_load_module_address().  Do not add the
+     symbol offset again:
+       address = tp + (int64_t)(lm_addr & ~1)
 
-   This only works correctly when the variable's storage is allocated
-   statically relative to the thread pointer, which is guaranteed for the
-   main executable. */
+   lm_addr != 0 and bit 0 clear  (global-dynamic / local-dynamic):
+     lm_addr is mod_id from the R_TLSM TOC slot a signed TP-relative
+     offset to the per-thread block pointer.  Dereference it, then add
+     the intra-module symbol offset:
+       address = *(tp + (int64_t) lm_addr) + (int64_t) offset  */
 
 static CORE_ADDR
 rs6000_aix_get_thread_local_address (struct gdbarch *gdbarch, ptid_t ptid,
@@ -1420,7 +1548,32 @@ rs6000_aix_get_thread_local_address (struct gdbarch *gdbarch, ptid_t ptid,
     throw_error (TLS_GENERIC_ERROR,
 		 _("Unable to fetch thread pointer for TLS lookup"));
 
-  return tp + (CORE_ADDR)(int64_t) offset;
+  if (lm_addr == 0)
+    {
+      /* local-exec (main executable): XCOFF symbol value is TP-relative.  */
+      return tp + (CORE_ADDR)(int64_t) offset;
+    }
+  else if (lm_addr & 1)
+    {
+      /* initial-exec (shared library): lm_addr holds the full runtime
+	 TP-relative offset from the R_TLS_IE TOC slot, with bit 0 set as
+	 a sentinel.  Strip the sentinel and compute the address directly.  */
+      return tp + (CORE_ADDR)(int64_t)(lm_addr & ~(CORE_ADDR)1);
+    }
+  else
+    {
+      /* global-dynamic / local-dynamic: lm_addr is mod_id from R_TLSM.
+	 Dereference the per-thread block pointer, then add the symbol offset.  */
+      CORE_ADDR region_ptr_addr = tp + (CORE_ADDR)(int64_t) lm_addr;
+      gdb_byte buf[8];
+      if (target_read_memory (region_ptr_addr, buf, sizeof buf) != 0)
+	throw_error (TLS_GENERIC_ERROR,
+		     _("Cannot read TLS region pointer for thread-local lookup"));
+
+      CORE_ADDR region_base
+	= extract_unsigned_integer (buf, sizeof buf, BFD_ENDIAN_BIG);
+      return region_base + (CORE_ADDR)(int64_t) offset;
+    }
 }
 
 static void
-- 
2.51.2