For a debugger to display statically-allocated[0] TLS variables the compiler must communicate information[1] that can be used in conjunction with knowledge of the runtime enviroment[2] to calculate a location for the variable for each thread. That need gives rise to dw_loc_dtprel in dwarf2out, a flag tracking whether the location description is dtprel, or relative to the "dynamic thread pointer". Location descriptions in the .debug_info section for TLS variables need to be relocated by the static linker accordingly, and dw_loc_dtprel controls emission of the needed relocations.
This is further complicated by -gsplit-dwarf. -gsplit-dwarf is designed to allow as much debugging information as possible to bypass the static linker to improve linking performance. One of the ways that is done is by introducing a layer of indirection for relocatable values[3]. That gives rise to addr_index_table which ultimately results in the .debug_addr section. While the code handling addr_index_table clearly contemplates the existence of dtprel entries[4] resolve_addr_in_expr does not, and the result is that when using -gsplit-dwarf the DWARF for TLS variables contains an address[5] rather than an offset, and debuggers can't work with that. This is visible on a trivial example. Compile ``` static __thread int tls_var; int main(void) { tls_var = 42; return 0; } ``` with -g and -g -gsplit-dwarf. Run the program under gdb. When examining the value of tls_var before and after the assignment, -g behaves as one would expect but -g -gsplit-dwarf does not. If the user is lucky and the miscalculated address is not mapped, gdb will print "Cannot access memory at address ...". If the user is unlucky and the miscalculated address is mapped, gdb will simply give the wrong value. You can further confirm that the issue is the address calculation by asking gdb for the address of tls_var and comparing that to what one would expect.[6] Thankfully this is trivial to fix by modifying resolve_addr_in_expr to propagate the dtprel character of the location where necessary. gdb begins working as expected and the diff in the generated assembly is clear. ``` .section .debug_addr,"",@progbits .long 0x14 .value 0x5 .byte 0x8 .byte 0 .Ldebug_addr0: - .quad tls_var + .long tls_var@dtpoff, 0 .quad .LFB0 ``` [0] Referring to e.g. __thread as statically-allocated vs. e.g. a dynamically-allocated pthread_key_create() call. [1] Generally an offset in a TLS block. [2] With glibc, provided by libthread_db.so. [3] Relocatable values are moved to a table in the .debug_addr section, those values in .debug_info are replaced with special values that look up indexes in that table, and then the static linker elsewhere assigns a single per-CU starting index in the .debug_addr section, allowing those special values to remain permanently fixed and the resulting data to be ignored by the linker. [4] ate_kind_rtx_dtprel exists, after all, and new_addr_loc_descr does produce it where appropriate. [5] e.g. an address in the .tbss/.tdata section. [6] e.g. on x86-64 by examining %fsbase and the offset in the assembly 2025-05-01 Kyle Huey <kh...@kylehuey.com> * dwarf2out.cc (resolve_addr_in_expr): Propagate dtprel into the address table when appropriate. --- gcc/dwarf2out.cc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gcc/dwarf2out.cc b/gcc/dwarf2out.cc index 34ffeed86ff..9aecdb9fd5a 100644 --- a/gcc/dwarf2out.cc +++ b/gcc/dwarf2out.cc @@ -31068,7 +31068,8 @@ resolve_addr_in_expr (dw_attr_node *a, dw_loc_descr_ref loc) return false; remove_addr_table_entry (loc->dw_loc_oprnd1.val_entry); loc->dw_loc_oprnd1.val_entry - = add_addr_table_entry (rtl, ate_kind_rtx); + = add_addr_table_entry (rtl, loc->dw_loc_dtprel + ? ate_kind_rtx_dtprel : ate_kind_rtx); } break; case DW_OP_const4u: -- 2.43.0