gdb: Introduce user-friendly namespace identifier for "info shared"

GDB has had basic support for linkage namespaces for some time already,
but only in the sense of managing multiple copies of the same shared
object being loaded, and a very fragile way to find the correct copy of
a symbol (see PR shlibs/32054).

This commit is the first step in improving the user experience around
multiple namespace support. It introduces a user-friendly identifier for
namespaces, in the format [[<number>]], that will keep consistent between
dlmopen and dlclose calls. The plan is for this identifier to be usable
in expressions like `print [[1]]::var` to find a specific instance of a
symbol, and so the identifier must not be a valid C++ or Ada namespace
identifier, otherwise disambiguation becomes a problem. Support for
those expressions has not been implemented yet, it is only mentioned to
explain why the identifier looks like this.

This syntax was chosen based on the C attributes, since nothing in GDB
uses a similar syntax that could confuse users. Other syntax options
that were explored were "#<number>" and "@<number>". The former was
abandoned because when printing a frame, the frame number is also
printed with #<number>, so in a lot of the context in which that the
identifier would show up, it appears in a confusing way. The latter
clashes with the array printing syntax, and I believe that the having
"@N::foo" working completely differently to "foo@2" would also lead to a
bad user experience.

The namespace identifiers are stored via a vector inside svr4_info
object. The vector stores the address of the r_debug objects used by
glibc to identify each namespace, and the user-friendly ID is the index
of the r_debug in the vector. This commit also introduces a set storing
the indices of active namespaces. The glibc I used to develop this patch
(glibc 2.40 on Fedora 41) doesn't allow an SO to be loaded into a
deactivated namespace, and requesting a new namespace when a namespace
was previously closed will reuse that namespace. Because of how this is
implemented, this patch lets GDB easily track the exact namespace IDs
that the inferior will see.

Finally, two new solib_ops function pointers were added, find_solib_ns
and num_active_namespaces, to allow code outside of solib-svr4 to find
and use the namespace identifiers and the number of namespaces,
respectively. As a sanity check, the command `info sharedlibrary` has
been changed to display the namespace identifier when the inferior has
more than one active namespace. With this final change, a couple of tests
had to be tweaked to handle the possible new column, and a new test has
been created to make sure that the column appears and disappears as
needed, and that GDB can track the value of the LMID for namespaces.

Approved-by: Kevin Buettner <kevinb@redhat.com>
This commit is contained in:
Guinevere Larsen 2025-03-13 10:20:53 -03:00
parent fcdce14251
commit 29bff2380e
11 changed files with 348 additions and 17 deletions

View File

@ -35,6 +35,11 @@
a -h or --help option, which prints each options and a brief
description.
* On systems that support linkage namespaces, the output of the command
"info sharedlibraries" may add one more column, NS, which identifies the
namespace into which the library was loaded, if more than one namespace
is active.
* New commands
maintenance check psymtabs

View File

@ -22172,6 +22172,11 @@ be determined then the address range for the @code{.text} section from
the library will be listed. If the @code{.text} section cannot be
found then no addresses will be listed.
On systems that support linkage namespaces, the output includes an
additional column @code{NS} if the inferior has more than one active
namespace when the command is used. This column the linkage namespace
that the shared library was loaded into.
@kindex info dll
@item info dll @var{regex}
This is an alias of @code{info sharedlibrary}.

View File

@ -405,11 +405,54 @@ struct svr4_info
The special entry zero is reserved for a linear list to support
gdbstubs that do not support namespaces. */
std::map<CORE_ADDR, std::vector<svr4_so>> solib_lists;
/* Mapping between r_debug[_ext] addresses and a user-friendly
identifier for the namespace. A vector is used to make it
easy to assign new internal IDs to namespaces.
For gdbservers that don't support namespaces, the first (and only)
entry of the vector will be 0.
A note on consistency. We can't make the IDs be consistent before
and after the initial relocation of the inferior (when the global
_r_debug is relocated, as mentioned in the previous comment). It is
likely that this is a non-issue, since the inferior can't have called
dlmopen yet, but I think it is worth noting.
The only issue I am aware at this point is that, if when parsing an
XML file, we read an LMID that given by an XML file (and read in
library_list_start_library) is the identifier obtained with dlinfo
instead of the address of r_debug[_ext], and after attaching the
inferior adds another SO to that namespace, we might double-count it
since we won't have access to the LMID later on. However, this is
already a problem with the existing solib_lists code. */
std::vector<CORE_ADDR> namespace_id;
/* This identifies which namespaces are active. A namespace is considered
active when there is at least one shared object loaded into it. */
std::set<size_t> active_namespaces;
};
/* Per-program-space data key. */
static const registry<program_space>::key<svr4_info> solib_svr4_pspace_data;
/* Check if the lmid address is already assigned an ID in the svr4_info,
and if not, assign it one and add it to the list of known namespaces. */
static void
svr4_maybe_add_namespace (svr4_info *info, CORE_ADDR lmid)
{
int i;
for (i = 0; i < info->namespace_id.size (); i++)
{
if (info->namespace_id[i] == lmid)
break;
}
if (i == info->namespace_id.size ())
info->namespace_id.push_back (lmid);
info->active_namespaces.insert (i);
}
/* Return whether DEBUG_BASE is the default namespace of INFO. */
static bool
@ -1041,14 +1084,18 @@ library_list_start_library (struct gdb_xml_parser *parser,
/* Older versions did not supply lmid. Put the element into the flat
list of the special namespace zero in that case. */
gdb_xml_value *at_lmid = xml_find_attribute (attributes, "lmid");
svr4_info *info = get_svr4_info (current_program_space);
if (at_lmid == nullptr)
solist = list->cur_list;
{
solist = list->cur_list;
svr4_maybe_add_namespace (info, 0);
}
else
{
ULONGEST lmid = *(ULONGEST *) at_lmid->value.get ();
solist = &list->solib_lists[lmid];
svr4_maybe_add_namespace (info, lmid);
}
solist->emplace_back (name, std::move (li));
}
@ -1286,6 +1333,8 @@ svr4_current_sos_direct (struct svr4_info *info)
/* Remove any old libraries. We're going to read them back in again. */
info->solib_lists.clear ();
info->active_namespaces.clear ();
/* Fall back to manual examination of the target if the packet is not
supported or gdbserver failed to find DT_DEBUG. gdb.server/solib-list.exp
tests a case where gdbserver cannot find the shared libraries list while
@ -1333,7 +1382,10 @@ svr4_current_sos_direct (struct svr4_info *info)
ignore_first = true;
auto cleanup = make_scope_exit ([info] ()
{ info->solib_lists.clear (); });
{
info->solib_lists.clear ();
info->active_namespaces.clear ();
});
/* Collect the sos in each namespace. */
CORE_ADDR debug_base = info->debug_base;
@ -1343,8 +1395,11 @@ svr4_current_sos_direct (struct svr4_info *info)
/* Walk the inferior's link map list, and build our so_list list. */
lm = solib_svr4_r_map (debug_base);
if (lm != 0)
svr4_read_so_list (info, lm, 0, info->solib_lists[debug_base],
ignore_first);
{
svr4_maybe_add_namespace (info, debug_base);
svr4_read_so_list (info, lm, 0, info->solib_lists[debug_base],
ignore_first);
}
}
/* On Solaris, the dynamic linker is not in the normal list of
@ -1361,8 +1416,11 @@ svr4_current_sos_direct (struct svr4_info *info)
{
/* Add the dynamic linker's namespace unless we already did. */
if (info->solib_lists.find (debug_base) == info->solib_lists.end ())
svr4_read_so_list (info, debug_base, 0, info->solib_lists[debug_base],
0);
{
svr4_maybe_add_namespace (info, debug_base);
svr4_read_so_list (info, debug_base, 0, info->solib_lists[debug_base],
0);
}
}
cleanup.release ();
@ -1778,6 +1836,10 @@ solist_update_incremental (svr4_info *info, CORE_ADDR debug_base,
return 0;
prev_lm = 0;
/* If the list is empty, we are seeing a new namespace for the
first time, so assign it an internal ID. */
svr4_maybe_add_namespace (info, debug_base);
}
else
prev_lm = solist.back ().lm_info->lm_addr;
@ -1845,6 +1907,8 @@ disable_probes_interface (svr4_info *info)
free_probes_table (info);
info->solib_lists.clear ();
info->namespace_id.clear ();
info->active_namespaces.clear ();
}
/* Update the solib list as appropriate when using the
@ -3042,6 +3106,8 @@ svr4_solib_create_inferior_hook (int from_tty)
/* Clear the probes-based interface's state. */
free_probes_table (info);
info->solib_lists.clear ();
info->namespace_id.clear ();
info->active_namespaces.clear ();
/* Relocate the main executable if necessary. */
svr4_relocate_main_executable ();
@ -3460,6 +3526,32 @@ svr4_find_solib_addr (solib &so)
return li->l_addr_inferior;
}
/* See solib_ops::find_solib_ns in solist.h. */
static int
svr4_find_solib_ns (const solib &so)
{
CORE_ADDR debug_base = find_debug_base_for_solib (&so);
svr4_info *info = get_svr4_info (current_program_space);
for (int i = 0; i < info->namespace_id.size (); i++)
{
if (info->namespace_id[i] == debug_base)
{
gdb_assert (info->active_namespaces.count (i) == 1);
return i;
}
}
error (_("No namespace found"));
}
/* see solib_ops::num_active_namespaces in solist.h. */
static int
svr4_num_active_namespaces ()
{
svr4_info *info = get_svr4_info (current_program_space);
return info->active_namespaces.size ();
}
const struct solib_ops svr4_so_ops =
{
svr4_relocate_section_addresses,
@ -3475,6 +3567,8 @@ const struct solib_ops svr4_so_ops =
svr4_update_solib_event_breakpoints,
svr4_handle_solib_event,
svr4_find_solib_addr,
svr4_find_solib_ns,
svr4_num_active_namespaces,
};
void _initialize_svr4_solib ();

View File

@ -1051,12 +1051,24 @@ info_sharedlibrary_command (const char *pattern, int from_tty)
}
}
/* How many columns the table should have. If the inferior has
more than one namespace active, we need a column to show that. */
int num_cols = 4;
const solib_ops *ops = gdbarch_so_ops (gdbarch);
if (ops->num_active_namespaces != nullptr
&& ops->num_active_namespaces () > 1)
num_cols++;
{
ui_out_emit_table table_emitter (uiout, 4, nr_libs, "SharedLibraryTable");
ui_out_emit_table table_emitter (uiout, num_cols, nr_libs,
"SharedLibraryTable");
/* The "- 1" is because ui_out adds one space between columns. */
uiout->table_header (addr_width - 1, ui_left, "from", "From");
uiout->table_header (addr_width - 1, ui_left, "to", "To");
if (ops->num_active_namespaces != nullptr
&& ops->num_active_namespaces () > 1)
uiout->table_header (5, ui_left, "namespace", "NS");
uiout->table_header (12 - 1, ui_left, "syms-read", "Syms Read");
uiout->table_header (0, ui_noalign, "name", "Shared Object Library");
@ -1083,6 +1095,19 @@ info_sharedlibrary_command (const char *pattern, int from_tty)
uiout->field_skip ("to");
}
if (ops->num_active_namespaces != nullptr
&& ops->num_active_namespaces ()> 1)
{
try
{
uiout->field_fmt ("namespace", "[[%d]]", ops->find_solib_ns (so));
}
catch (const gdb_exception_error &er)
{
uiout->field_skip ("namespace");
}
}
if (!top_level_interpreter ()->interp_ui_out ()->is_mi_like_p ()
&& so.symbols_loaded && !objfile_has_symbols (so.objfile))
{

View File

@ -180,6 +180,20 @@ struct solib_ops
name). */
std::optional<CORE_ADDR> (*find_solib_addr) (solib &so);
/* Return which linker namespace contains the current so.
If the linker or libc does not support linkage namespaces at all
(which is basically all of them but solib-svr4), this function should
be set to nullptr, so that "info shared" won't add an unnecessary
column.
If the namespace can not be determined (such as when we're stepping
though the dynamic linker), this function should throw a
gdb_exception_error. */
int (*find_solib_ns) (const solib &so);
/* Returns the number of active namespaces in the inferior. */
int (*num_active_namespaces) ();
};
/* A unique pointer to a so_list. */

View File

@ -35,7 +35,7 @@ if ![runto_main] {
}
set test "sanity check info shared"
gdb_test_multiple "info shared" $test {
-re "From\[ \t\]+To\[ \t\]+Syms Read\[ \t\]+Shared Object Library\r\n0x.*\r\n$gdb_prompt $" {
-re "From\[ \t\]+To(\\s+NS)?\[ \t\]+Syms Read\[ \t\]+Shared Object Library\r\n0x.*\r\n$gdb_prompt $" {
pass $test
}
-re "No shared libraries loaded at this time\\.\r\n$gdb_prompt $" {
@ -62,6 +62,6 @@ if { ![gdb_attach $testpid] } {
return
}
gdb_test "set architecture $arch" "The target architecture is set to \"$arch\"\\."
gdb_test "info shared" "From\[ \t\]+To\[ \t\]+Syms Read\[ \t\]+Shared Object Library\r\n0x.*"
gdb_test "info shared" "From\[ \t\]+To(\\s+NS)?\[ \t\]+Syms Read\[ \t\]+Shared Object Library\r\n0x.*"
kill_wait_spawned_process $test_spawn_id

View File

@ -0,0 +1,28 @@
/* 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 <http://www.gnu.org/licenses/>.
*/
int gdb_dlmopen_glob = 0;
__attribute__((visibility ("default")))
int
inc (int n)
{
int amount = gdb_dlmopen_glob;
return n + amount; /* bp.inc. */
}

View File

@ -0,0 +1,54 @@
/* 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 <http://www.gnu.org/licenses/>.
*/
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stddef.h>
#include <assert.h>
#include <unistd.h>
#include <stdio.h>
int
main (void)
{
void *handle[4];
int (*fun) (int);
Lmid_t lmid;
int dl;
handle[0] = dlmopen (LM_ID_NEWLM, DSO_NAME, RTLD_LAZY | RTLD_LOCAL);
assert (handle[0] != NULL);
handle[1] = dlmopen (LM_ID_NEWLM, DSO_NAME, RTLD_LAZY | RTLD_LOCAL);
assert (handle[1] != NULL);
handle[2] = dlmopen (LM_ID_NEWLM, DSO_NAME, RTLD_LAZY | RTLD_LOCAL);
assert (handle[2] != NULL);
dlclose (handle[0]); /* TAG: first dlclose */
dlclose (handle[1]); /* TAG: second dlclose */
dlclose (handle[2]); /* TAG: third dlclose */
handle[3] = dlmopen (LM_ID_NEWLM, DSO_NAME, RTLD_LAZY | RTLD_LOCAL);
dlinfo (handle[3], RTLD_DI_LMID, &lmid);
dlclose (handle[3]); /* TAG: fourth dlclose */
return 0;
}

View File

@ -0,0 +1,106 @@
# 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 <http://www.gnu.org/licenses/>.
#
#
# Test several things related to handling linker namespaces:
# * That the user-facing namespace ID is consistent;
require allow_dlmopen_tests
standard_testfile -main.c -lib.c
set srcfile_lib $srcfile2
set binfile_lib [standard_output_file dlmopen-lib.so]
if { [build_executable "build shlib" $binfile_lib $srcfile_lib \
[list debug shlib]] == -1 } {
return
}
if { [build_executable "failed to build" $testfile $srcfile \
[list additional_flags=-DDSO_NAME=\"$binfile_lib\" \
shlib_load debug]] } {
return
}
# Run the command "info sharedlibrary" and get the first namespace
# for the so
proc get_first_so_ns {} {
set ns -1
gdb_test_multiple "info sharedlibrary" "get SO namespace" -lbl {
-re "From\\s+To\\s+\(NS\\s+\)?Syms\\s+Read\\s+Shared Object Library\r\n" {
exp_continue
}
-re "^$::hex\\s+$::hex\\s+\\\[\\\[($::decimal)\\\]\\\]\\s+\[^\r\n]+$::binfile_lib.*" {
set ns $expect_out(1,string)
}
-re "^$::gdb_prompt $" {
}
-re "^\[^\r\n\]+\r\n" {
exp_continue
}
}
return $ns
}
# Run the tests relating to the command "info sharedlibrary", to
# verify that the namespace ID is consistent.
proc test_info_shared {} {
clean_restart $::binfile
if { ![runto_main] } {
return
}
# First test that we don't print a namespace column at the start.
gdb_test "info sharedlibrary" \
"From\\s+To\\s+Syms\\s+Read\\s+Shared Object Library.*" \
"before loading anything"
gdb_breakpoint [gdb_get_line_number "TAG: first dlclose"]
gdb_continue_to_breakpoint "TAG: first dlclose"
# Next, test that we *do* print a namespace column after loading SOs.
gdb_test "info sharedlibrary" \
"From\\s+To\\s+NS\\s+Syms\\s+Read\\s+Shared Object Library.*" \
"after loading everything"
gdb_assert {[get_first_so_ns] == 1} "before closing any library"
gdb_test "next" ".*second dlclose.*" "close first library"
gdb_assert {[get_first_so_ns] == 2} "after closing one library"
gdb_test "next" ".*third dlclose.*" "close second library"
gdb_assert {[get_first_so_ns] == 3} "before closing two libraries"
gdb_breakpoint [gdb_get_line_number "TAG: fourth dlclose"]
gdb_continue_to_breakpoint "TAG: fourth dlclose"
# As of writing this test, glibc's LMID is just an index on an array of
# namespaces. After closing a namespace, requesting a new one will
# return the index of the lowest-closed namespace, so this will likely
# be namespace 1, and because of glibc's reuse of the r_debug object,
# GDB should be able to assign the same number.
gdb_assert {[get_first_so_ns] == [get_integer_valueof "lmid" "-1"]} \
"reopen a namespace"
gdb_test "next" ".*return 0.*" "final namespace inactive"
gdb_test "info sharedlibrary" \
"From\\s+To\\s+Syms\\s+Read\\s+Shared Object Library.*" \
"after unloading everything"
}
test_info_shared

View File

@ -106,7 +106,7 @@ proc check_dso_count { dso num } {
set count 0
gdb_test_multiple "info shared" "info shared" {
-re "$hex $hex Yes \[^\r\n\]*$dso\r\n" {
-re "$hex $hex \(\[\[$::decimal\]\]\\s+\)\?Yes \[^\r\n\]*$dso\r\n" {
# use longer form so debug remote does not interfere
set count [expr $count + 1]
exp_continue
@ -233,12 +233,12 @@ proc get_dyld_info {} {
set dyld_count 0
set dyld_start_addr ""
gdb_test_multiple "info sharedlibrary" "" {
-re "From\\s+To\\s+Syms\\s+Read\\s+Shared Object Library\r\n" {
-re "From\\s+To\\s+\(NS\\s+\)?Syms\\s+Read\\s+Shared Object Library\r\n" {
exp_continue
}
-re "^($::hex)\\s+$::hex\\s+\[^/\]+(/\[^\r\n\]+)\r\n" {
-re "^($::hex)\\s+$::hex\\s+\(\#$::decimal\\s+\)?\[^/\]+(/\[^\r\n\]+)\r\n" {
set addr $expect_out(1,string)
set lib $expect_out(2,string)
set lib $expect_out(3,string)
if { [is_dyln $lib] } {
# This looks like it might be the dynamic linker.

View File

@ -81,12 +81,12 @@ proc get_dyld_info {} {
set dyld_count 0
set dyld_start_addr ""
gdb_test_multiple "info sharedlibrary" "" {
-re "~\"From\\s+To\\s+Syms\\s+Read\\s+Shared Object Library\\\\n\"\r\n" {
-re "~\"From\\s+To(\\s+NS)?\\s+Syms\\s+Read\\s+Shared Object Library\\\\n\"\r\n" {
exp_continue
}
-re "^~\"($::hex)\\s+$::hex\\s+\[^/\]+(/\[^\r\n\]+)\\\\n\"\r\n" {
-re "^~\"($::hex)\\s+${::hex}(\\s+$::decimal)?\\s+\[^/\]+(/\[^\r\n\]+)\\\\n\"\r\n" {
set addr $expect_out(1,string)
set lib $expect_out(2,string)
set lib $expect_out(3,string)
if { [is_dyln $lib] } {
# This looks like it might be the dynamic linker.