From: Andrew Burgess Date: Tue, 21 Jan 2025 17:22:04 +0000 (+0000) Subject: gdb: include NT_I386_TLS note in generated core files X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=91eee81d23537366bb6072c66f662ab46d88380a;p=thirdparty%2Fbinutils-gdb.git gdb: include NT_I386_TLS note in generated core files This commit extends GDB for x86/Linux to include the NT_I386_TLS note in generated core files (i.e. created with `generate-core-file` or `gcore` command). This note contains the 3 per-thread TLS related GDT (global descriptor table) entries, and is present for i386 binaries, or those compiled on x86-64 with -m32. The approach I have taken to achieve this, is to make the 3 GDT entries available within 3 new registers. I added these registers to the org.gnu.gdb.i386.linux target description feature, as this feature seemed perfectly named. As the new registers are optional I don't see any harm in extending this existing feature. I did consider adding a new feature with `tls` in the name, but this seemed excessive given the existing feature. Which GDT entries are used for TLS varies between i386 and x86-64 running in 32-bit mode. As such the registers are named with suffixes 0, 1, and 2, and it is left to GDB or gdbserver, to find the correct GDT entries (based on the precise target) and place the contents into these registers. With this done, adding the relevant regset is sufficient to get the tls contents emitted as a core file note. Support for emitting the note into the generated core file relies on some BFD changes which were made in an earlier commit: commit ea6ec00ff4520895735e4913cb90c933c7296f04 Date: Fri Jul 25 19:51:58 2025 +0100 bfd: support for NT_386_TLS notes The three new registers are readable and writable. Writing to one of the new registers will update the relevant kernel GDT entry. Each TLS GDT is represented by a 'struct user_desc' (see 'man 2 get_thread_area' for details), the first 4 bytes of each 'user_desc' is the 'entry_number' field, this is the index of the GDT within the kernel, and cannot be modified. Attempts to write to this region of the register will be ignored, but will not give an error. I did consider not including this part of the user_desc within the register value, but this becomes difficult when we consider remote targets, GDB would then need to figure out what these indexes were so that the core file note could be generated. Sure, we probably could figure the correct index values out, but I figure, why bother, we can just pass them through in the register and know for certain that we have the correct values. For testing, there's a new test that covers the basic functionality, including read/write access to the new registers, and checking that the NT_386_TLS note is added to the core file, and that the note contents can be read by GDB. I also manually tested opening a core file generated from an old GDB (so no NT_386_TLS notes) using a GDB with this patch. This works fine, the new tls registers are not created as the NT_GDB_TDESC note (the target description) doesn't include the new registers. Out of interest I also patched an old version of GDB to avoid creating the NT_GDB_TDESC, and created a core file. This core file contained neither the NT_386_TLS nor NT_GDB_TDESC. When opening this core file with a patched GDB, the new registers do show up, but their contents are given as , which is exactly what we'd expect, GDB builds a target description based on the architecture, the architecture says these registers should exist, but they are missing from the core file, hence, . I also tested using a patched GDB with an old version of gdbserver, the new registers don't show up as the old gdbserver doesn't send them in its target description. And a core file created using the gcore command in such a setup leaves no NT_386_TLS notes added, which is what we'd expect. And I also tested a new gdbserver running with an old version of GDB. As the new tls registers are now mentioned in the target description, then obviously, the old GDB does see the registers, and present them to the user, however GDB doesn't know how to use these registers to create a NT_386_TLS, so that note isn't added to any core files. Also, while a new GDB places the tls registers into the 'system' group, an old GDB doesn't do this, so the registers end up in the 'general' group by default. This means they show up within 'info registers' output. This isn't ideal, but there's not much that can be done about this. Overall, I feel the combinations of old and new tools has been tested, and the behaviours are what we'd want or expect. I'm tagging this commit with PR gdb/15591, even though this patch isn't directly related. That bug is for improving GDB's testing of TLS support in core files. The test in this commit does do some very simple reading of a TLS variable, but there's only two threads, and one TLS variable, so it's not extensive. Additionally, the test in this commit is x86 only, so this should not be considered a full resolution to that bug. But still, it's something. Bug: https://sourceware.org/bugzilla/show_bug.cgi?id=15591 Reviewed-By: Eli Zaretskii Reviewed-By: Christina Schimpe Reviewed-By: Keith Seitz --- diff --git a/gdb/NEWS b/gdb/NEWS index c19f0ed1e36..c976727ee45 100644 --- a/gdb/NEWS +++ b/gdb/NEWS @@ -181,6 +181,10 @@ qExecAndArgs In systems that don't support linker namespaces, or if the inferior hasn't started yet, these always return the integer 0. +* The 'org.gnu.gdb.i386.linux' target description feature can now + contain three additional registers which provide access to the TLS + related GDT entries on i386 (and x86-64 when compiling with -m32). + * Add record full support for rv64gc architectures * Debugging Linux programs that use AArch64 Guarded Control Stacks is now diff --git a/gdb/amd64-linux-nat.c b/gdb/amd64-linux-nat.c index 96acfda7d00..53a92f89957 100644 --- a/gdb/amd64-linux-nat.c +++ b/gdb/amd64-linux-nat.c @@ -38,6 +38,9 @@ #include "x86-linux-nat.h" #include "nat/linux-ptrace.h" #include "nat/amd64-linux-siginfo.h" +#include "nat/i386-linux.h" + +#include /* This definition comes from prctl.h. Kernels older than 2.5.64 do not have it. */ @@ -89,7 +92,8 @@ static int amd64_linux_gregset32_reg_offset[] = -1, /* PKEYS register PKRU */ -1, /* SSP register. */ -1, -1, /* fs/gs base registers. */ - ORIG_RAX * 8 /* "orig_eax" */ + ORIG_RAX * 8, /* "orig_eax" */ + -1, -1, -1, /* TLS GDT regs: i386_tls_gdt_0...2. */ }; @@ -236,7 +240,13 @@ amd64_linux_nat_target::fetch_registers (struct regcache *regcache, int regnum) if (regnum == -1 || !amd64_native_gregset_supplies_p (gdbarch, regnum)) { - elf_fpregset_t fpregs; + if (regnum == -1 || i386_is_tls_regnum_p (regnum)) + { + if (tdep->i386_linux_tls) + i386_fetch_tls_regs (regcache, tid, regnum); + if (regnum != -1) + return; + } if (have_ptrace_getregset == TRIBOOL_TRUE) { @@ -266,6 +276,8 @@ amd64_linux_nat_target::fetch_registers (struct regcache *regcache, int regnum) } else { + elf_fpregset_t fpregs; + if (ptrace (PTRACE_GETFPREGS, tid, 0, (long) &fpregs) < 0) perror_with_name (_("Couldn't get floating point status")); @@ -308,7 +320,13 @@ amd64_linux_nat_target::store_registers (struct regcache *regcache, int regnum) if (regnum == -1 || !amd64_native_gregset_supplies_p (gdbarch, regnum)) { - elf_fpregset_t fpregs; + if (regnum == -1 || i386_is_tls_regnum_p (regnum)) + { + if (tdep->i386_linux_tls) + i386_store_tls_regs (regcache, tid, regnum); + if (regnum != -1) + return; + } if (have_ptrace_getregset == TRIBOOL_TRUE) { @@ -337,6 +355,8 @@ amd64_linux_nat_target::store_registers (struct regcache *regcache, int regnum) } else { + elf_fpregset_t fpregs; + if (ptrace (PTRACE_GETFPREGS, tid, 0, (long) &fpregs) < 0) perror_with_name (_("Couldn't get floating point status")); diff --git a/gdb/doc/gdb.texinfo b/gdb/doc/gdb.texinfo index 87e994defa6..1cc7d295fbf 100644 --- a/gdb/doc/gdb.texinfo +++ b/gdb/doc/gdb.texinfo @@ -50388,8 +50388,15 @@ describe the upper 128 bits of @sc{ymm} registers: @samp{ymm0h} through @samp{ymm15h} for amd64 @end itemize -The @samp{org.gnu.gdb.i386.linux} feature is optional. It should -describe a single register, @samp{orig_eax}. +The @samp{org.gnu.gdb.i386.linux} feature is optional. If the feature +is present, then it should describe the 32 bit register, @samp{orig_eax}. + +Additionally, the @samp{org.gnu.gdb.i386.linux} feature can optionally +contain three 128 bit registers called @samp{i386_tls_gdt_0}, +@samp{i386_tls_gdt_1}, and @samp{i386_tls_gdt_2}. Each of these +registers contains one 16 byte @samp{struct user_desc} (see @kbd{man +2 get_thread_area}) object which describes one of the three TLS +related GDT entries. The @samp{org.gnu.gdb.i386.segments} feature is optional. It should describe two system registers: @samp{fs_base} and @samp{gs_base}. diff --git a/gdb/features/i386/32bit-linux.c b/gdb/features/i386/32bit-linux.c index 3289f07d332..6fba8a6880b 100644 --- a/gdb/features/i386/32bit-linux.c +++ b/gdb/features/i386/32bit-linux.c @@ -9,7 +9,14 @@ create_feature_i386_32bit_linux (struct target_desc *result, long regnum) struct tdesc_feature *feature; feature = tdesc_create_feature (result, "org.gnu.gdb.i386.linux"); + tdesc_type *element_type; + element_type = tdesc_named_type (feature, "uint32"); + tdesc_create_vector (feature, "i386_tls_gdt_reg", element_type, 4); + regnum = 41; tdesc_create_reg (feature, "orig_eax", regnum++, 1, NULL, 32, "int"); + tdesc_create_reg (feature, "i386_tls_gdt_0", regnum++, 1, "system", 128, "i386_tls_gdt_reg"); + tdesc_create_reg (feature, "i386_tls_gdt_1", regnum++, 1, "system", 128, "i386_tls_gdt_reg"); + tdesc_create_reg (feature, "i386_tls_gdt_2", regnum++, 1, "system", 128, "i386_tls_gdt_reg"); return regnum; } diff --git a/gdb/features/i386/32bit-linux.xml b/gdb/features/i386/32bit-linux.xml index 0bfc0bc5563..7de3198b7da 100644 --- a/gdb/features/i386/32bit-linux.xml +++ b/gdb/features/i386/32bit-linux.xml @@ -8,4 +8,9 @@ + + + + + diff --git a/gdb/i386-linux-nat.c b/gdb/i386-linux-nat.c index 4af06c448a8..f86005e30e8 100644 --- a/gdb/i386-linux-nat.c +++ b/gdb/i386-linux-nat.c @@ -447,6 +447,8 @@ store_fpxregs (const struct regcache *regcache, int tid, int regno) void i386_linux_nat_target::fetch_registers (struct regcache *regcache, int regno) { + gdbarch *gdbarch = regcache->arch (); + const i386_gdbarch_tdep *tdep = gdbarch_tdep (gdbarch); pid_t tid; /* Use the old method of peeking around in `struct user' if the @@ -470,6 +472,9 @@ i386_linux_nat_target::fetch_registers (struct regcache *regcache, int regno) zero. */ if (regno == -1) { + if (tdep->i386_linux_tls) + i386_fetch_tls_regs (regcache, tid, regno); + fetch_regs (regcache, tid); /* The call above might reset `have_ptrace_getregs'. */ @@ -514,6 +519,12 @@ i386_linux_nat_target::fetch_registers (struct regcache *regcache, int regno) return; } + if (tdep->i386_linux_tls && i386_is_tls_regnum_p (regno)) + { + i386_fetch_tls_regs (regcache, tid, regno); + return; + } + internal_error (_("Got request for bad register number %d."), regno); } @@ -523,6 +534,8 @@ i386_linux_nat_target::fetch_registers (struct regcache *regcache, int regno) void i386_linux_nat_target::store_registers (struct regcache *regcache, int regno) { + gdbarch *gdbarch = regcache->arch (); + const i386_gdbarch_tdep *tdep = gdbarch_tdep (gdbarch); pid_t tid; /* Use the old method of poking around in `struct user' if the @@ -545,6 +558,8 @@ i386_linux_nat_target::store_registers (struct regcache *regcache, int regno) store_fpxregs can fail, and return zero. */ if (regno == -1) { + if (tdep->i386_linux_tls) + i386_store_tls_regs (regcache, tid, regno); store_regs (regcache, tid, regno); if (store_xstateregs (regcache, tid, regno)) return; @@ -578,6 +593,12 @@ i386_linux_nat_target::store_registers (struct regcache *regcache, int regno) return; } + if (tdep->i386_linux_tls && i386_is_tls_regnum_p (regno)) + { + i386_store_tls_regs (regcache, tid, regno); + return; + } + internal_error (_("Got request to store bad register number %d."), regno); } diff --git a/gdb/i386-linux-tdep.c b/gdb/i386-linux-tdep.c index 20adf169c81..1d6abe2f546 100644 --- a/gdb/i386-linux-tdep.c +++ b/gdb/i386-linux-tdep.c @@ -59,7 +59,7 @@ static int i386_linux_register_reggroup_p (struct gdbarch *gdbarch, int regnum, const struct reggroup *group) { - if (regnum == I386_LINUX_ORIG_EAX_REGNUM) + if (regnum == I386_LINUX_ORIG_EAX_REGNUM || i386_is_tls_regnum_p (regnum)) return (group == system_reggroup || group == save_reggroup || group == restore_reggroup); @@ -1047,6 +1047,7 @@ int i386_linux_gregset_reg_offset[] = -1, /* SSP register. */ -1, -1, /* fs/gs base registers. */ 11 * 4, /* "orig_eax" */ + -1, -1, -1, /* TLS GDT regs: i386_tls_gdt_0...2. */ }; /* Mapping between the general-purpose registers in `struct @@ -1163,15 +1164,150 @@ i386_linux_collect_xstateregset (const struct regset *regset, i387_collect_xsave (regcache, regnum, xstateregs, 1); } +/* Within a tdep file we don't have access to system headers. This + structure is a clone of 'struct user_desc' from 'asm/ldt.h' on x86 + GNU/Linux systems. See 'see man 2 get_thread_area' on a suitable x86 + machine for more details. */ + +struct x86_user_desc +{ + uint32_t entry_number; + uint32_t base_addr; + uint32_t limit; + + /* In the actual struct, these flags are a series of 1-bit separate + flags. But we don't need that level of insight for the + processing we do in GDB, so just make it a single field. */ + uint32_t flags; +}; + +/* Supply the 3 tls related registers from BUFFER (length LEN) into + REGCACHE. The REGSET and REGNUM are ignored, all three registers are + always supplied from BUFFER. */ + +static void +i386_linux_supply_tls_regset (const regset *regset, + regcache *regcache, int regnum, + const void *buffer, size_t len) +{ + gdbarch *gdbarch = regcache->arch (); + i386_gdbarch_tdep *tdep = gdbarch_tdep (gdbarch); + + if (!tdep->i386_linux_tls) + return; + + gdb_assert (len == sizeof (x86_user_desc) * 3); + + for (int i = 0; i < 3; ++i) + { + int tls_regno = I386_LINUX_TLS_GDT_0 + i; + + gdb_assert (regcache->register_size (tls_regno) + == sizeof (x86_user_desc)); + + regcache->raw_supply (tls_regno, buffer); + buffer = static_cast (buffer) + 1; + } +} + +/* Collect the 3 tls related registers from REGCACHE, placing the results + in to BUFFER (length LEN). The REGSET and REGNUM are ignored, all three + registers are always collected from REGCACHE. */ + +static void +i386_linux_collect_tls_regset (const regset *regset, + const regcache *regcache, + int regnum, void *buffer, size_t len) +{ + gdbarch *gdbarch = regcache->arch (); + i386_gdbarch_tdep *tdep = gdbarch_tdep (gdbarch); + + if (!tdep->i386_linux_tls) + return; + + gdb_assert (len == sizeof (x86_user_desc) * 3); + + for (int i = 0; i < 3; ++i) + { + x86_user_desc desc; + int tls_regno = I386_LINUX_TLS_GDT_0 + i; + + gdb_assert (regcache->register_size (tls_regno) == sizeof (desc)); + + regcache->raw_collect (tls_regno, &desc); + memcpy (buffer, &desc, sizeof (desc)); + buffer = static_cast (buffer) + 1; + } +} + /* Register set definitions. */ -static const struct regset i386_linux_xstateregset = +static const regset i386_linux_xstateregset = { NULL, i386_linux_supply_xstateregset, i386_linux_collect_xstateregset }; +static const regset i386_linux_tls_regset = + { + NULL, + i386_linux_supply_tls_regset, + i386_linux_collect_tls_regset + }; + +/* Helper for i386_linux_iterate_over_regset_sections. Should we + visit the NT_386_TLS note? If REGCACHE is NULL then we are reading + the notes from the corefile, so we always visit the note. If + REGCACHE is not NULL, in this case we are creating a corefile. In + this case, we only visit the note if all the TLS registers are + valid, and their base address and limit are not zero, this mirrors + the kernel behaviour where the TLS note is elided when the TLS GDT + entries have not been set. + + Only call for architectures where i386_gdbarch_tdep::i386_linux_tls + is true. */ + +static bool +should_visit_i386_tls_note (const regcache *regcache) +{ + if (regcache == nullptr) + return true; + + /* Check the pre-condition. */ + gdbarch *gdbarch = regcache->arch (); + i386_gdbarch_tdep *tdep = gdbarch_tdep (gdbarch); + gdb_assert (tdep->i386_linux_tls); + + for (int i = 0; i < 3; ++i) + { + int tls_regno = I386_LINUX_TLS_GDT_0 + i; + + /* If we failed to read any of the registers then we'll not be + able to emit valid note. */ + if (regcache->get_register_status (tls_regno) != REG_VALID) + return false; + + /* As i386_gdbarch_tdep::i386_linux_tls is true, the registers + must be the right size. The flag is only set true when this + condition holds. */ + gdb_assert (regcache->register_size (tls_regno) + == sizeof (x86_user_desc)); + + /* Read the TLS GDT entry. If it is in use then we want to + write the NT_386_TLS note. */ + x86_user_desc ud; + regcache->raw_collect (tls_regno, &ud); + if (ud.base_addr != 0 && ud.limit != 0) + return true; + } + + /* Made it through the loop without finding any in-use TLS related + GDT entries. No point creating the NT_386_TLS note, the kernel + doesn't. */ + return false; +} + /* Iterate over core file register note sections. */ static void @@ -1193,6 +1329,9 @@ i386_linux_iterate_over_regset_sections (struct gdbarch *gdbarch, cb_data); else cb (".reg2", 108, 108, &i386_fpregset, NULL, cb_data); + + if (tdep->i386_linux_tls && should_visit_i386_tls_note (regcache)) + cb (".reg-i386-tls", 48, 48, &i386_linux_tls_regset, nullptr, cb_data); } /* Linux kernel shows PC value after the 'int $0x80' instruction even if @@ -1272,6 +1411,37 @@ i386_linux_init_abi (struct gdbarch_info info, struct gdbarch *gdbarch) if (!valid_p) return; + /* Helper function. Look for TLS_REG_NAME in I386_FEATURE (with the + associated LOCAL_TDESC_DATA), and if the register is found assign it + TLS_REGNO. Return true if the register is found, and it is the size + of 'struct user_desc' (see man 2 get_thread_area), otherwise, return + false. */ + static const auto valid_tls_reg + = [] (const tdesc_feature *i386_feature, + tdesc_arch_data *local_tdesc_data, + const char *tls_reg_name, int tls_regno) -> bool + { + static constexpr int required_reg_size + = sizeof (x86_user_desc) * HOST_CHAR_BIT; + return (tdesc_numbered_register (i386_feature, local_tdesc_data, + tls_regno, tls_reg_name) + && (tdesc_register_bitsize (i386_feature, tls_reg_name) + == required_reg_size)); + }; + + /* Check all the expected tls related registers are found, and are the + correct size. If they are then mark the tls feature as being active + in TDEP. Otherwise, leave the feature as deactivated. */ + valid_p = (valid_tls_reg (feature, tdesc_data, "i386_tls_gdt_0", + I386_LINUX_TLS_GDT_0) + && valid_tls_reg (feature, tdesc_data, "i386_tls_gdt_1", + I386_LINUX_TLS_GDT_1) + && valid_tls_reg (feature, tdesc_data, "i386_tls_gdt_2", + I386_LINUX_TLS_GDT_2)); + + if (valid_p) + tdep->i386_linux_tls = true; + /* Add the %orig_eax register used for syscall restarting. */ set_gdbarch_write_pc (gdbarch, i386_linux_write_pc); diff --git a/gdb/i386-linux-tdep.h b/gdb/i386-linux-tdep.h index 8a1a2447776..e75e53aba95 100644 --- a/gdb/i386-linux-tdep.h +++ b/gdb/i386-linux-tdep.h @@ -39,6 +39,13 @@ enum i386_linux_regnum is supposed to restart. */ I386_LINUX_ORIG_EAX_REGNUM = I386_NUM_REGS, + /* Register numbers for the three TLS GDT registers. These contain the + 'struct user_desc' (see 'man 2 get_thread_area') values for the three + TLS related Global Descriptor Table entries. */ + I386_LINUX_TLS_GDT_0, + I386_LINUX_TLS_GDT_1, + I386_LINUX_TLS_GDT_2, + /* Total number of registers for GNU/Linux. */ I386_LINUX_NUM_REGS @@ -61,4 +68,12 @@ extern bool i386_linux_core_read_x86_xsave_layout (struct gdbarch *gdbarch, extern int i386_linux_gregset_reg_offset[]; +/* Return true if REGNUM is one of the 3 tls gdt registers. */ + +static inline bool +i386_is_tls_regnum_p (int regnum) +{ + return regnum >= I386_LINUX_TLS_GDT_0 && regnum <= I386_LINUX_TLS_GDT_2; +} + #endif /* GDB_I386_LINUX_TDEP_H */ diff --git a/gdb/i386-tdep.h b/gdb/i386-tdep.h index cfe0d7383d6..ad355eb597a 100644 --- a/gdb/i386-tdep.h +++ b/gdb/i386-tdep.h @@ -240,6 +240,11 @@ struct i386_gdbarch_tdep : gdbarch_tdep_base struct type *i386_zmm_type = nullptr; struct type *i387_ext_type = nullptr; + /* If the registers containing the i386 Linux TLS related global + descriptor table information are available. This is used to decide + whether to add the NT_386_TLS note to the core file or not. */ + bool i386_linux_tls = false; + /* Process record/replay target. */ /* The map for registers because the AMD64's registers order in GDB is not same as I386 instructions. */ diff --git a/gdb/nat/amd64-linux.h b/gdb/nat/amd64-linux.h new file mode 100644 index 00000000000..1d5bf573829 --- /dev/null +++ b/gdb/nat/amd64-linux.h @@ -0,0 +1,29 @@ +/* Native-dependent code for GNU/Linux amd64. + + Copyright (C) 2025 Free Software Foundation, Inc. + + This file is part of GDB. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . */ + +#ifndef GDB_NAT_AMD64_LINUX_H +#define GDB_NAT_AMD64_LINUX_H + +/* See nat/i386-linux.h for a full description of this constant. This is + the version used when GDB is compiled for amd64, and is running an + executable compiled with -m32. */ + +static inline constexpr int i386_initial_tls_gdt = 12; + +#endif /* GDB_NAT_AMD64_LINUX_H */ diff --git a/gdb/nat/i386-linux.h b/gdb/nat/i386-linux.h index d80e0f999bf..b99f0a02418 100644 --- a/gdb/nat/i386-linux.h +++ b/gdb/nat/i386-linux.h @@ -34,4 +34,14 @@ variable. */ extern tribool have_ptrace_getfpxregs; +/* This constant defines the first GDT (Global Descriptor Table) entry + that the kernel allocates for holding TLS descriptors. There are three + entries, starting at this index which can be accessed using the + PTRACE_GET_THREAD_AREA and PTRACE_SET_THREAD_AREA ptrace calls. This + constant is only valid for true i386 kernels. For amd64 kernels + running in 32-bit mode (i.e. executables compiled -m32) there is a + different constant, see nat/amd64-linux.h. */ + +static inline constexpr int i386_initial_tls_gdt = 6; + #endif /* GDB_NAT_I386_LINUX_H */ diff --git a/gdb/nat/x86-linux.c b/gdb/nat/x86-linux.c index 55158268970..8906b7e6104 100644 --- a/gdb/nat/x86-linux.c +++ b/gdb/nat/x86-linux.c @@ -28,6 +28,12 @@ #include "nat/gdb_ptrace.h" #include +#ifndef __x86_64__ +#include "nat/i386-linux.h" +#else +#include "nat/amd64-linux.h" +#endif + /* Per-thread arch-specific data we want to keep. */ struct arch_lwp_info @@ -185,3 +191,41 @@ x86_check_ssp_support (const int tid) return true; } + +/* See nat/x86-linux.h. */ + +bool +i386_ptrace_get_tls_data (int pid, gdb::array_view buffer) +{ + gdb_assert (buffer.size () == 3); + + for (int i = 0; i < 3; ++i) + { + void *addr = (void *) (uintptr_t) (i386_initial_tls_gdt + i); + void *data = buffer.slice (i, 1).data (); + + if (ptrace (PTRACE_GET_THREAD_AREA, pid, addr, data) < 0) + return false; + } + + return true; +} + +/* See nat/x86-linux.h. */ + +bool +i386_ptrace_set_tls_data (int pid, gdb::array_view buffer) +{ + gdb_assert (buffer.size () == 3); + + for (int i = 0; i < 3; ++i) + { + void *addr = (void *) (uintptr_t) (i386_initial_tls_gdt + i); + void *data = buffer.slice (i, 1).data (); + + if (ptrace (PTRACE_SET_THREAD_AREA, pid, addr, data) < 0) + return false; + } + + return true; +} diff --git a/gdb/nat/x86-linux.h b/gdb/nat/x86-linux.h index 1783aae05d0..c982a46bf2e 100644 --- a/gdb/nat/x86-linux.h +++ b/gdb/nat/x86-linux.h @@ -21,6 +21,7 @@ #define GDB_NAT_X86_LINUX_H #include "nat/linux-nat.h" +#include /* Set whether our local mirror of LWP's debug registers has been changed since the values were last written to the thread. Nonzero @@ -79,4 +80,18 @@ extern x86_linux_arch_size x86_linux_ptrace_get_arch_size (int tid); extern bool x86_check_ssp_support (const int tid); +/* Get the three TLS related GDT (Global Descriptor Table) entries from + the kernel for thread PID, placing the results into BUFFER, an array of + length 3. */ + +extern bool i386_ptrace_get_tls_data + (int pid, gdb::array_view buffer); + +/* Store the three TLS related GDT (Global Descriptor Table) entries held + in BUFFER back into the kernel for thread PID. BUFFER must have a + length of three. */ + +extern bool i386_ptrace_set_tls_data + (int pid, gdb::array_view buffer); + #endif /* GDB_NAT_X86_LINUX_H */ diff --git a/gdb/testsuite/gdb.arch/i386-tls-regs.c b/gdb/testsuite/gdb.arch/i386-tls-regs.c new file mode 100644 index 00000000000..420d1566f20 --- /dev/null +++ b/gdb/testsuite/gdb.arch/i386-tls-regs.c @@ -0,0 +1,74 @@ +/* This testcase is part of GDB, the GNU debugger. + + Copyright 2025 Free Software Foundation, Inc. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . */ + +#include +#include +#include + +static pthread_barrier_t barrier; + +static __thread int local_var = 0; + +volatile int wait_for_gdb_p = 1; + +volatile int should_dump_core_p = 1; + +void +crash_func (void) +{ + if (should_dump_core_p) + abort (); +} + +void +spin_forever (void) +{ + while (wait_for_gdb_p) + sleep (1); +} + +void * +thread_func (void *arg) +{ + local_var = *((int*) arg); + + pthread_barrier_wait (&barrier); + + spin_forever (); +} + +int +main (void) +{ + pthread_t thr; + + if (pthread_barrier_init (&barrier, NULL, 2) != 0) + abort (); + + local_var = 1; + int i = 2; + if (pthread_create (&thr, NULL, thread_func, &i) != 0) + abort (); + + pthread_barrier_wait (&barrier); + + crash_func (); + + spin_forever (); + + return 0; +} diff --git a/gdb/testsuite/gdb.arch/i386-tls-regs.exp b/gdb/testsuite/gdb.arch/i386-tls-regs.exp new file mode 100644 index 00000000000..378a8e85f02 --- /dev/null +++ b/gdb/testsuite/gdb.arch/i386-tls-regs.exp @@ -0,0 +1,335 @@ +# Copyright 2025 Free Software Foundation, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# Check that the TLS GDT registers are available for i386 executable. + +require {is_any_target "i?86-*-linux*" "x86_64-*-linux*"} + +standard_testfile + +set options {debug pthreads} +if {![istarget "i386-*-*"]} { + lappend options "additional_flags=-m32" +} + +# Check that we can actually compile a 32-bit executable. +# +# It is possible that even with the above setting of OPTIONS, we might +# still get a 64-bit executable, for example, running on x86-64 with +# '--target_board=unix/-m64' will add the -m64 after the above -m32, +# meaning we always end up with a 64-bit executable. +# +# If the following compiles then we are getting a 32-bit executable. +if {![gdb_can_simple_compile is_lp64_target { + int dummy[sizeof (int) == 4 + && sizeof (void *) == 4 + && sizeof (long) == 4 ? 1 : -1];} \ + object $options]} { + unsupported "cannot compile 32-bit executable" + return +} + +if { [build_executable "failed to prepare" $testfile $srcfile $options] } { + return +} + +# Start an inferior and check we can read and modify the TLS registers. +proc_with_prefix test_tls_reg_availability {} { + clean_restart $::testfile + + if {![runto_main]} { + return + } + + set tls_reg_vals { "" "" "" } + gdb_test_multiple "info registers system" "check for TLS regs" { + -re "^info registers system\r\n" { + exp_continue + } + -re "^i386_tls_gdt_(\\d) \\{($::hex), ($::hex), ($::hex), ($::hex)\\}\r\n" { + set idx $expect_out(1,string) + set val0 $expect_out(2,string) + set val1 $expect_out(3,string) + set val2 $expect_out(4,string) + set val3 $expect_out(5,string) + set val [list $val0 $val1 $val2 $val3] + set tls_reg_vals [lreplace $tls_reg_vals $idx $idx $val] + exp_continue + } + -re "^$::gdb_prompt $" { + gdb_assert {[lsearch -exact $tls_reg_vals ""] == -1} $gdb_test_name + } + -re "^\[^\r\n\]+\r\n" { + exp_continue + } + } + + if { [lindex $tls_reg_vals 0] != [lindex $tls_reg_vals 1] } { + set new_vals [lindex $tls_reg_vals 0] + set old_vals [lindex $tls_reg_vals 1] + + set val0 [lindex $old_vals 0] + set val1 [lindex $new_vals 1] + set val2 [lindex $new_vals 2] + set val3 [lindex $new_vals 3] + + set new_val_str "{$val0, $val1, $val2, $val3}" + + gdb_test_no_output "set \$i386_tls_gdt_1 = $new_val_str" + set re [string_to_regexp $new_val_str] + gdb_test "print /x \$i386_tls_gdt_1" " = $re" + } + + gdb_test_no_output "set should_dump_core_p=0" + gdb_test_no_output "set wait_for_gdb_p=0" + + gdb_continue_to_end "" continue true +} + +# Start GDB using global BINFILE, load COREFILE (which must match +# BINFILE), and check that the core has two threads, that the TLS +# registers are visible in both threads, and that the TLS register +# values are different in each thread. +proc load_core_and_test_tls_regs { corefile } { + clean_restart $::testfile + + gdb_core_cmd $corefile "load corefile" + + gdb_test "info threads" \ + [multi_line \ + "\\*\\s+1\\s+Thread \[^\r\n\]+" \ + "\\s+2\\s+Thread \[^\r\n\]+"] \ + "check for two threads" + + # Record the TLS values in thread 1. + set tls_reg_vals_thr_1 { "" "" "" } + gdb_test_multiple "info registers system" "check for TLS regs thread 1" { + -re "^info registers system\r\n" { + exp_continue + } + -re "^i386_tls_gdt_(\\d) (\\{$::hex, $::hex, $::hex, $::hex\\})\r\n" { + set idx $expect_out(1,string) + set val $expect_out(2,string) + set tls_reg_vals_thr_1 [lreplace $tls_reg_vals_thr_1 $idx $idx $val] + exp_continue + } + -re "^$::gdb_prompt $" { + gdb_assert {[lsearch -exact $tls_reg_vals_thr_1 ""] == -1} $gdb_test_name + } + -re "^\[^\r\n\]+\r\n" { + exp_continue + } + } + + # Check a TLS variable in thread 1. + gdb_test "print local_var" " = 1" \ + "check TLS variable in thread 1" + + # Switch to thread 2 and confirm the values are different. + gdb_test "thread 2" + + set tls_reg_vals_thr_2 { "" "" "" } + gdb_test_multiple "info registers system" "check for TLS regs thread 2" { + -re "^info registers system\r\n" { + exp_continue + } + -re "^i386_tls_gdt_(\\d) (\\{$::hex, $::hex, $::hex, $::hex\\})\r\n" { + set idx $expect_out(1,string) + set val $expect_out(2,string) + set tls_reg_vals_thr_2 \ + [lreplace $tls_reg_vals_thr_2 $idx $idx $val] + exp_continue + } + -re "^$::gdb_prompt $" { + gdb_assert {[lsearch -exact $tls_reg_vals_thr_2 ""] == -1} \ + $gdb_test_name + } + -re "^\[^\r\n\]+\r\n" { + exp_continue + } + } + + # Check a TLS variable in thread 2. + gdb_test "print local_var" " = 2" \ + "check TLS variable in thread 2" + + set all_same [expr {[lindex $tls_reg_vals_thr_1 0] == [lindex $tls_reg_vals_thr_2 0] \ + && [lindex $tls_reg_vals_thr_1 1] == [lindex $tls_reg_vals_thr_2 1] \ + && [lindex $tls_reg_vals_thr_1 2] == [lindex $tls_reg_vals_thr_2 2]}] + gdb_assert {!$all_same} \ + "tls regs are different between threads" +} + +# Generate a core file using the gcore command. Load it into GDB and +# check we can still read the TLS registers. +proc_with_prefix test_gcore_tls {} { + + if {![gcore_cmd_available]} { + unsupported "gcore command not available" + return + } + + clean_restart $::testfile + + if {![runto_main]} { + return + } + + gdb_test_no_output "set should_dump_core_p=0" + + gdb_breakpoint crash_func + gdb_continue_to_breakpoint "stop at crash_func" + + set corefile [standard_output_file ${::testfile}.gcore] + if {![gdb_gcore_cmd $corefile "dump core"]} { + return + } + + set readelf_program [gdb_find_readelf] + set res [catch {exec $readelf_program -n $corefile} output] + if { $res != 0 } { + unresolved "unable to run readelf to check for TLS notes" + return + } + set lines [split $output \n] + set tls_count 0 + foreach line $lines { + if {[regexp "^\\s+LINUX\\s+$::hex\\s+NT_386_TLS" $line]} { + incr tls_count + } + } + gdb_assert {$tls_count == 2} \ + "expected number of TLS notes" + + load_core_and_test_tls_regs $corefile +} + +# Generate a core file using the gcore command, but before doing so, +# clear all the TLS related GDT entries. When the TLS GDT entries +# have a base address and limit of zero the kernel doesn't emit the +# NT_386_TLS note, GDB copies this behaviour. +proc_with_prefix test_gcore_no_tls {} { + + if {![gcore_cmd_available]} { + unsupported "gcore command not available" + return + } + + clean_restart $::testfile + + if {![runto_main]} { + return + } + + gdb_test_no_output "set should_dump_core_p=0" + + gdb_breakpoint crash_func + gdb_continue_to_breakpoint "stop at crash_func" + + # Clear the TLS registers in each thread. + foreach_with_prefix thr { 1 2 } { + gdb_test "thread $thr" ".*" \ + "switch thread to clear tls regs" + gdb_test_no_output "set \$i386_tls_gdt_0 = { 0x0, 0x0, 0x0, 0x28 }" + gdb_test_no_output "set \$i386_tls_gdt_1 = { 0x0, 0x0, 0x0, 0x28 }" + gdb_test_no_output "set \$i386_tls_gdt_2 = { 0x0, 0x0, 0x0, 0x28 }" + } + + set corefile [standard_output_file ${::testfile}.gcore] + if {![gdb_gcore_cmd $corefile "dump core"]} { + return + } + + # Look in the core file for NT_386_TLS entries. There should be + # none. + set readelf_program [gdb_find_readelf] + set res [catch {exec $readelf_program -n $corefile} output] + if { $res != 0 } { + unresolved "unable to run readelf to check for TLS notes" + return + } + set lines [split $output \n] + set tls_count 0 + foreach line $lines { + if {[regexp "^\\s+LINUX\\s+$::hex\\s+NT_386_TLS" $line]} { + incr tls_count + } + } + gdb_assert {$tls_count == 0} \ + "expected number of TLS notes" + + # Restart GDB and load the core file. As there are no NT_386_TLS + # entries the TLS registers should show as unavailable. + clean_restart $::testfile + + gdb_core_cmd $corefile "load corefile" + + gdb_test "info threads" \ + [multi_line \ + "\\*\\s+1\\s+Thread \[^\r\n\]+" \ + "\\s+2\\s+Thread \[^\r\n\]+"] \ + "check for two threads" + + foreach_with_prefix thr { 1 2 } { + set unavailable_tls_count 0 + set valid_tls_count 0 + gdb_test "thread $thr" ".*" \ + "switch thread to check TLS register status" + gdb_test_multiple "info registers system" "check TLS reg values" { + -re "^info registers system\r\n" { + exp_continue + } + -re "^i386_tls_gdt_\\d \\{, , , \\}\r\n" { + incr unavailable_tls_count + exp_continue + } + -re "^i386_tls_gdt_\\d \[^\r\n\]+\r\n" { + incr valid_tls_count + exp_continue + } + + -re "^$::gdb_prompt $" { + gdb_assert {$valid_tls_count == 0 && \ + $unavailable_tls_count == 3} \ + $gdb_test_name + } + -re "^\[^\r\n\]+\r\n" { + exp_continue + } + } + + # Check a TLS variable in this thread. + gdb_test "print local_var" " = $thr" \ + "check TLS variable in thread $thr" + } +} + +# Generate a native core file. Load it into GDB and check the TLS +# registers can be read. +proc_with_prefix test_native_core {} { + set corefile [core_find $::binfile] + if { $corefile eq "" } { + unsupported "unable to generate core file" + return + } + + load_core_and_test_tls_regs $corefile +} + +# Run the tests. +test_tls_reg_availability +test_gcore_tls +test_gcore_no_tls +test_native_core diff --git a/gdb/x86-linux-nat.c b/gdb/x86-linux-nat.c index 660a906cdb6..0b84f4c0ada 100644 --- a/gdb/x86-linux-nat.c +++ b/gdb/x86-linux-nat.c @@ -254,6 +254,73 @@ x86_linux_store_ssp (const regcache *regcache, const int tid) perror_with_name (_("Failed to write pl3_ssp register")); } +/* Copy from BUFFER into REGCACHE, supplying the tls gdt entry registers. + Use REGNUM to decide exactly which registers are copied. */ + +static void +i386_supply_tls_regs (regcache *regcache, int regnum, + gdb::array_view buffer) +{ + gdb_assert (buffer.size () == 3); + + for (int i = 0; i < 3; ++i) + { + if (regnum == -1 || regnum == I386_LINUX_TLS_GDT_0 + i) + regcache->raw_supply (I386_LINUX_TLS_GDT_0 + i, + buffer.slice (i, 1).data ()); + } +} + +/* Copy from REGCACHE into BUFFER, collecting the tls gdt entry + registers. Use REGNUM to decide which registers are copied. */ + +static void +i386_collect_tls_regs (regcache *regcache, int regnum, + gdb::array_view buffer) +{ + gdb_assert (buffer.size () == 3); + + for (int i = 0; i < 3; ++i) + { + if (regnum == -1 || regnum == I386_LINUX_TLS_GDT_0 + i) + regcache->raw_collect (I386_LINUX_TLS_GDT_0 + i, + buffer.slice (i, 1).data ()); + } +} + +/* See x86-linux-nat.h. */ + +void +i386_fetch_tls_regs (regcache *regcache, int tid, int regnum) +{ + user_desc tls_ud[3]; + if (!i386_ptrace_get_tls_data (tid, tls_ud)) + perror_with_name (_("Couldn't get TLS area data")); + + i386_supply_tls_regs (regcache, regnum, tls_ud); +} + +/* See x86-linux-nat.h. */ + +void +i386_store_tls_regs (regcache *regcache, int tid, int regnum) +{ + /* Read current values in to TLS_UD. */ + user_desc tls_ud[3]; + if (!i386_ptrace_get_tls_data (tid, tls_ud)) + perror_with_name (_("Couldn't get TLS area data")); + + /* Write new values from regcache into TLS_UD. Overwriting the + current values. */ + i386_collect_tls_regs (regcache, regnum, tls_ud); + + /* Write the new values back to the kernel. */ + if (!i386_ptrace_set_tls_data (tid, tls_ud)) + perror_with_name (_("Couldn't write TLS area data")); +} + + + INIT_GDB_FILE (x86_linux_nat) { /* Initialize the debug register function vectors. */ diff --git a/gdb/x86-linux-nat.h b/gdb/x86-linux-nat.h index c4556532226..16a3b35b547 100644 --- a/gdb/x86-linux-nat.h +++ b/gdb/x86-linux-nat.h @@ -25,6 +25,7 @@ #include "gdbsupport/x86-xstate.h" #include "x86-nat.h" #include "nat/x86-linux.h" +#include "i386-linux-tdep.h" struct x86_linux_nat_target : public x86_nat_target { @@ -103,4 +104,18 @@ extern void x86_linux_fetch_ssp (regcache *regcache, const int tid); extern void x86_linux_store_ssp (const regcache *regcache, const int tid); +/* Fetch the tls related registers for thread TID from the kernel and place + them into REGCACHE. If REGNUM is -1 then all 3 tls registers are + fetched, otherwise only the register matching REGNUM is fetched. A tls + register number is one for which i386_is_tls_regnum_p returns true. */ + +extern void i386_fetch_tls_regs (regcache *regcache, int tid, int regnum); + +/* Store the tls related registers for thread TID from REGCACHE back in to + the kernel. If REGNUM is -1 then all 3 tls registers are stored, + otherwise only the register matching REGNUM is stored. A tls register + number is one for which i386_is_tls_regnum_p returns true. */ + +extern void i386_store_tls_regs (regcache *regcache, int tid, int regnum); + #endif /* GDB_X86_LINUX_NAT_H */ diff --git a/gdbserver/linux-x86-low.cc b/gdbserver/linux-x86-low.cc index 2aa85ec3d9c..0fd32a22b42 100644 --- a/gdbserver/linux-x86-low.cc +++ b/gdbserver/linux-x86-low.cc @@ -53,6 +53,8 @@ #include "nat/x86-linux-dregs.h" #include "nat/x86-linux-tdesc.h" +#include + #ifdef __x86_64__ static target_desc_up tdesc_amd64_linux_no_xml; #endif @@ -132,6 +134,10 @@ public: int get_ipa_tdesc_idx () override; + /* Override these to provide access to i386 TLS state. */ + void fetch_registers (regcache *regcache, int regno) override; + void store_registers (regcache *regcache, int regno) override; + protected: void low_arch_setup () override; @@ -573,6 +579,105 @@ static struct regset_info x86_regsets[] = NULL_REGSET }; +/* Fetch TLS area data from the kernel and copy it into REGCACHE. REGNO + indicates which TLS area register is wanted, or -1 for all of them. + + If anything goes wrong then this function will return without updating + REGCACHE. */ + +static void +fetch_tls_area_register (regcache *regcache, int regno) +{ + int tid = current_thread->id.lwp (); + + /* Fetch all the TLS area data from the kernel. */ + user_desc tls_ud[3]; + if (!i386_ptrace_get_tls_data (tid, tls_ud)) + { + warning (_("failed to read TLS GDT entries for register read")); + return; + } + + /* Now copy the values from TLS_UD back into the register cache. */ + int tls_regno[3] = { + find_regno (regcache->tdesc, "i386_tls_gdt_0"), + find_regno (regcache->tdesc, "i386_tls_gdt_1"), + find_regno (regcache->tdesc, "i386_tls_gdt_2") + }; + + for (int i = 0; i < std::size (tls_regno); ++i) + supply_register (regcache, tls_regno[i], &tls_ud[i]); +} + +/* See class declaration above. */ + +void +x86_target::fetch_registers (regcache *regcache, int regno) +{ + linux_process_target::fetch_registers (regcache, regno); + +#ifdef __x86_64__ + if (!is_64bit_tdesc (current_thread)) +#endif + { + fetch_tls_area_register (regcache, regno); + } +} + +/* Copy TLS area data from REGCACHE back to the kernel. REGNO indicates + which TLS area register should be copied, or -1 for all of them. If + anything goes wrong then return immediately; some of the register may + have been written back to the kernel in this case. */ + +static void +store_tls_area_registers (regcache *regcache, int regno) +{ + int tid = current_thread->id.lwp (); + + /* Read current TLS area data from the kernel into TLS_UD. We then + overwrite this with values from REGCACHE, and finally, copy the + updated values back to the kernel. */ + user_desc tls_ud[3]; + if (!i386_ptrace_get_tls_data (tid, tls_ud)) + { + warning (_("failed to read TLS GDT entries for register store")); + return; + } + + int tls_regno[] = { + find_regno (regcache->tdesc, "i386_tls_gdt_0"), + find_regno (regcache->tdesc, "i386_tls_gdt_1"), + find_regno (regcache->tdesc, "i386_tls_gdt_2") + }; + + /* Now copy data from REGCACHE over the top of the values written + into TLS_UD. */ + for (int i = 0; i < std::size (tls_regno); ++i) + collect_register (regcache, tls_regno[i], &tls_ud[i]); + + /* And write the contents of TLS_UD back to the kernel. We ignore the + return value from this call; if anything went wrong then there's + nothing we can do about it. */ + if (!i386_ptrace_set_tls_data (tid, tls_ud)) + warning (_("failed to write updated TLS GDT entries for register store")); + +} + +/* See class declaration above. */ + +void +x86_target::store_registers (regcache *regcache, int regno) +{ + linux_process_target::store_registers (regcache, regno); + +#ifdef __x86_64__ + if (!is_64bit_tdesc (current_thread)) +#endif + { + store_tls_area_registers (regcache, regno); + } +} + bool x86_target::low_supports_breakpoints () {