[lldb][RISCV] Implement access to TLS variables on RISC-V (#191410)

On RISC-V Linux, LLDB computes TLS variable addresses incorrectly:
`GetThreadLocalData` returns a correct tls_block, but then
unconditionally adds tls_file_addr from `DW_OP_GNU_push_tls_address`,
which on RISC-V/glibc is a VMA inside PT_TLS, not a pure offset. This
results in an over-shifted address.

This patch:

* Adds a small helper that, for an ELF module, finds the PT_TLS program
header and reads its p_vaddr.

* In `DynamicLoaderPOSIXDYLD::GetThreadLocalData`, normalizes
tls_file_addr to an offset: if `PT_TLS` is found and tls_file_addr >=
p_vaddr, it uses tpoff = tls_file_addr - p_vaddr, otherwise keeps the
old value.

* Returns tls_block + tpoff instead of always tls_block + tls_file_addr.

This fixes reading TLS variables on RISC-V/glibc while leaving behavior
unchanged on targets where DWARF already provides a plain offset (e.g.
x86_64).

Also this patch fixes `TestTlsGlobals.py`

---------

Co-authored-by: Jonas Devlieghere <jonas@devlieghere.com>
This commit is contained in:
Georgiy Samoylov
2026-04-20 11:46:46 +03:00
committed by GitHub
parent 936643678f
commit 920b203fe2
4 changed files with 58 additions and 5 deletions

View File

@@ -91,7 +91,7 @@ static const std::array<RegisterInfo, 33> g_register_infos = {
DEFINE_GENERIC_REGISTER_STUB(ra, nullptr, LLDB_REGNUM_GENERIC_RA),
DEFINE_GENERIC_REGISTER_STUB(sp, nullptr, LLDB_REGNUM_GENERIC_SP),
DEFINE_REGISTER_STUB(gp, nullptr),
DEFINE_REGISTER_STUB(tp, nullptr),
DEFINE_GENERIC_REGISTER_STUB(tp, nullptr, LLDB_REGNUM_GENERIC_TP),
DEFINE_REGISTER_STUB(t0, nullptr),
DEFINE_REGISTER_STUB(t1, nullptr),
DEFINE_REGISTER_STUB(t2, nullptr),
@@ -820,6 +820,7 @@ static uint32_t GetGenericNum(llvm::StringRef name) {
.Cases({"ra", "x1"}, LLDB_REGNUM_GENERIC_RA)
.Cases({"sp", "x2"}, LLDB_REGNUM_GENERIC_SP)
.Cases({"fp", "s0"}, LLDB_REGNUM_GENERIC_FP)
.Cases({"tp", "x4"}, LLDB_REGNUM_GENERIC_TP)
.Case("a0", LLDB_REGNUM_GENERIC_ARG1)
.Case("a1", LLDB_REGNUM_GENERIC_ARG2)
.Case("a2", LLDB_REGNUM_GENERIC_ARG3)

View File

@@ -9,6 +9,7 @@
// Main header include
#include "DynamicLoaderPOSIXDYLD.h"
#include "Plugins/ObjectFile/ELF/ObjectFileELF.h"
#include "Plugins/ObjectFile/Placeholder/ObjectFilePlaceholder.h"
#include "lldb/Breakpoint/BreakpointLocation.h"
#include "lldb/Core/Debugger.h"
@@ -27,6 +28,7 @@
#include "lldb/Utility/LLDBLog.h"
#include "lldb/Utility/Log.h"
#include "lldb/Utility/ProcessInfo.h"
#include "llvm/BinaryFormat/ELF.h"
#include "llvm/Support/ThreadPool.h"
#include <memory>
@@ -814,6 +816,26 @@ addr_t DynamicLoaderPOSIXDYLD::GetEntryPoint() {
return m_entry_point;
}
static lldb::addr_t GetPTTLSVAddr(const lldb::ModuleSP &module_sp) {
if (!module_sp)
return LLDB_INVALID_ADDRESS;
ObjectFile *objfile = module_sp->GetObjectFile();
if (!objfile)
return LLDB_INVALID_ADDRESS;
auto *elf_obj = llvm::dyn_cast<ObjectFileELF>(objfile);
if (!elf_obj)
return LLDB_INVALID_ADDRESS;
for (const auto &phdr : elf_obj->ProgramHeaders()) {
if (phdr.p_type == llvm::ELF::PT_TLS)
return phdr.p_vaddr;
}
return LLDB_INVALID_ADDRESS;
}
lldb::addr_t
DynamicLoaderPOSIXDYLD::GetThreadLocalData(const lldb::ModuleSP module_sp,
const lldb::ThreadSP thread,
@@ -894,6 +916,18 @@ DynamicLoaderPOSIXDYLD::GetThreadLocalData(const lldb::ModuleSP module_sp,
dtv_ptr = tp;
}
}
const llvm::Triple &triple = module_sp->GetArchitecture().GetTriple();
// On RISC-V with glibc the TLS layout uses Variant I (TLS_DTV_AT_TP).
// The thread pointer (tp) points just past a tcbhead_t header which
// contains two pointers: { dtv, private }. This means the DTV pointer
// itself is located two pointer-sized slots before tp, so dtv_ptr must
// be computed as tp - 2 * sizeof(void*). See MaskRay, “All about
// thread-local storage” (RISC-V/glibc section) and glibc's RISC-V
// TLS port (__tls_get_addr / THREAD_DTV).
if (triple.isRISCV())
dtv_ptr = tp - 2 * triple.getArchPointerBitWidth() / 8;
addr_t dtv = (dtv_ptr != LLDB_INVALID_ADDRESS) ? ReadPointer(dtv_ptr)
: LLDB_INVALID_ADDRESS;
if (dtv == LLDB_INVALID_ADDRESS) {
@@ -915,8 +949,26 @@ DynamicLoaderPOSIXDYLD::GetThreadLocalData(const lldb::ModuleSP module_sp,
if (tls_block == LLDB_INVALID_ADDRESS) {
LLDB_LOGF(log, "GetThreadLocalData error: fail to read tls_block");
return LLDB_INVALID_ADDRESS;
} else
return tls_block + tls_file_addr;
}
// DW_OP_GNU_push_tls_address gives us a value in tls_file_addr that can be
// either:
// - a pure offset inside the TLS block (e.g. x86_64/glibc), or
// - a virtual address inside the PT_TLS segment (e.g. RISC-V/glibc),
// roughly PT_TLS.p_vaddr + TPOFF(sym).
// To handle both cases, we try to normalize it to a plain offset (tpoff)
// by subtracting PT_TLS.p_vaddr when available.
addr_t pt_tls_vaddr = GetPTTLSVAddr(module_sp);
addr_t tpoff = tls_file_addr;
// If the module has a PT_TLS segment and the DWARF value lies at or above
// its p_vaddr, treat tls_file_addr as a VMA within PT_TLS and convert it
// to an offset. Otherwise, keep the original value (it is already an offset
// on targets like x86_64).
if (pt_tls_vaddr != LLDB_INVALID_ADDRESS && tls_file_addr >= pt_tls_vaddr)
tpoff = tls_file_addr - pt_tls_vaddr;
return tls_block + tpoff;
}
void DynamicLoaderPOSIXDYLD::ResolveExecutableModule(

View File

@@ -85,7 +85,7 @@ static lldb_private::RegisterInfo g_register_infos_riscv64_gpr[] = {
DEFINE_GPR64_ALT(ra, x1, LLDB_REGNUM_GENERIC_RA),
DEFINE_GPR64_ALT(sp, x2, LLDB_REGNUM_GENERIC_SP),
DEFINE_GPR64_ALT(gp, x3, LLDB_INVALID_REGNUM),
DEFINE_GPR64_ALT(tp, x4, LLDB_INVALID_REGNUM),
DEFINE_GPR64_ALT(tp, x4, LLDB_REGNUM_GENERIC_TP),
DEFINE_GPR64_ALT(t0, x5, LLDB_INVALID_REGNUM),
DEFINE_GPR64_ALT(t1, x6, LLDB_INVALID_REGNUM),
DEFINE_GPR64_ALT(t2, x7, LLDB_INVALID_REGNUM),

View File

@@ -279,7 +279,7 @@ Changes to LLDB
### Linux
* 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.
* Thread local variables are now supported on Arm and RISC-V Linux if the program being debugged is using glibc.
* LLDB now supports AArch64 Linux systems that only have SME (as opposed to
SVE and SME). See the AArch64 Linux [documentation](https://lldb.llvm.org/use/aarch64-linux.html#sme-only-systems)
for more details.