Skip to content

[LLD][AArch64] Handle R_AARCH64_TLS_DTPREL64 in non-alloc sections - #183962

Merged
xgupta merged 3 commits into
llvm:mainfrom
xgupta:lld-tls
Mar 31, 2026
Merged

xgupta merged 3 commits into
llvm:mainfrom
xgupta:lld-tls

Conversation

@xgupta

@xgupta xgupta commented Feb 28, 2026 •

Copy link
Copy Markdown
Contributor

Clang plan to emit R_AARCH64_TLS_DTPREL64 in .debug_info (see PR #146572). LLD currently fails to recognize this relocation.

This prevent the debugger from correctly locating TLS variables when using the DWARF DW_OP_GNU_push_tls_address or DW_AT_location with DTPREL offsets.

This patch adds support for R_AARCH64_TLS_DTPREL64, adds its mapping to R_DTPREL.

@xgupta
xgupta requested a review from MaskRay February 28, 2026 22:32
@github-actions

Copy link
Copy Markdown

Thank you for submitting a Pull Request (PR) to the LLVM Project!

This PR will be automatically labeled and the relevant teams will be notified.

If you wish to, you can add reviewers by using the "Reviewers" section on this page.

If this is not working for you, it is probably because you do not have write permissions for the repository. In which case you can instead tag reviewers by name in a comment by using @ followed by their GitHub username.

If you have received no comments on your PR for a week, you can request a review by "ping"ing the PR by adding a comment “Ping”. The common courtesy "ping" rate is once a week. Please remember that you are asking for valuable time from other developers.

If you have further questions, they may be answered by the LLVM GitHub User Guide.

You can also ask questions in a comment on this PR, on the LLVM Discord or on the forums.

@llvmbot

llvmbot commented Feb 28, 2026

Copy link
Copy Markdown
Member

@llvm/pr-subscribers-lld-elf

Author: Shivam Gupta (xgupta)

Changes

With PR#155776, clang started to emit R_AARCH64_TLS_DTPREL64 in .debug_info section to help in debugging TLS variables. This patch helps recognise that relocation in LLD linker.


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

2 Files Affected:

  • (modified) lld/ELF/Arch/AArch64.cpp (+6)
  • (added) lld/test/ELF/aarch64-tls-dtprel.s (+41)
diff --git a/lld/ELF/Arch/AArch64.cpp b/lld/ELF/Arch/AArch64.cpp
index f85a3f48f2183..c329e653b9853 100644
--- a/lld/ELF/Arch/AArch64.cpp
+++ b/lld/ELF/Arch/AArch64.cpp
@@ -156,6 +156,8 @@ RelExpr AArch64::getRelExpr(RelType type, const Symbol &s,
   case R_AARCH64_PREL32:
   case R_AARCH64_PREL64:
     return R_PC;
+  case R_AARCH64_TLS_DTPREL64:
+    return R_DTPREL;
   case R_AARCH64_NONE:
     return R_NONE;
   default:
@@ -649,6 +651,10 @@ void AArch64::relocate(uint8_t *loc, const Relocation &rel,
     checkInt(ctx, loc, val, 32, rel);
     write32(ctx, loc, val);
     break;
+  case R_AARCH64_TLS_DTPREL64:
+    checkInt(ctx, loc, val, 64, rel);
+    write64(ctx, loc, val);
+    break;
   case R_AARCH64_ADD_ABS_LO12_NC:
   case R_AARCH64_AUTH_GOT_ADD_LO12_NC:
     write32Imm12(loc, val);
diff --git a/lld/test/ELF/aarch64-tls-dtprel.s b/lld/test/ELF/aarch64-tls-dtprel.s
new file mode 100644
index 0000000000000..49c1ac1456354
--- /dev/null
+++ b/lld/test/ELF/aarch64-tls-dtprel.s
@@ -0,0 +1,41 @@
+# REQUIRES: aarch64
+# RUN: llvm-mc -filetype=obj -triple=aarch64-linux-gnu %s -o %t.o
+# RUN: llvm-readobj -r %t.o | FileCheck %s
+# RUN: ld.lld %t.o -o %t
+
+# CHECK:      .rela.debug_info {
+# CHECK-NEXT:   0x6 R_AARCH64_ABS32 .debug_abbrev 0x0
+# CHECK-NEXT:   0xD R_AARCH64_TLS_DTPREL64 var 0x0
+# CHECK-NEXT: }
+
+.section .tdata,"awT",@progbits
+.globl var
+var:
+  .word 0
+
+.section .debug_abbrev,"",@progbits
+.byte 1                  // Abbreviation Code
+.byte 17                 // DW_TAG_compile_unit
+.byte 1                  // DW_CHILDREN_yes
+.byte 0                  // EOM(1)
+.byte 0                  // EOM(2)
+
+.byte 2                  // Abbreviation Code
+.byte 52                 // DW_TAG_variable
+.byte 0                  // DW_CHILDREN_no
+.byte 2;                 // DW_AT_location
+.byte 24                 // DW_FORM_exprloc
+.byte 0                  // EOM(1)
+.byte 0                  // EOM(2)
+
+.section        .debug_info,"",@progbits
+.Lcu_begin0:
+  .word .Lcu_end - .Lcu_body // Length of Unit
+.Lcu_body:
+  .hword 4               // DWARF version number
+  .word   .debug_abbrev  // Offset Into Abbrev. Section
+  .byte   8              // Address Size (in bytes)
+  .byte   1              // Abbrev [1] DW_TAG_compile_unit
+  .byte   2              // Abbrev [2] DW_TAG_variable
+  .xword  %dtprel(var)
+.Lcu_end:

@llvmbot

llvmbot commented Feb 28, 2026

Copy link
Copy Markdown
Member

@llvm/pr-subscribers-lld

Author: Shivam Gupta (xgupta)

Changes

With PR#155776, clang started to emit R_AARCH64_TLS_DTPREL64 in .debug_info section to help in debugging TLS variables. This patch helps recognise that relocation in LLD linker.


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

2 Files Affected:

  • (modified) lld/ELF/Arch/AArch64.cpp (+6)
  • (added) lld/test/ELF/aarch64-tls-dtprel.s (+41)
diff --git a/lld/ELF/Arch/AArch64.cpp b/lld/ELF/Arch/AArch64.cpp
index f85a3f48f2183..c329e653b9853 100644
--- a/lld/ELF/Arch/AArch64.cpp
+++ b/lld/ELF/Arch/AArch64.cpp
@@ -156,6 +156,8 @@ RelExpr AArch64::getRelExpr(RelType type, const Symbol &s,
   case R_AARCH64_PREL32:
   case R_AARCH64_PREL64:
     return R_PC;
+  case R_AARCH64_TLS_DTPREL64:
+    return R_DTPREL;
   case R_AARCH64_NONE:
     return R_NONE;
   default:
@@ -649,6 +651,10 @@ void AArch64::relocate(uint8_t *loc, const Relocation &rel,
     checkInt(ctx, loc, val, 32, rel);
     write32(ctx, loc, val);
     break;
+  case R_AARCH64_TLS_DTPREL64:
+    checkInt(ctx, loc, val, 64, rel);
+    write64(ctx, loc, val);
+    break;
   case R_AARCH64_ADD_ABS_LO12_NC:
   case R_AARCH64_AUTH_GOT_ADD_LO12_NC:
     write32Imm12(loc, val);
diff --git a/lld/test/ELF/aarch64-tls-dtprel.s b/lld/test/ELF/aarch64-tls-dtprel.s
new file mode 100644
index 0000000000000..49c1ac1456354
--- /dev/null
+++ b/lld/test/ELF/aarch64-tls-dtprel.s
@@ -0,0 +1,41 @@
+# REQUIRES: aarch64
+# RUN: llvm-mc -filetype=obj -triple=aarch64-linux-gnu %s -o %t.o
+# RUN: llvm-readobj -r %t.o | FileCheck %s
+# RUN: ld.lld %t.o -o %t
+
+# CHECK:      .rela.debug_info {
+# CHECK-NEXT:   0x6 R_AARCH64_ABS32 .debug_abbrev 0x0
+# CHECK-NEXT:   0xD R_AARCH64_TLS_DTPREL64 var 0x0
+# CHECK-NEXT: }
+
+.section .tdata,"awT",@progbits
+.globl var
+var:
+  .word 0
+
+.section .debug_abbrev,"",@progbits
+.byte 1                  // Abbreviation Code
+.byte 17                 // DW_TAG_compile_unit
+.byte 1                  // DW_CHILDREN_yes
+.byte 0                  // EOM(1)
+.byte 0                  // EOM(2)
+
+.byte 2                  // Abbreviation Code
+.byte 52                 // DW_TAG_variable
+.byte 0                  // DW_CHILDREN_no
+.byte 2;                 // DW_AT_location
+.byte 24                 // DW_FORM_exprloc
+.byte 0                  // EOM(1)
+.byte 0                  // EOM(2)
+
+.section        .debug_info,"",@progbits
+.Lcu_begin0:
+  .word .Lcu_end - .Lcu_body // Length of Unit
+.Lcu_body:
+  .hword 4               // DWARF version number
+  .word   .debug_abbrev  // Offset Into Abbrev. Section
+  .byte   8              // Address Size (in bytes)
+  .byte   1              // Abbrev [1] DW_TAG_compile_unit
+  .byte   2              // Abbrev [2] DW_TAG_variable
+  .xword  %dtprel(var)
+.Lcu_end:

@github-actions

github-actions Bot commented Feb 28, 2026 •

Copy link
Copy Markdown

🐧 Linux x64 Test Results

  • 3786 tests passed
  • 74 tests skipped

✅ The build succeeded and all tests passed.

@github-actions

github-actions Bot commented Feb 28, 2026 •

Copy link
Copy Markdown

🪟 Windows x64 Test Results

  • 3118 tests passed
  • 61 tests skipped

✅ The build succeeded and all tests passed.

@MaskRay MaskRay left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Needs to wait for the assembler %dtprel() to land first

Comment thread lld/ELF/Arch/AArch64.cpp Outdated
Comment thread lld/test/ELF/aarch64-tls-dtprel.s Outdated
@jrtc27

jrtc27 commented Mar 1, 2026

Copy link
Copy Markdown
Contributor

I'm curious about this being pushed forward despite #146572 (comment)

@MaskRay

MaskRay commented Mar 1, 2026

Copy link
Copy Markdown
Member

I'm curious about this being pushed forward despite #146572 (comment)

@jrtc27
I've made some investigation. #83466 (comment) While GDB likely uses this information, the proposer hasn't yet proven it’s essential and what's missing without DW_OP_form_tls_address. The full story for TLS debugging remains unclear. So this lld/ELF patch is definitely not ready.

@xgupta

xgupta commented Mar 2, 2026

Copy link
Copy Markdown
Contributor Author

Okay, we will first try for lldb, why it is necessary to have this relocation.

Without this relocation in clang for test case
thread_local unsigned long v = 1; int main() { return v; }

llvm-dwarfdump shows

0x00000023:   DW_TAG_variable
                DW_AT_name      ("v")
                DW_AT_type      (0x0000002b "unsigned long")
                DW_AT_external  (true)
                DW_AT_decl_file ("/llvm-project/build/test.cpp")
                DW_AT_decl_line (1)

So it is missing the DW_AT_location attribute. New change in clang emitted it. Which that help, LLDB debug the variable. Without the location even after LLDB fix in #183993, LLDB could not use symbol table.

(lldb) p v
error: Couldn't apply expression side effects : Couldn't dematerialize a result variable: couldn't read its memory

@xgupta

xgupta commented Mar 2, 2026

Copy link
Copy Markdown
Contributor Author

Also at the end of https://sourceware.org/bugzilla/show_bug.cgi?id=27886, Tom de Vries prove that only symbol table approach not work for static TLS variables, it need DW_AT_location.

@smithp35

smithp35 commented Mar 2, 2026

Copy link
Copy Markdown
Contributor

If it helps the original ABI issue ARM-software/abi-aa#176 took inspiration from x86_64 which emits R_X86_64_DTPOFF64 against a DWARF location DW_AT_location (DW_OP_const8u 0x0, DW_OP_form_tls_address)

Our GNU community recieved a request for something similar (assembler expression) https://sourceware.org/bugzilla/show_bug.cgi?id=28351 which claims as a pre-requisite for GDB support.

I tried to see if I could examine the location of a TLS variable on an AArch64 in qemu-user mode without DW_AT_location (works on x86), but it seems like qemu user mode doesn't implement enough of the remote protocol. If I get time this evening I'll try on hardware.

@xgupta

xgupta commented Mar 6, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @smithp35 for testing this. Let me know if you have been succeeded in examine the location of TLS variable without DW_AT_location.

@smithp35

smithp35 commented Mar 6, 2026

Copy link
Copy Markdown
Contributor

Thanks @smithp35 for testing this. Let me know if you have been succeeded in examine the location of TLS variable without DW_AT_location.

Apologies for the delay. Happen to be working from home today so I've been able to check on a Raspberry Pi.

I can reproduce the behaviour in https://sourceware.org/bugzilla/show_bug.cgi?id=28351 and on my own examples. The DW_OP_form_tls_address expression is needed for static TLS variables (claim to be optimized out) but not for global TLS variables. GDB seems to be able to find those without the expression.

There may be an advantage in clang not using the Dwarf expression for preemptible global TLS variables. I note that the definition of the DW_OP_form_tls_address will always give the offset of the variable from the "modules" TLS block which if the symbol is preempted will give the wrong value. I can confirm that with an example in gdb.

@xgupta

xgupta commented Mar 7, 2026

Copy link
Copy Markdown
Contributor Author

Apologies for the delay. Happen to be working from home today so I've been able to check on a Raspberry Pi.

No worries, thank you very much for taking time to check on your system.

I can reproduce the behaviour in https://sourceware.org/bugzilla/show_bug.cgi?id=28351 and on my own examples. The DW_OP_form_tls_address expression is needed for static TLS variables (claim to be optimized out) but not for global TLS variables. GDB seems to be able to find those without the expression.

There may be an advantage in clang not using the Dwarf expression for preemptible global TLS variables. I note that the definition of the DW_OP_form_tls_address will always give the offset of the variable from the "modules" TLS block which if the symbol is preempted will give the wrong value. I can confirm that with an example in gdb.

So I have updated the corresponding clang patch to conditionally emit DW_OP_form_tls_address(with DW_AT_location). I am able to debug it with gdb but it seems something is missing on lldb side so it is not able to debug without DW_AT_location. But I hope that should not block clang and lld patches which are doing things according to ABI.

@avikivity

Copy link
Copy Markdown
Contributor

I am able to debug it with gdb

Is this gdb with no further patches?

@xgupta

xgupta commented Mar 8, 2026 •

Copy link
Copy Markdown
Contributor Author

I am able to debug it with gdb

Is this gdb with no further patches?

Yes with the test case given at end of https://sourceware.org/bugzilla/show_bug.cgi?id=27886.

gdb and g++

Breakpoint 1, foo () at test-2.c:6
6         a::v1.x++;
(gdb) n
7         a::v1.y++;
(gdb) p a::v1 
$1 = <optimized out>

and now with gdb and clang++(updated with DW_AT_location) I have

Breakpoint 1, foo () at test-2.c:6
6         a::v1.x++;
(gdb) n
7         a::v1.y++;
(gdb) p a::v1 
$1 = {x = 2, y = 2, z = 3}

which was before printing in bug report as
p a::v1
$1 = {x = 1, y = 2, z = 3}

With lldb and clang++(update) I still have same

Process 5523 stopped
* thread #1, name = 'test123', stop reason = breakpoint 1.1
      frame #0: 0x0000aaaacd5a0acc test123`foo() at test-2.c:6:3
   3    void
   4    foo (void)
   5    {
-> 6      a::v1.x++;
   7      a::v1.y++;
   8      a::v1.z++;
   9    }
(lldb) n
Process 5523 stopped
* thread #1, name = 'test123', stop reason = step over
      frame #0: 0x0000aaaacd5a0adc test123`foo() at test-2.c:7:3
   4    foo (void)
   5    {
   6      a::v1.x++;
-> 7      a::v1.y++;
   8      a::v1.z++;
   9    }
(lldb) p a::v1 
(kk)  (x = 1, y = 2, z = 3)

With preemptible global variable v(without DW_AT_location) -
$ cat main.cpp

#include <stdio.h>

extern "C" unsigned long get_v();

int main() {
    printf("%lu\n", get_v());
    return 0;
}

$ cat lib.cpp

thread_local unsigned long v = 1;

extern "C" unsigned long get_v() {
    return v;
}

With lldb and clang++

* thread #1, name = 'test1', stop reason = breakpoint 1.1
      frame #0: 0x0000aaaae16007ec test1`main at main.cpp:6:21
   3    extern "C" unsigned long get_v();
   4   
   5    int main() {
-> 6        printf("%lu\n", get_v());
   7        return 0;
   8    }
(lldb) p v
         
error: Couldn't apply expression side effects : Couldn't dematerialize a result variable: couldn't read its memory
(lldb) p get_v()
(unsigned long) 1

with gdb and clang++

Breakpoint 1, main () at main.cpp:6
6           printf("%lu\n", get_v());
(gdb) p v
$1 = 1
(gdb) p get_v()
$2 = 1

@avikivity

Copy link
Copy Markdown
Contributor

Thanks.

@xgupta

xgupta commented Mar 10, 2026 •

Copy link
Copy Markdown
Contributor Author

There may be an advantage in clang not using the Dwarf expression for preemptible global TLS variables. I note that the definition of the DW_OP_form_tls_address will always give the offset of the variable from the "modules" TLS block which if the symbol is preempted will give the wrong value. I can confirm that with an example in gdb.

Hi @smithp35 can you please share the example when it gets wrong, someone object to adding the check that forbid emitting DW_AT_location on clang patch.

@smithp35

Copy link
Copy Markdown
Contributor

I'm assuming that "going wrong" is what happens when the symbol is pre-empted. You will need to run this on x86_64 or some other target that uses the DW_OP_form_tls_address expression.

//tlslib.c
__thread int x = 10; // will be preempted by definition of x in tlsmain.c
int func(void) {
return x;
}

//tlsmain.c
#include <stdio.h>
__thread int x = 20; // preempt definition of x in tlslib.c
extern int func(void);
int main(void) {
printf("%d %d\n", x, func());
}

clang tlslib.c -fpic --shared -o tlslib.so -g
clang tlsmain.c -g tlslib.so -o tlsmain -rpath $(pwd)

././tlsmain
20 20

gdb tlsmain
GNU gdb (Ubuntu 15.0.50.20240403-0ubuntu1) 15.0.50.20240403-git
Copyright (C) 2024 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later http://gnu.org/licenses/gpl.html
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
https://www.gnu.org/software/gdb/bugs/.
Find the GDB manual and other documentation resources online at:
http://www.gnu.org/software/gdb/documentation/.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from tlsmain...
(gdb) break func
Breakpoint 1 at 0x1040
(gdb) run
Starting program: /data_nvme0n1/work/scratch/tlsmain
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 1, func () at tlslib.c:4
4 return x;
(gdb) print /x x
$1 = 0xa

Note that the value of x is 20, but the debugger prints 0xa (10) when we ask for the value of the variable. By the definition of DW_OP_form_tls_address the debugger will access the value of x within the shared libraries TLS block, which is indeed 0xa.

I found that gdb for AArch64 without DW_OP_form_tls_address was able to use the dynamic symbol table to find the pre-empted definition of x.

It may that various debuggers can't handle the mix of DW_OP_form_tls_address and symbol table. I've only tested on gdb.

Thinking about this from first principles pre-emptible symbol isn't ideal either, if we were to be precise it would be "symbol is in the dynamic symbol table", and that is very difficult for the compiler to know. In the example above the symbol x in tlsmain.c would get exported into the dynamic symbol table because tlslib.so also defines it (and it may be pre-empted by it). If I add another global __thread int y; to tlsmain.c that isn't accessed by any shared-library then the linker does not export y into the dynamic symbol table so the debugger wouldn't be able to look it up.

I think this points to either always using DW_OP_form_tls_address or never. When it is used we will have to live with the symbol pre-emption not being respected.

@xgupta

xgupta commented Mar 10, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @smithp35 for clearing the things. I feel could keep preemptible check(also Aarch64) and let debugger handle that case with dynamic symbol table. GDB is doing it so LLDB can also do it in future.

Or maybe as in #146572 (comment) it is being point out that it is not a very common user case to override TLS variables so emit in all cases and let debuggers handle it in whichever way it can/want.

WDTY? Which approach to choose.

(And sorry it created a noise when this discussion should be on LLVM side patch.)

@jrtc27

jrtc27 commented Mar 10, 2026

Copy link
Copy Markdown
Contributor

Perhaps a stupid question: why does preempting normal globals not hit the same issue with debug info?

@smithp35

Copy link
Copy Markdown
Contributor

Perhaps a stupid question: why does preempting normal globals not hit the same issue with debug info?

I think in this case it comes from how the DW_OP_form_tls_address is defined in DWARF. I think other DWARF expressions interact with the symbol table, or are more precise. There is some commentary in the spec for DW_OP_form_tls_address

Computing the address of the appropriate block can be complex (in some cases, the compiler emits a function call to do it), and difficult to describe using ordinary DWARF location descriptions. Instead of forcing complex thread-local storage calculations into the DWARF expressions, the DW_OP_form_tls_address allows the consumer to perform the computation based on the run-time environment.

I'm no expert in that area so there's probably a better answer to be found.

@smithp35

Copy link
Copy Markdown
Contributor

Thanks @smithp35 for clearing the things. I feel could keep preemptible check(also Aarch64) and let debugger handle that case with dynamic symbol table. GDB is doing it so LLDB can also do it in future.

Or maybe as in #146572 (comment) it is being point out that it is not a very common user case to override TLS variables so emit in all cases and let debuggers handle it in whichever way it can/want.

WDTY? Which approach to choose.

(And sorry it created a noise when this discussion should be on LLVM side patch.)

Given that there are cases when the compiler can't know at compile time if the linker will put a symbol in the dynamic symbol table (exporting symbols from executables only when shared-libraries reference them), I think it would be best to keep it simple and always use the DW_OP_form_tls_address .

Comment thread lld/test/ELF/aarch64-tls-dtprel.s
Comment thread lld/test/ELF/aarch64-tls-dtprel.s Outdated
@MaskRay

MaskRay commented Mar 12, 2026

Copy link
Copy Markdown
Member

With PR #146572, clang started to emit R_AARCH64_TLS_DTPREL64 in .debug_info section for variables to help in debugging TLS variables. This patch helps recognise that relocation in LLD linker.

This is out of order. The lld patch must be merged before Clang begins emitting relocations that would trigger an error. Similarly, ensure GNU ld supports R_AARCH64_TLS_DTPREL64 before updating Clang.

Clang and lld versions are usually synchronized, but we need to support very old GNU ld. Once GNU ld support is upstreamed we could gate this emission under -fbinutils-version. However, I’m still concerned about the Clang change; can the debugger use st_value from the ELF symbol table as a alternative approach?

@igorkudrin

Copy link
Copy Markdown
Contributor

However, I’m still concerned about the Clang change; can the debugger use st_value from the ELF symbol table as a alternative approach?

It can, but this is sub-optimal. Some thread-local variables do not have entries in the symbol table, like ones inside functions. Others may have entries with the same symbol names, for example, when similar variables are declared in anonymous namespaces in different files. Only the compiler, together with the linker, can provide accurate location information.

@MaskRay

MaskRay commented Mar 12, 2026

Copy link
Copy Markdown
Member

However, I’m still concerned about the Clang change; can the debugger use st_value from the ELF symbol table as a alternative approach?

It can, but this is sub-optimal. Some thread-local variables do not have entries in the symbol table, like ones inside functions.

Do you have an example that a thread-locaal variable doesn't have a symbol table entry?

TLS relocations cannot be adjusted to be against the section symbols, so all STT_TLS symbols are retained.

int foo() {
  thread_local int x = 0;
  return ++x;
}

[[gnu::noinline]] static int loc() {
  thread_local int x = 0;
  return ++x;
}
int use_loc() { return loc(); }

While an executable can have multiple local symbols of the same name, local symbols are grouped starting with a STT_FILE symbol, which could be used to resolve ambiguity.

Others may have entries with the same symbol names, for example, when similar variables are declared in anonymous namespaces in different files. Only the compiler, together with the linker, can provide accurate location information.

Internal linkage TLS variables still lower to local symbols. There is no lost information.

Perhaps inaccurate location information happens with struct member access (struct A { int a, b; };).

@smithp35

Copy link
Copy Markdown
Contributor

However, I’m still concerned about the Clang change; can the debugger use st_value from the ELF symbol table as a alternative approach?

It can, but this is sub-optimal. Some thread-local variables do not have entries in the symbol table, like ones inside functions.

Do you have an example that a thread-locaal variable doesn't have a symbol table entry?

TLS relocations cannot be adjusted to be against the section symbols, so all STT_TLS symbols are retained.

int foo() {
  thread_local int x = 0;
  return ++x;
}

[[gnu::noinline]] static int loc() {
  thread_local int x = 0;
  return ++x;
}
int use_loc() { return loc(); }

While an executable can have multiple local symbols of the same name, local symbols are grouped starting with a STT_FILE symbol, which could be used to resolve ambiguity.

Others may have entries with the same symbol names, for example, when similar variables are declared in anonymous namespaces in different files. Only the compiler, together with the linker, can provide accurate location information.

Internal linkage TLS variables still lower to local symbols. There is no lost information.

Perhaps inaccurate location information happens with struct member access (struct A { int a, b; };).

From looking up what GDB does, there seems to be a difference in how variables that are described as DW_AT_external and those that aren't. If there is no location information in dwarf for DW_AT_external = false (directive omitted), then gdb assigns LOC_OPTIMIZED_OUT and drops all information about it. For DW_AT_external these are assigned LOC_UNRESOLVED and there is an attempt to read the symbol table, which can work out that the symbol is TLS.

Not entirely sure why this is, possibly because only global symbols are guaranteed to have unique names in the ELF files symbol table, whereas the debugger wouldn't necessarily have enough information to find the object file.

@igorkudrin

igorkudrin commented Mar 13, 2026 •

Copy link
Copy Markdown
Contributor

Do you have an example that a thread-locaal variable doesn't have a symbol table entry?

While an executable can have multiple local symbols of the same name, local symbols are grouped starting with a STT_FILE symbol, which could be used to resolve ambiguity.

Internal linkage TLS variables still lower to local symbols. There is no lost information.

Perhaps inaccurate location information happens with struct member access (struct A { int a, b; };).

You are right, they have symtab entries. However, as compilers often do not generate the DW_AT_linkage_name for these variables, the debugger would need to calculate it itself, which might be a really complex task:

  • The symbol name has to be reconstructed, taking into account whether the variable is declared inside a subroutine, namespace, or CU.
  • There may be several variables with the same name in a function, so a suffix should be added. Or maybe not, if some of these variables are automatic and do not have symtab entries.
  • There may be variables with the same name with internal linkage, so their symtab entries will also have the same names. The debugger can try to track such variables, assuming the order in the symbol table matches the debug info; this is not guaranteed because the linker can rearrange input sections.
  • As the symbol table is not required for running a program, it can be stripped, totally or partially.

Knowing the linkage name can be beneficial for global variables that can be interposed. But for the basic scenario, when the debugger simply needs to find the location of the variable, it is overcomplicated. The symtab entry contains the offset of the variable in the module's TLS data, and exactly the same value is stored in the DW_AT_location attribute of the variable's DIE, followed by DW_OP_GNU_push_tls_address / DW_OP_form_tls_address. It is much easier for the debugger to simply read the location expression from the variable's DIE attribute than to take the complex approach.

@igorkudrin

igorkudrin commented Mar 13, 2026 •

Copy link
Copy Markdown
Contributor

Perhaps a stupid question: why does preempting normal globals not hit the same issue with debug info?

Your question is absolutely legit. lldb does not consider preempting, while gdb does it for normal (not-TLS) globals.

> cat app.c
int var = 11;
int get_dso_var();
int main() {
  int var_from_dso = get_dso_var();
}
> cat lib.c
int var = 22;
int get_dso_var() { return var; }
> clang -g -O0 -fpic -c app.c -o app.o
> clang -g -O0 -fpic -c lib.c -o lib.o
> clang -shared lib.o -o lib.so
> clang app.o ./lib.so -o app.out
> lldb -v
lldb version 21.1.8
> lldb app.out
(lldb) br set -n get_dso_var
(lldb) r
-> 2    int get_dso_var() { return var; }
(lldb) p var
(int) 22
(lldb) n
-> 4      int var_from_dso = get_dso_var();
(lldb) p var
(int) 11
(lldb) q
> gdb -v
GNU gdb (Debian 17.1-3) 17.1
> gdb app.out
(gdb) br get_dso_var
(gdb) r
Breakpoint 1, get_dso_var () at ../lib.c:2
2       int get_dso_var() { return var; }
(gdb) p var
$1 = 11
(gdb) n
main () at ../app.c:5
5       }
(gdb) p var
$2 = 11
(gdb) q

@MaskRay

MaskRay commented Mar 13, 2026

Copy link
Copy Markdown
Member

Do you have an example that a thread-locaal variable doesn't have a symbol table entry?
While an executable can have multiple local symbols of the same name, local symbols are grouped starting with a STT_FILE symbol, which could be used to resolve ambiguity.
Internal linkage TLS variables still lower to local symbols. There is no lost information.
Perhaps inaccurate location information happens with struct member access (struct A { int a, b; };).

You are right, they have symtab entries. However, as compilers often do not generate the DW_AT_linkage_name for these variables, the debugger would need to calculate it itself, which might be a really complex task:

  • The symbol name has to be reconstructed, taking into account whether the variable is declared inside a subroutine, namespace, or CU.
  • There may be several variables with the same name in a function, so a suffix should be added. Or maybe not, if some of these variables are automatic and do not have symtab entries.
  • There may be variables with the same name with internal linkage, so their symtab entries will also have the same names. The debugger can try to track such variables, assuming the order in the symbol table matches the debug info; this is not guaranteed because the linker can rearrange input sections.
  • As the symbol table is not required to run a program, it can be stripped, totally or partially.

Knowing the linkage name can be beneficial for global variables that can be interposed. But for the basic scenario, when the debugger simply needs to find the location of the variable, it is overcomplicated. The symtab entry contains the offset of the variable in the module's TLS data, and exactly the same value is stored in the DW_AT_location attribute of the variable's DIE, followed by DW_OP_GNU_push_tls_address / DW_OP_form_tls_address. It is so much easier for the debugger to just read the location expression for the corresponding attribute of the variable's DIE, than to try to acquire the same information through the symtab.

Thanks for the explanation!

@xgupta

xgupta commented Mar 14, 2026 •

Copy link
Copy Markdown
Contributor Author

With PR #146572, clang started to emit R_AARCH64_TLS_DTPREL64 in .debug_info section for variables to help in debugging TLS variables. This patch helps recognise that relocation in LLD linker.

This is out of order. The lld patch must be merged before Clang begins emitting relocations that would trigger an error. Similarly, ensure GNU ld supports R_AARCH64_TLS_DTPREL64 before updating Clang.

Clang and lld versions are usually synchronized, but we need to support very old GNU ld. Once GNU ld support is upstreamed we could gate this emission under -fbinutils-version.

Updated the PR description and also sent a GNU patch(with gas, ld and gold) -https://sourceware.org/pipermail/binutils/2026-March/148550.html.

xgupta added 3 commits March 19, 2026 03:06
With PR#155776, clang started to emit R_AARCH64_TLS_DTPREL64 in
.debug_info section to help in debugging TLS variables. This patch
help recognise that relocation in LLD linker.
@xgupta

xgupta commented Mar 18, 2026

Copy link
Copy Markdown
Contributor Author

Hi @MaskRay, I rebased the branch since Assembler side of patch is committed. Had a question, does this feature require GNU support first? Or it can be submitted since the user have to pass clang --aarch64-emit-debug-tls-location option to generate this relocation so they know what they are doing.

I observed from archive GNU/binutils patches takes longer time to get reviewed and commit...

@xgupta

xgupta commented Mar 23, 2026

Copy link
Copy Markdown
Contributor Author

Gentle Ping!

@xgupta

xgupta commented Mar 30, 2026

Copy link
Copy Markdown
Contributor Author

Gentle Ping!

Can we merge this now? So other two PRs(llc and llvm-dwarfdump) can be merge. llvm-mc support already merged.

@xgupta
xgupta merged commit 14ce208 into llvm:main Mar 31, 2026
10 checks passed
@github-actions

Copy link
Copy Markdown

@xgupta Congratulations on having your first Pull Request (PR) merged into the LLVM Project!

Your changes will be combined with recent changes from other authors, then tested by our build bots. If there is a problem with a build, you may receive a report in an email or a comment on this PR.

Please check whether problems have been caused by your change specifically, as the builds can include changes from many authors. It is not uncommon for your change to be included in a build that fails due to someone else's changes, or infrastructure issues.

How to do this, and the rest of the post-merge process, is covered in detail here.

If your change does cause a problem, it may be reverted, or you can revert it yourself. This is a normal part of LLVM development. You can fix your changes and open a new PR to merge them again.

If you don't get any reports, no action is required from you. Your changes are working as expected, well done!

@llvm-ci

llvm-ci commented Mar 31, 2026

Copy link
Copy Markdown

LLVM Buildbot has detected a new failure on builder sanitizer-x86_64-linux-fast running on sanitizer-buildbot3 while building lld at step 2 "annotate".

Full details are available at: https://lab.llvm.org/buildbot/#/builders/169/builds/21454

Here is the relevant piece of the build log for the reference
Step 2 (annotate) failure: 'python ../sanitizer_buildbot/sanitizers/zorg/buildbot/builders/sanitizers/buildbot_selector.py' (failure)
...
llvm-lit: /home/b/sanitizer-x86_64-linux-fast/build/llvm-project/llvm/utils/lit/lit/llvm/config.py:569: note: using lld-link: /home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/bin/lld-link
llvm-lit: /home/b/sanitizer-x86_64-linux-fast/build/llvm-project/llvm/utils/lit/lit/llvm/config.py:569: note: using ld64.lld: /home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/bin/ld64.lld
llvm-lit: /home/b/sanitizer-x86_64-linux-fast/build/llvm-project/llvm/utils/lit/lit/llvm/config.py:569: note: using wasm-ld: /home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/bin/wasm-ld
llvm-lit: /home/b/sanitizer-x86_64-linux-fast/build/llvm-project/llvm/utils/lit/lit/llvm/config.py:569: note: using ld.lld: /home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/bin/ld.lld
llvm-lit: /home/b/sanitizer-x86_64-linux-fast/build/llvm-project/llvm/utils/lit/lit/llvm/config.py:569: note: using lld-link: /home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/bin/lld-link
llvm-lit: /home/b/sanitizer-x86_64-linux-fast/build/llvm-project/llvm/utils/lit/lit/llvm/config.py:569: note: using ld64.lld: /home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/bin/ld64.lld
llvm-lit: /home/b/sanitizer-x86_64-linux-fast/build/llvm-project/llvm/utils/lit/lit/llvm/config.py:569: note: using wasm-ld: /home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/bin/wasm-ld
llvm-lit: /home/b/sanitizer-x86_64-linux-fast/build/llvm-project/llvm/utils/lit/lit/main.py:74: note: The test suite configuration requested an individual test timeout of 0 seconds but a timeout of 900 seconds was requested on the command line. Forcing timeout to be 900 seconds.
-- Testing: 97785 tests, 64 workers --
Testing:  0.. 10.. 20.. 30.. 40.. 50.. 60.. 70.. 80.. 
FAIL: LLVM :: CodeGen/X86/basic-block-sections-clusters-bb-hash.ll (30810 of 97785)
******************** TEST 'LLVM :: CodeGen/X86/basic-block-sections-clusters-bb-hash.ll' FAILED ********************
Exit Code: 1

Command Output (stdout):
--
# RUN: at line 11
/home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/bin/llc /home/b/sanitizer-x86_64-linux-fast/build/llvm-project/llvm/test/CodeGen/X86/basic-block-sections-clusters-bb-hash.ll -O0 -mtriple=x86_64-pc-linux -function-sections -filetype=obj -basic-block-address-map -emit-bb-hash -o /home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/test/CodeGen/X86/Output/basic-block-sections-clusters-bb-hash.ll.tmp.o
# executed command: /home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/bin/llc /home/b/sanitizer-x86_64-linux-fast/build/llvm-project/llvm/test/CodeGen/X86/basic-block-sections-clusters-bb-hash.ll -O0 -mtriple=x86_64-pc-linux -function-sections -filetype=obj -basic-block-address-map -emit-bb-hash -o /home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/test/CodeGen/X86/Output/basic-block-sections-clusters-bb-hash.ll.tmp.o
# note: command had no output on stdout or stderr
# RUN: at line 16
echo 'v1' > /home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/test/CodeGen/X86/Output/basic-block-sections-clusters-bb-hash.ll.tmp1
# executed command: echo v1
# note: command had no output on stdout or stderr
# RUN: at line 17
echo 'f foo' >> /home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/test/CodeGen/X86/Output/basic-block-sections-clusters-bb-hash.ll.tmp1
# executed command: echo 'f foo'
# note: command had no output on stdout or stderr
# RUN: at line 18
echo 'g 0:100,1:100,2:0 1:100,3:100 2:0,3:0 3:100' >> /home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/test/CodeGen/X86/Output/basic-block-sections-clusters-bb-hash.ll.tmp1
# executed command: echo 'g 0:100,1:100,2:0 1:100,3:100 2:0,3:0 3:100'
# note: command had no output on stdout or stderr
# RUN: at line 22
/home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/bin/llvm-readobj /home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/test/CodeGen/X86/Output/basic-block-sections-clusters-bb-hash.ll.tmp.o --bb-addr-map |  awk 'BEGIN {printf "h"}      /ID: [0-9]+/ {id=$2}      /Hash: 0x[0-9A-Fa-f]+/ {gsub(/^0x/, "", $2); hash=$2; printf " %s:%s", id, hash}      END {print ""}'  >> /home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/test/CodeGen/X86/Output/basic-block-sections-clusters-bb-hash.ll.tmp1
# executed command: /home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/bin/llvm-readobj /home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/test/CodeGen/X86/Output/basic-block-sections-clusters-bb-hash.ll.tmp.o --bb-addr-map
# note: command had no output on stdout or stderr
# executed command: awk 'BEGIN {printf "h"}      /ID: [0-9]+/ {id=$2}      /Hash: 0x[0-9A-Fa-f]+/ {gsub(/^0x/, "", $2); hash=$2; printf " %s:%s", id, hash}      END {print ""}'
# note: command had no output on stdout or stderr
# RUN: at line 29
/home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/bin/llc < /home/b/sanitizer-x86_64-linux-fast/build/llvm-project/llvm/test/CodeGen/X86/basic-block-sections-clusters-bb-hash.ll -O0 -mtriple=x86_64-pc-linux -function-sections -basic-block-sections=/home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/test/CodeGen/X86/Output/basic-block-sections-clusters-bb-hash.ll.tmp1 -basic-block-section-match-infer |  /home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/bin/FileCheck /home/b/sanitizer-x86_64-linux-fast/build/llvm-project/llvm/test/CodeGen/X86/basic-block-sections-clusters-bb-hash.ll -check-prefixes=CHECK,LINUX-SECTIONS1
# executed command: /home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/bin/llc -O0 -mtriple=x86_64-pc-linux -function-sections -basic-block-sections=/home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/test/CodeGen/X86/Output/basic-block-sections-clusters-bb-hash.ll.tmp1 -basic-block-section-match-infer
# note: command had no output on stdout or stderr
# executed command: /home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/bin/FileCheck /home/b/sanitizer-x86_64-linux-fast/build/llvm-project/llvm/test/CodeGen/X86/basic-block-sections-clusters-bb-hash.ll -check-prefixes=CHECK,LINUX-SECTIONS1
# .---command stderr------------
# | /home/b/sanitizer-x86_64-linux-fast/build/llvm-project/llvm/test/CodeGen/X86/basic-block-sections-clusters-bb-hash.ll:80:26: error: LINUX-SECTIONS1-LABEL: expected string not found in input
# | ; LINUX-SECTIONS1-LABEL: # %bb.1:
# |                          ^
# | <stdin>:7:5: note: scanning from here
# | foo: # @foo
Step 13 (stage2/msan check) failure: stage2/msan check (failure)
...
llvm-lit: /home/b/sanitizer-x86_64-linux-fast/build/llvm-project/llvm/utils/lit/lit/llvm/config.py:569: note: using lld-link: /home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/bin/lld-link
llvm-lit: /home/b/sanitizer-x86_64-linux-fast/build/llvm-project/llvm/utils/lit/lit/llvm/config.py:569: note: using ld64.lld: /home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/bin/ld64.lld
llvm-lit: /home/b/sanitizer-x86_64-linux-fast/build/llvm-project/llvm/utils/lit/lit/llvm/config.py:569: note: using wasm-ld: /home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/bin/wasm-ld
llvm-lit: /home/b/sanitizer-x86_64-linux-fast/build/llvm-project/llvm/utils/lit/lit/llvm/config.py:569: note: using ld.lld: /home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/bin/ld.lld
llvm-lit: /home/b/sanitizer-x86_64-linux-fast/build/llvm-project/llvm/utils/lit/lit/llvm/config.py:569: note: using lld-link: /home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/bin/lld-link
llvm-lit: /home/b/sanitizer-x86_64-linux-fast/build/llvm-project/llvm/utils/lit/lit/llvm/config.py:569: note: using ld64.lld: /home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/bin/ld64.lld
llvm-lit: /home/b/sanitizer-x86_64-linux-fast/build/llvm-project/llvm/utils/lit/lit/llvm/config.py:569: note: using wasm-ld: /home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/bin/wasm-ld
llvm-lit: /home/b/sanitizer-x86_64-linux-fast/build/llvm-project/llvm/utils/lit/lit/main.py:74: note: The test suite configuration requested an individual test timeout of 0 seconds but a timeout of 900 seconds was requested on the command line. Forcing timeout to be 900 seconds.
-- Testing: 97785 tests, 64 workers --
Testing:  0.. 10.. 20.. 30.. 40.. 50.. 60.. 70.. 80.. 
FAIL: LLVM :: CodeGen/X86/basic-block-sections-clusters-bb-hash.ll (30810 of 97785)
******************** TEST 'LLVM :: CodeGen/X86/basic-block-sections-clusters-bb-hash.ll' FAILED ********************
Exit Code: 1

Command Output (stdout):
--
# RUN: at line 11
/home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/bin/llc /home/b/sanitizer-x86_64-linux-fast/build/llvm-project/llvm/test/CodeGen/X86/basic-block-sections-clusters-bb-hash.ll -O0 -mtriple=x86_64-pc-linux -function-sections -filetype=obj -basic-block-address-map -emit-bb-hash -o /home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/test/CodeGen/X86/Output/basic-block-sections-clusters-bb-hash.ll.tmp.o
# executed command: /home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/bin/llc /home/b/sanitizer-x86_64-linux-fast/build/llvm-project/llvm/test/CodeGen/X86/basic-block-sections-clusters-bb-hash.ll -O0 -mtriple=x86_64-pc-linux -function-sections -filetype=obj -basic-block-address-map -emit-bb-hash -o /home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/test/CodeGen/X86/Output/basic-block-sections-clusters-bb-hash.ll.tmp.o
# note: command had no output on stdout or stderr
# RUN: at line 16
echo 'v1' > /home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/test/CodeGen/X86/Output/basic-block-sections-clusters-bb-hash.ll.tmp1
# executed command: echo v1
# note: command had no output on stdout or stderr
# RUN: at line 17
echo 'f foo' >> /home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/test/CodeGen/X86/Output/basic-block-sections-clusters-bb-hash.ll.tmp1
# executed command: echo 'f foo'
# note: command had no output on stdout or stderr
# RUN: at line 18
echo 'g 0:100,1:100,2:0 1:100,3:100 2:0,3:0 3:100' >> /home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/test/CodeGen/X86/Output/basic-block-sections-clusters-bb-hash.ll.tmp1
# executed command: echo 'g 0:100,1:100,2:0 1:100,3:100 2:0,3:0 3:100'
# note: command had no output on stdout or stderr
# RUN: at line 22
/home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/bin/llvm-readobj /home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/test/CodeGen/X86/Output/basic-block-sections-clusters-bb-hash.ll.tmp.o --bb-addr-map |  awk 'BEGIN {printf "h"}      /ID: [0-9]+/ {id=$2}      /Hash: 0x[0-9A-Fa-f]+/ {gsub(/^0x/, "", $2); hash=$2; printf " %s:%s", id, hash}      END {print ""}'  >> /home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/test/CodeGen/X86/Output/basic-block-sections-clusters-bb-hash.ll.tmp1
# executed command: /home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/bin/llvm-readobj /home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/test/CodeGen/X86/Output/basic-block-sections-clusters-bb-hash.ll.tmp.o --bb-addr-map
# note: command had no output on stdout or stderr
# executed command: awk 'BEGIN {printf "h"}      /ID: [0-9]+/ {id=$2}      /Hash: 0x[0-9A-Fa-f]+/ {gsub(/^0x/, "", $2); hash=$2; printf " %s:%s", id, hash}      END {print ""}'
# note: command had no output on stdout or stderr
# RUN: at line 29
/home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/bin/llc < /home/b/sanitizer-x86_64-linux-fast/build/llvm-project/llvm/test/CodeGen/X86/basic-block-sections-clusters-bb-hash.ll -O0 -mtriple=x86_64-pc-linux -function-sections -basic-block-sections=/home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/test/CodeGen/X86/Output/basic-block-sections-clusters-bb-hash.ll.tmp1 -basic-block-section-match-infer |  /home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/bin/FileCheck /home/b/sanitizer-x86_64-linux-fast/build/llvm-project/llvm/test/CodeGen/X86/basic-block-sections-clusters-bb-hash.ll -check-prefixes=CHECK,LINUX-SECTIONS1
# executed command: /home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/bin/llc -O0 -mtriple=x86_64-pc-linux -function-sections -basic-block-sections=/home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/test/CodeGen/X86/Output/basic-block-sections-clusters-bb-hash.ll.tmp1 -basic-block-section-match-infer
# note: command had no output on stdout or stderr
# executed command: /home/b/sanitizer-x86_64-linux-fast/build/llvm_build_msan/bin/FileCheck /home/b/sanitizer-x86_64-linux-fast/build/llvm-project/llvm/test/CodeGen/X86/basic-block-sections-clusters-bb-hash.ll -check-prefixes=CHECK,LINUX-SECTIONS1
# .---command stderr------------
# | /home/b/sanitizer-x86_64-linux-fast/build/llvm-project/llvm/test/CodeGen/X86/basic-block-sections-clusters-bb-hash.ll:80:26: error: LINUX-SECTIONS1-LABEL: expected string not found in input
# | ; LINUX-SECTIONS1-LABEL: # %bb.1:
# |                          ^
# | <stdin>:7:5: note: scanning from here
# | foo: # @foo

@avikivity

Copy link
Copy Markdown
Contributor

Thanks a lot while working on this. While this isn't fixing a regression, I think a backport to 22.1 is warranted, the aarch64 port is useless without it for a class of programs that use thread-local storage heavily.

@xgupta

xgupta commented Mar 31, 2026

Copy link
Copy Markdown
Contributor Author

/cherry-pick 14ce208

@llvmbot

llvmbot commented Mar 31, 2026

Copy link
Copy Markdown
Member

Failed to cherry-pick: 14ce208

https://github.com/llvm/llvm-project/actions/runs/23795326630

Please manually backport the fix and push it to your github fork. Once this is done, please create a pull request

xgupta added a commit to xgupta/llvm-project that referenced this pull request Apr 9, 2026
…lvm#183962)

Clang plan to emit R_AARCH64_TLS_DTPREL64 in .debug_info (see PR

This prevent the debugger from correctly locating TLS variables when
using the DWARF DW_OP_GNU_push_tls_address or DW_AT_location with DTPREL
offsets.

This patch adds support for R_AARCH64_TLS_DTPREL64, adds its mapping to
R_DTPREL.
saagarjha pushed a commit to ahjragaas/binutils-gdb that referenced this pull request Apr 9, 2026
This patch allows R_AARCH64_TLS_DTPREL64 relocations in non-allocated
sections, which is required for DWARF debug information when using
Thread Local Storage. This matches the behavior in LLD.

Also a new syntax to parse dtprel operator use to describe tls
location in debug information. Please see the reference 3 below.

References:
  - llvm/llvm-project#146572
    [AArch64] Support TLS variables in debug info
  - llvm/llvm-project#183962
    [LLD][AArch64] Handle R_AARCH64_TLS_DTPREL64 in non-alloc sections
  - ARM-software/abi-aa#330
    [AAELF64] Allow R_AARCH64_TLS_DTPREL to be used statically.

bfd/
        * elfnn-aarch64.c (elfNN_aarch64_final_link_relocate): Handle
        BFD_RELOC_AARCH64_TLS_DTPREL.

gas/
	* config/tc-aarch64.c (s_aarch64_cons): Parse %dtprel(var) syntax.
	* testsuite/gas/aarch64/tls-debug.s: New test.
	* testsuite/gas/aarch64/tls-debug.d: Run the test.

ld/
	* testsuite/ld-aarch64/tls-debug.s: New test.
	* testsuite/ld-aarch64/tls-debug.d: Run the test.

Bug: https://sourceware.org/PR28351

Signed-off-by: Shivam Gupta <shivam98.tkg@gmail.com>
zwu-2025 pushed a commit to zwu-2025/llvm-project that referenced this pull request May 17, 2026
…lvm#183962)

Clang plan to emit R_AARCH64_TLS_DTPREL64 in .debug_info (see PR
llvm#146572). LLD currently fails to recognize this relocation.

This prevent the debugger from correctly locating TLS variables when
using the DWARF DW_OP_GNU_push_tls_address or DW_AT_location with DTPREL
offsets.

This patch adds support for R_AARCH64_TLS_DTPREL64, adds its mapping to
R_DTPREL.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Development

Successfully merging this pull request may close these issues.

8 participants