[2/3] gdb: include a still-mapped flag in solib unload notification

Message ID 552eb20db6848505d7c985addf1a4acb04867871.1735041587.git.aburgess@redhat.com
State New
Headers
Series Don't disable breakpoints in still loaded libraries |

Commit Message

Andrew Burgess Dec. 24, 2024, 12:05 p.m. UTC
  Consider the gdb.base/dlmopen.exp test case.  The executable in this
test uses dlmopen to load libraries into multiple linker namespaces.

When a library is loaded into a separate namespace, its dependencies
are also loaded into that namespace.

This means that an inferior can have multiple copies of some
libraries, including the dynamic linker, loaded at once.

However, glibc optimises at least the dynamic linker case.  Though the
library appears to be mapped multiple times (it is in the inferiors
solib list multiple times), there is really only one copy mapped into
the inferior's address space.  Here is the 'info sharedlibrary' output
on an x86-64/Linux machine once all the libraries are loaded:

  (gdb) info sharedlibrary
  From                To                  Syms Read   Shared Object Library
  0x00007ffff7fca000  0x00007ffff7ff03f5  Yes         /lib64/ld-linux-x86-64.so.2
  0x00007ffff7eda3d0  0x00007ffff7f4e898  Yes         /lib64/libm.so.6
  0x00007ffff7d0e800  0x00007ffff7e6dccd  Yes         /lib64/libc.so.6
  0x00007ffff7fbd040  0x00007ffff7fbd116  Yes         /tmp/build/gdb/testsuite/outputs/gdb.base/dlmopen/dlmopen-lib.1.so
  0x00007ffff7fb8040  0x00007ffff7fb80f9  Yes         /tmp/build/gdb/testsuite/outputs/gdb.base/dlmopen/dlmopen-lib-dep.so
  0x00007ffff7bfe3d0  0x00007ffff7c72898  Yes         /lib64/libm.so.6
  0x00007ffff7a32800  0x00007ffff7b91ccd  Yes         /lib64/libc.so.6
  0x00007ffff7fca000  0x00007ffff7ff03f5  Yes         /lib64/ld-linux-x86-64.so.2
  0x00007ffff7fb3040  0x00007ffff7fb3116  Yes         /tmp/build/gdb/testsuite/outputs/gdb.base/dlmopen/dlmopen-lib.1.so
  0x00007ffff7fae040  0x00007ffff7fae0f9  Yes         /tmp/build/gdb/testsuite/outputs/gdb.base/dlmopen/dlmopen-lib-dep.so
  0x00007ffff7ce1040  0x00007ffff7ce1116  Yes         /tmp/build/gdb/testsuite/outputs/gdb.base/dlmopen/dlmopen-lib.1.so
  0x00007ffff7cdc040  0x00007ffff7cdc0f9  Yes         /tmp/build/gdb/testsuite/outputs/gdb.base/dlmopen/dlmopen-lib-dep.so
  0x00007ffff79253d0  0x00007ffff7999898  Yes         /lib64/libm.so.6
  0x00007ffff7759800  0x00007ffff78b8ccd  Yes         /lib64/libc.so.6
  0x00007ffff7fca000  0x00007ffff7ff03f5  Yes         /lib64/ld-linux-x86-64.so.2
  0x00007ffff7cd7040  0x00007ffff7cd7116  Yes         /tmp/build/gdb/testsuite/outputs/gdb.base/dlmopen/dlmopen-lib.2.so

Notice that every copy of /lib64/ld-linux-x86-64.so.2 is mapped at the
same address.

As the inferior closes the libraries that it loaded, the various
copies of the dynamic linker will also be unloaded.

Currently, when this happens GDB calls notify_solib_unloaded, which
triggers the gdb::observers::solib_unloaded observer.  This observer
will call disable_breakpoints_in_unloaded_shlib (in breakpoint.c),
which disables any breakpoints in the unloaded solib.

The problem with this, is that, when the dynamic linker (or any solib)
is only really mapped once as is the case here, we only want to
disable breakpoints in the library when the last instance of the
library is unloaded.

The first idea that comes to mind is that GDB should not emit the
solib_unloaded notification if a shared library is still in use,
however, this could break MI consumers.

Currently, every time a copy of ld-linux-x86-64.so.2 is unloaded,
GDB's MI interpreter will emit a =library-unloaded event.  An MI
consumer might use this to update the library list that it displays to
the user, and fewer notify_solib_unloaded calls will mean fewer MI
events, which will mean the MI consumer's library list could get out
of sync with GDB.

Instead I propose that we extend GDB's solib_unloaded event to add a
new flag.  The new flag indicates if the library mapping is still in
use within the inferior.  Now the MI will continue to emit the
expected =library-unloaded events, but
disable_breakpoints_in_unloaded_shlib can check the new flag, when it
is true (indicating that the library is still mapped into the
inferior), no breakpoints should be disabled.

The other user of the solib_unloaded observer, in bsd-uthread.c,
should, I think, do nothing if the mapping is still in use.

Most of the changes in this commit relate to passing the new flag
around in the event.  The interesting changes are mostly in solib.c,
with the new flag being read in breakpoint.c and bsd-uthread.c.
---
 gdb/breakpoint.c                          |  11 +-
 gdb/bsd-uthread.c                         |   5 +-
 gdb/interps.c                             |   4 +-
 gdb/interps.h                             |  14 +-
 gdb/mi/mi-interp.c                        |   2 +-
 gdb/mi/mi-interp.h                        |   2 +-
 gdb/observable.h                          |   3 +-
 gdb/solib.c                               |  30 +++-
 gdb/testsuite/gdb.base/dlmopen.exp        | 167 +++++++++++++++++
 gdb/testsuite/gdb.mi/mi-dlmopen-lib-dep.c |  21 +++
 gdb/testsuite/gdb.mi/mi-dlmopen-lib.c     |  28 +++
 gdb/testsuite/gdb.mi/mi-dlmopen.c         |  59 ++++++
 gdb/testsuite/gdb.mi/mi-dlmopen.exp       | 209 ++++++++++++++++++++++
 gdb/testsuite/lib/gdb.exp                 |  33 ++++
 gdb/testsuite/lib/prelink-support.exp     |  33 ----
 15 files changed, 566 insertions(+), 55 deletions(-)
 create mode 100644 gdb/testsuite/gdb.mi/mi-dlmopen-lib-dep.c
 create mode 100644 gdb/testsuite/gdb.mi/mi-dlmopen-lib.c
 create mode 100644 gdb/testsuite/gdb.mi/mi-dlmopen.c
 create mode 100644 gdb/testsuite/gdb.mi/mi-dlmopen.exp
  

Patch

diff --git a/gdb/breakpoint.c b/gdb/breakpoint.c
index e7fdeca91ff..6457285caeb 100644
--- a/gdb/breakpoint.c
+++ b/gdb/breakpoint.c
@@ -8090,11 +8090,18 @@  disable_breakpoints_in_shlibs (program_space *pspace)
 
 /* Disable any breakpoints and tracepoints that are in SOLIB upon
    notification of unloaded_shlib.  Only apply to enabled breakpoints,
-   disabled ones can just stay disabled.  */
+   disabled ones can just stay disabled.
+
+   When STILL_IN_USE is true, SOLIB hasn't really been unmapped from
+   the inferior.  In this case, don't disable anything.  */
 
 static void
-disable_breakpoints_in_unloaded_shlib (program_space *pspace, const solib &solib)
+disable_breakpoints_in_unloaded_shlib (program_space *pspace, const solib &solib,
+				       bool still_in_use)
 {
+  if (still_in_use)
+    return;
+
   bool disabled_shlib_breaks = false;
 
   for (bp_location *loc : all_bp_locations ())
diff --git a/gdb/bsd-uthread.c b/gdb/bsd-uthread.c
index eb1ed421abc..67db0ca4bda 100644
--- a/gdb/bsd-uthread.c
+++ b/gdb/bsd-uthread.c
@@ -294,9 +294,10 @@  bsd_uthread_solib_loaded (solib &so)
 }
 
 static void
-bsd_uthread_solib_unloaded (program_space *pspace, const solib &so)
+bsd_uthread_solib_unloaded (program_space *pspace, const solib &so,
+			    bool still_in_use)
 {
-  if (bsd_uthread_solib_name.empty ())
+  if (bsd_uthread_solib_name.empty () || still_in_use)
     return;
 
   if (so.so_original_name == bsd_uthread_solib_name)
diff --git a/gdb/interps.c b/gdb/interps.c
index bd65d1a5cc6..8b8b5782966 100644
--- a/gdb/interps.c
+++ b/gdb/interps.c
@@ -496,9 +496,9 @@  interps_notify_solib_loaded (const solib &so)
 /* See interps.h.  */
 
 void
-interps_notify_solib_unloaded (const solib &so)
+interps_notify_solib_unloaded (const solib &so, bool still_in_use)
 {
-  interps_notify (&interp::on_solib_unloaded, so);
+  interps_notify (&interp::on_solib_unloaded, so, still_in_use);
 }
 
 /* See interps.h.  */
diff --git a/gdb/interps.h b/gdb/interps.h
index 987465c894b..2dcf244dbd7 100644
--- a/gdb/interps.h
+++ b/gdb/interps.h
@@ -153,8 +153,11 @@  class interp : public intrusive_list_node<interp>
   /* Notify the interpreter that solib SO has been loaded.  */
   virtual void on_solib_loaded (const solib &so) {}
 
-  /* Notify the interpreter that solib SO has been unloaded.  */
-  virtual void on_solib_unloaded (const solib &so) {}
+  /* Notify the interpreter that solib SO has been unloaded.  When
+     STILL_IN_USE is true, the objfile backing SO is still in use,
+     this indicates that SO was loaded multiple times, but only mapped
+     in once (the mapping was reused).  */
+  virtual void on_solib_unloaded (const solib &so, bool still_in_use) {}
 
   /* Notify the interpreter that a command it is executing is about to cause
      the inferior to proceed.  */
@@ -332,8 +335,11 @@  extern void interps_notify_target_resumed (ptid_t ptid);
 /* Notify all interpreters that solib SO has been loaded.  */
 extern void interps_notify_solib_loaded (const solib &so);
 
-/* Notify all interpreters that solib SO has been unloaded.  */
-extern void interps_notify_solib_unloaded (const solib &so);
+/* Notify all interpreters that solib SO has been unloaded.  When
+   STILL_IN_USE is true, the objfile backing SO is still in use, this
+   indicates that SO was loaded multiple times, but only mapped in
+   once (the mapping was reused).  */
+extern void interps_notify_solib_unloaded (const solib &so, bool still_in_use);
 
 /* Notify all interpreters that the selected traceframe changed.
 
diff --git a/gdb/mi/mi-interp.c b/gdb/mi/mi-interp.c
index ff4a0ff3202..9512706d02f 100644
--- a/gdb/mi/mi-interp.c
+++ b/gdb/mi/mi-interp.c
@@ -760,7 +760,7 @@  mi_interp::on_solib_loaded (const solib &solib)
 }
 
 void
-mi_interp::on_solib_unloaded (const solib &solib)
+mi_interp::on_solib_unloaded (const solib &solib, bool still_in_use)
 {
   ui_out *uiout = this->interp_ui_out ();
 
diff --git a/gdb/mi/mi-interp.h b/gdb/mi/mi-interp.h
index 8f5eee6f558..beff1c1a98c 100644
--- a/gdb/mi/mi-interp.h
+++ b/gdb/mi/mi-interp.h
@@ -61,7 +61,7 @@  class mi_interp final : public interp
 			  const char *format) override;
   void on_target_resumed (ptid_t ptid) override;
   void on_solib_loaded (const solib &so) override;
-  void on_solib_unloaded (const solib &so) override;
+  void on_solib_unloaded (const solib &so, bool still_in_use) override;
   void on_about_to_proceed () override;
   void on_traceframe_changed (int tfnum, int tpnum) override;
   void on_tsv_created (const trace_state_variable *tsv) override;
diff --git a/gdb/observable.h b/gdb/observable.h
index 077014c8401..deea1ffe7ff 100644
--- a/gdb/observable.h
+++ b/gdb/observable.h
@@ -104,7 +104,8 @@  extern observable<solib &/* solib */> solib_loaded;
 /* The shared library SOLIB has been unloaded from program space PSPACE.
    Note  when gdb calls this observer, the library's symbols have not
    been unloaded yet, and thus are still available.  */
-extern observable<program_space *, const solib &/* solib */> solib_unloaded;
+extern observable<program_space *, const solib &/* solib */,
+		  bool /* still_in_use */> solib_unloaded;
 
 /* The symbol file specified by OBJFILE has been loaded.  */
 extern observable<struct objfile */* objfile */> new_objfile;
diff --git a/gdb/solib.c b/gdb/solib.c
index 4a04f1ddb1f..eee02ca5d24 100644
--- a/gdb/solib.c
+++ b/gdb/solib.c
@@ -691,13 +691,17 @@  notify_solib_loaded (solib &so)
   gdb::observers::solib_loaded.notify (so);
 }
 
-/* Notify interpreters and observers that solib SO has been unloaded.  */
+/* Notify interpreters and observers that solib SO has been unloaded.
+   When STILL_IN_USE is true, the objfile backing SO is still in use,
+   this indicates that SO was loaded multiple times, but only mapped
+   in once (the mapping was reused).  */
 
 static void
-notify_solib_unloaded (program_space *pspace, const solib &so)
+notify_solib_unloaded (program_space *pspace, const solib &so,
+		       bool still_in_use)
 {
-  interps_notify_solib_unloaded (so);
-  gdb::observers::solib_unloaded.notify (pspace, so);
+  interps_notify_solib_unloaded (so, still_in_use);
+  gdb::observers::solib_unloaded.notify (pspace, so, still_in_use);
 }
 
 /* See solib.h.  */
@@ -792,18 +796,23 @@  update_solib_list (int from_tty)
       /* If it's not on the inferior's list, remove it from GDB's tables.  */
       else
 	{
+	  bool still_in_use
+	    = (gdb_iter->objfile != nullptr
+	       && solib_used (current_program_space, *gdb_iter));
+
 	  /* Notify any observer that the shared object has been
 	     unloaded before we remove it from GDB's tables.  */
-	  notify_solib_unloaded (current_program_space, *gdb_iter);
-
-	  current_program_space->deleted_solibs.push_back (gdb_iter->so_name);
+	  notify_solib_unloaded (current_program_space, *gdb_iter,
+				 still_in_use);
 
 	  /* Unless the user loaded it explicitly, free SO's objfile.  */
 	  if (gdb_iter->objfile != nullptr
 	      && !(gdb_iter->objfile->flags & OBJF_USERLOADED)
-	      && !solib_used (current_program_space, *gdb_iter))
+	      && !still_in_use)
 	    gdb_iter->objfile->unlink ();
 
+	  current_program_space->deleted_solibs.push_back (gdb_iter->so_name);
+
 	  /* Some targets' section tables might be referring to
 	     sections from so.abfd; remove them.  */
 	  current_program_space->remove_target_sections (&*gdb_iter);
@@ -1158,7 +1167,10 @@  clear_solib (program_space *pspace)
 
   for (solib &so : pspace->so_list)
     {
-      notify_solib_unloaded (pspace, so);
+      bool still_in_use
+	= (so.objfile != nullptr && solib_used (pspace, so));
+
+      notify_solib_unloaded (pspace, so, still_in_use);
       pspace->remove_target_sections (&so);
     };
 
diff --git a/gdb/testsuite/gdb.base/dlmopen.exp b/gdb/testsuite/gdb.base/dlmopen.exp
index f1da76a13f6..14f9084dd6f 100644
--- a/gdb/testsuite/gdb.base/dlmopen.exp
+++ b/gdb/testsuite/gdb.base/dlmopen.exp
@@ -88,6 +88,18 @@  if { [build_executable "failed to build" $testfile $srcfile \
 set bp_inc [gdb_get_line_number "bp.inc" $srcfile_lib]
 set bp_main [gdb_get_line_number "bp.main" $srcfile]
 
+# Figure out the file name for the dynamic linker.
+set dyln_name [section_get $binfile .interp]
+if { $dyln_name eq "" } {
+    unsupported "couldn't find dynamic linker name"
+    return
+}
+
+# Return true if FILENAME is the dynamic linker.  Otherwise return false.
+proc is_dyln { filename } {
+    return [expr {$filename eq $::dyln_name}]
+}
+
 # Check that 'info shared' show NUM occurrences of DSO.
 proc check_dso_count { dso num } {
     global gdb_prompt hex
@@ -210,6 +222,161 @@  proc test_dlmopen_with_attach {} {
     }
 }
 
+# Run 'info sharedlibrary' and count the number of mappings that look
+# like they might be the dynamic linker.  This will only work for
+# Linux right now.
+proc get_dyld_info {} {
+    if { ![istarget *-linux*] } {
+	return [list 0 ""]
+    }
+
+    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" {
+	    exp_continue
+	}
+	-re "^($::hex)\\s+$::hex\\s+\[^/\]+(/\[^\r\n\]+)\r\n" {
+	    set addr $expect_out(1,string)
+	    set lib $expect_out(2,string)
+
+	    if { [is_dyln $lib] } {
+		# This looks like it might be the dynamic linker.
+		incr dyld_count
+		if { $dyld_start_addr eq "" } {
+		    set dyld_start_addr $addr
+		} elseif { $dyld_start_addr ne $addr } {
+		    set dyld_start_addr "MULTIPLE"
+		}
+	    }
+
+	    exp_continue
+	}
+	-re "\\(\\*\\): Shared library is missing debugging information\\.\r\n" {
+	    exp_continue
+	}
+	-re "^$::gdb_prompt $" {
+	}
+    }
+
+    if { $dyld_start_addr eq "MULTIPLE" } {
+	set dyld_start_addr ""
+    }
+
+    return [list $dyld_count $dyld_start_addr]
+}
+
+# The inferior for this test causes the dynamic linker to be appear
+# multiple times in the inferior's shared library list, but (at least
+# with glibc), the dynamic linker is really only mapped in once.  That
+# is, each of the dynamic linker instances that appear in the 'info
+# sharedlibrary' output, will have the same address range.
+#
+# This test creates a user breakpoint in the dynamic linker, and then
+# runs over the dlcose calls, which unmap all but one of the dynamic
+# linker instances.
+#
+# The expectation is that the user breakpoint in the dynamic linker
+# should still be active.  Older versions of GDB had a bug where the
+# breakpoint would become pending.
+proc_with_prefix test_solib_unmap_events { } {
+
+    # This test relies on finding the dynamic linker library, and is
+    # currently written assuming Linux.
+    if { ![istarget *-linux*] } {
+	unsupport "cannot find dynamic linker library on this target"
+	return
+    }
+
+    clean_restart $::binfile
+
+    if { ![runto_main] } {
+	return
+    }
+
+    # Check that before any of our dlopen/dlmopen calls, we can find a
+    # single copy of the dynamic linker in the shared library list.
+    set dyld_info [get_dyld_info]
+    set dyld_count [lindex $dyld_info 0]
+    if { $dyld_count != 1 } {
+	unsupported "initial dyld state appears strange"
+	return
+    }
+
+    # Continue the inferior until all solib are loaded.
+    set alarm_lineno [gdb_get_line_number "alarm" $::srcfile]
+    gdb_breakpoint ${::srcfile}:${alarm_lineno}
+    gdb_continue_to_breakpoint "all solib are now loaded"
+
+    # Check that we have multiple copies of dynamic linker loaded, and
+    # that the dynamic linker is only loaded at a single address.
+    set dyld_info [get_dyld_info]
+    set dyld_count [lindex $dyld_info 0]
+    set dyld_start_addr [lindex $dyld_info 1]
+
+    # If we didn't find a suitable dynamic linker address, or we
+    # didn't find multiple copies of the dynamic linker, then
+    # something has gone wrong with the test setup.
+    if { $dyld_count < 2 } {
+	unsupported "multiple copies of the dynamic linker not found"
+	return
+    }
+    if { $dyld_start_addr eq "" } {
+	unsupported "unable to find suitable dynamic linker start address"
+	return
+    }
+
+    # Check the address we found is (likely) writable.
+    gdb_test_multiple "x/1i $dyld_start_addr" "check b/p address" {
+	-re -wrap "Cannot access memory at address \[^\r\n\]+" {
+	    unsupported "dynamic linker address is not accessible"
+	    return
+	}
+	-re -wrap "" {
+	}
+    }
+
+    # Create a breakpoint within the dynamic linker.  It is pretty unlikely
+    # that this breakpoint will ever be hit, but just in case it is, make it
+    # conditional, with a condition that will never be true.  All we really
+    # care about for this test is whether the breakpoint will be made
+    # pending or not (it should not).
+    gdb_test "break *$dyld_start_addr if (0)" \
+	"Breakpoint $::decimal at $::hex\[^\r\n\]+" \
+	"create breakpoint within dynamic linker"
+    set bpnum [get_integer_valueof "\$bpnum" INVALID "get bpnum"]
+
+    # Now continue until the 'bp.main' location, this will unload some
+    # copies, but not all copies, of the dynamic linker.
+    gdb_test "print wait_for_gdb = 0" " = 0"
+    set bp_main [gdb_get_line_number "bp.main" $::srcfile]
+
+    gdb_breakpoint $::srcfile:$bp_main
+    gdb_continue_to_breakpoint "stop at bp.main"
+
+    # At one point, GDB would incorrectly mark the breakpoints in the
+    # dynamic linker as pending when some instances of the library were
+    # unloaded, despite there really only being one copy of the dynamic
+    # linker actually loaded into the inferior's address space.
+    gdb_test_multiple "info breakpoints $bpnum" "check b/p status" {
+	-re -wrap "$bpnum\\s+breakpoint\\s+keep\\s+y\\s+<PENDING>\\s+\\*$::hex\\s*\r\n\\s+stop only if \\(0\\)" {
+	    fail $gdb_test_name
+	}
+
+	-re -wrap "$bpnum\\s+breakpoint\\s+keep\\s+y\\s+$::hex\\s*\[^\r\n\]+\r\n\\s+stop only if \\(0\\)" {
+	    pass $gdb_test_name
+	}
+    }
+
+    # With all the dlclose calls now complete, we should be back to a
+    # single copy of the dynamic linker.
+    set dyld_info [get_dyld_info]
+    set dyld_count [lindex $dyld_info 0]
+    gdb_assert { $dyld_count == 1 } \
+	"one dynamic linker found after dlclose calls"
+}
+
 # Run the actual tests.
 test_dlmopen_no_attach
 test_dlmopen_with_attach
+test_solib_unmap_events
diff --git a/gdb/testsuite/gdb.mi/mi-dlmopen-lib-dep.c b/gdb/testsuite/gdb.mi/mi-dlmopen-lib-dep.c
new file mode 100644
index 00000000000..1645f55b865
--- /dev/null
+++ b/gdb/testsuite/gdb.mi/mi-dlmopen-lib-dep.c
@@ -0,0 +1,21 @@ 
+/* This testcase is part of GDB, the GNU debugger.
+
+   Copyright 2021-2024 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/>.
+
+*/
+
+__attribute__((visibility ("default")))
+int gdb_dlmopen_glob = 1;
diff --git a/gdb/testsuite/gdb.mi/mi-dlmopen-lib.c b/gdb/testsuite/gdb.mi/mi-dlmopen-lib.c
new file mode 100644
index 00000000000..4ce9280bad3
--- /dev/null
+++ b/gdb/testsuite/gdb.mi/mi-dlmopen-lib.c
@@ -0,0 +1,28 @@ 
+/* This testcase is part of GDB, the GNU debugger.
+
+   Copyright 2021-2024 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/>.
+
+*/
+
+extern int gdb_dlmopen_glob;
+
+__attribute__((visibility ("default")))
+int
+inc (int n)
+{
+  int amount = gdb_dlmopen_glob;
+  return n + amount;  /* bp.inc.  */
+}
diff --git a/gdb/testsuite/gdb.mi/mi-dlmopen.c b/gdb/testsuite/gdb.mi/mi-dlmopen.c
new file mode 100644
index 00000000000..d3853f8d9e5
--- /dev/null
+++ b/gdb/testsuite/gdb.mi/mi-dlmopen.c
@@ -0,0 +1,59 @@ 
+/* This testcase is part of GDB, the GNU debugger.
+
+   Copyright 2021-2024 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>
+
+int
+main (void)
+{
+  void *handle[4];
+  int (*fun) (int);
+  Lmid_t lmid;
+  int dl;
+
+  handle[0] = dlmopen (LM_ID_NEWLM, DSO1_NAME, RTLD_LAZY | RTLD_LOCAL);
+  assert (handle[0] != NULL);
+
+  dlinfo (handle[0], RTLD_DI_LMID, &lmid);
+
+  handle[1] = dlopen (DSO1_NAME, RTLD_LAZY | RTLD_LOCAL);
+  assert (handle[1] != NULL);
+
+  handle[2] = dlmopen (LM_ID_NEWLM, DSO1_NAME, RTLD_LAZY | RTLD_LOCAL);
+  assert (handle[2] != NULL);
+
+  handle[3] = dlmopen (lmid, DSO2_NAME, RTLD_LAZY | RTLD_LOCAL);
+  assert (handle[3] != NULL);	/* bp.loaded */
+
+  for (dl = 0; dl < 4; ++dl)
+    {
+      fun = dlsym (handle[dl], "inc");
+      assert (fun != NULL);
+
+      fun (42);
+
+      dlclose (handle[dl]);
+    }
+
+  return 0;  /* bp.main  */
+}
diff --git a/gdb/testsuite/gdb.mi/mi-dlmopen.exp b/gdb/testsuite/gdb.mi/mi-dlmopen.exp
new file mode 100644
index 00000000000..77d166273e3
--- /dev/null
+++ b/gdb/testsuite/gdb.mi/mi-dlmopen.exp
@@ -0,0 +1,209 @@ 
+# This testcase is part of GDB, the GNU debugger.
+#
+# Copyright 2024 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/>.
+
+# MI tests related to loading shared libraries into different namespaces
+# with dlmopen().  The source files for this test are copied (almost)
+# verbatim from the gdb.base/dlmopen.exp test.
+
+load_lib "mi-support.exp"
+
+require allow_dlmopen_tests
+
+standard_testfile .c -lib.c -lib-dep.c
+
+set basename_lib dlmopen-lib
+set srcfile_lib $srcfile2
+set binfile_lib1 [standard_output_file $basename_lib.1.so]
+set binfile_lib2 [standard_output_file $basename_lib.2.so]
+set srcfile_lib_dep $srcfile3
+set binfile_lib_dep [standard_output_file $basename_lib-dep.so]
+
+if { [build_executable "build shlib dep" $binfile_lib_dep $srcfile_lib_dep \
+	  {debug shlib}] == -1 } {
+    return
+}
+
+if { [build_executable "build shlib" $binfile_lib1 $srcfile_lib \
+	  [list debug shlib_load shlib libs=$binfile_lib_dep]] == -1 } {
+    return
+}
+
+if { [build_executable "build shlib" $binfile_lib2 $srcfile_lib \
+	  [list debug shlib_load shlib libs=$binfile_lib_dep]] == -1 } {
+    return
+}
+
+if { [build_executable "failed to build" $testfile $srcfile \
+	  [list additional_flags=-DDSO1_NAME=\"$binfile_lib1\" \
+	       additional_flags=-DDSO2_NAME=\"$binfile_lib2\" \
+	       shlib_load debug]] } {
+    return
+}
+
+# Figure out the file name for the dynamic linker.
+set dyln_name [section_get $binfile .interp]
+if { $dyln_name eq "" } {
+    unsupported "couldn't find dynamic linker name"
+    return
+}
+
+# Some source locations needed by the tests.
+set bp_main [gdb_get_line_number "bp.main" $srcfile]
+set bp_loaded [gdb_get_line_number "bp.loaded" $srcfile]
+
+# Return true if FILENAME is the dynamic linker.  Otherwise return false.
+proc is_dyln { filename } {
+    return [expr {$filename eq $::dyln_name}]
+}
+
+# Run 'info sharedlibrary' and count the number of mappings that look
+# like they might be the dynamic linker.  This will only work for
+# Linux right now.
+proc get_dyld_info {} {
+    if { ![istarget *-linux*] } {
+	return [list 0 ""]
+    }
+
+    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" {
+	    exp_continue
+	}
+	-re "^~\"($::hex)\\s+$::hex\\s+\[^/\]+(/\[^\r\n\]+)\\\\n\"\r\n" {
+	    set addr $expect_out(1,string)
+	    set lib $expect_out(2,string)
+
+	    if { [is_dyln $lib] } {
+		# This looks like it might be the dynamic linker.
+		incr dyld_count
+		if { $dyld_start_addr eq "" } {
+		    set dyld_start_addr $addr
+		} elseif { $dyld_start_addr ne $addr } {
+		    set dyld_start_addr "MULTIPLE"
+		}
+	    }
+
+	    exp_continue
+	}
+	-re "~\"\\(\\*\\): Shared library is missing debugging information\\.\\\\n\"\r\n" {
+	    exp_continue
+	}
+	-re "^\\^done\r\n" {
+	    exp_continue
+	}
+	-re "^$::mi_gdb_prompt$" {
+	}
+    }
+
+    if { $dyld_start_addr eq "MULTIPLE" } {
+	set dyld_start_addr ""
+    }
+
+    return [list $dyld_count $dyld_start_addr]
+}
+
+# Run the inferior over all the 'dlclose' calls and capture the
+# resulting library-unloaded events.  Check that we see the expected
+# number of unload events for the libraries created for this test, and
+# additionally, check for dynamic linker unload events.
+proc check_solib_unload_events {} {
+    mi_clean_restart $::binfile
+
+    if {[mi_runto_main] == -1} {
+	return
+    }
+
+    # After starting we expect the dynamic linker to be loaded exactly
+    # once.  If it is not then we'll not be able to check the dynamic
+    # linker unloaded events later in this script.
+    set dyld_info [get_dyld_info]
+    set dyld_count [lindex $dyld_info 0]
+    if { $dyld_count != 1 } {
+	unsupported "dynamic linker doesn't appear to be loaded"
+	return
+    }
+
+    # Create breakpoints.
+    mi_create_breakpoint "$::srcfile:$::bp_loaded" \
+	"create b/p once libraries are loaded" \
+	-disp keep -func main -file ".*$::srcfile" -line $::bp_loaded
+    mi_create_breakpoint "$::srcfile:$::bp_main" "create b/p at dlclose" \
+	-disp keep -func main -file ".*$::srcfile" -line $::bp_main
+
+    # Run past all the dlopen and dlmopen calls.
+    mi_execute_to "exec-continue" "breakpoint-hit" main "" ".*" $::bp_loaded \
+	{"" "disp=\"keep\""} "continue until all libraries are loaded"
+
+    # Check that the dynamic linker has now been loaded multiple times.
+    set dyld_info [get_dyld_info]
+    set dyld_count [lindex $dyld_info 0]
+    if { $dyld_count < 2 } {
+	unsupported "not enough instances of the dynamic linker are mapped in"
+	return
+    }
+
+    # Continue.  This will run until the end of 'main', and will pass
+    # over all the dlclose calls.
+    if {[mi_send_resuming_command "exec-continue" "exec-next"] == -1} {
+	return
+    }
+
+    # As a result of all the dlclose calls we should see some library
+    # unload events.  Process them now.
+    set dyld_unload_count 0
+    array set unload_counts {}
+    gdb_test_multiple "" "" -prompt $::mi_gdb_prompt {
+	-re "=library-unloaded,id=\"(\[^\"\]+)\",\[^\r\n\]+\r\n" {
+	    set lib $expect_out(1,string)
+	    if {[is_dyln $lib]} {
+		# This is the dynamic linker being unloaded.
+		incr dyld_unload_count
+	    }
+	    set filename [file tail $lib]
+	    incr unload_counts($filename)
+	    exp_continue
+	}
+	-re "\\*stopped,reason=\"breakpoint-hit\",\[^\r\n\]+\r\n$::mi_gdb_prompt" {
+	}
+    }
+
+    # Check we saw the dynamic linker being unloaded the expected number of
+    # times.
+    gdb_assert { $dyld_unload_count == $dyld_count - 1 } \
+	"expected number of dynamic linker unloads"
+
+    # Check that we saw the expected number of library-unloaded events for
+    # each library.  Each DESC is a list of two elements, a filename for a
+    # library, and the number of times it should have been unloaded.
+    foreach desc [list [list $::binfile_lib1 3] \
+		       [list $::binfile_lib_dep 3] \
+		       [list $::binfile_lib2 1]] {
+	set filename [file tail [lindex $desc 0]]
+	set count [lindex $desc 1]
+	gdb_assert { $unload_counts($filename) == $count } \
+	    "check unload count for $filename"
+    }
+
+    # Check that the dynamic linker still shows as loaded exactly once.
+    set dyld_info [get_dyld_info]
+    set dyld_count [lindex $dyld_info 0]
+    gdb_assert { $dyld_count == 1 } \
+	"dynamic linker is mapped once at final b/p"
+}
+
+check_solib_unload_events
diff --git a/gdb/testsuite/lib/gdb.exp b/gdb/testsuite/lib/gdb.exp
index 7ee2043f0f8..dca3d5f2b63 100644
--- a/gdb/testsuite/lib/gdb.exp
+++ b/gdb/testsuite/lib/gdb.exp
@@ -10963,5 +10963,38 @@  gdb_caching_proc root_user {} {
     return [expr $uid == 0]
 }
 
+# Return nul-terminated string read from section SECTION of EXEC.  Return ""
+# if no such section or nul-terminated string was found.  Function is useful
+# for sections ".interp" or ".gnu_debuglink".
+
+proc section_get {exec section} {
+    global subdir
+    set tmp [standard_output_file section_get.tmp]
+    set objcopy_program [gdb_find_objcopy]
+
+    set command "exec $objcopy_program -O binary --set-section-flags $section=A --change-section-address $section=0 -j $section $exec $tmp"
+    verbose -log "command is $command"
+    set result [catch $command output]
+    verbose -log "result is $result"
+    verbose -log "output is $output"
+    if {$result == 1} {
+	return ""
+    }
+    set fi [open $tmp]
+    fconfigure $fi -translation binary
+    set data [read $fi]
+    close $fi
+    file delete $tmp
+    # .interp has size $len + 1 but .gnu_debuglink contains garbage after \000.
+    set len [string first \000 $data]
+    if {$len < 0} {
+	verbose -log "section $section not found"
+	return ""
+    }
+    set retval [string range $data 0 [expr $len - 1]]
+    verbose -log "section $section is <$retval>"
+    return $retval
+}
+
 # Always load compatibility stuff.
 load_lib future.exp
diff --git a/gdb/testsuite/lib/prelink-support.exp b/gdb/testsuite/lib/prelink-support.exp
index 894af399bce..3aaea0a36ed 100644
--- a/gdb/testsuite/lib/prelink-support.exp
+++ b/gdb/testsuite/lib/prelink-support.exp
@@ -13,39 +13,6 @@ 
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-# Return nul-terminated string read from section SECTION of EXEC.  Return ""
-# if no such section or nul-terminated string was found.  Function is useful
-# for sections ".interp" or ".gnu_debuglink".
-
-proc section_get {exec section} {
-    global subdir
-    set tmp [standard_output_file section_get.tmp]
-    set objcopy_program [gdb_find_objcopy]
-
-    set command "exec $objcopy_program -O binary --set-section-flags $section=A --change-section-address $section=0 -j $section $exec $tmp"
-    verbose -log "command is $command"
-    set result [catch $command output]
-    verbose -log "result is $result"
-    verbose -log "output is $output"
-    if {$result == 1} {
-	return ""
-    }
-    set fi [open $tmp]
-    fconfigure $fi -translation binary
-    set data [read $fi]
-    close $fi
-    file delete $tmp
-    # .interp has size $len + 1 but .gnu_debuglink contains garbage after \000.
-    set len [string first \000 $data]
-    if {$len < 0} {
-	verbose -log "section $section not found"
-	return ""
-    }
-    set retval [string range $data 0 [expr $len - 1]]
-    verbose -log "section $section is <$retval>"
-    return $retval
-}
-
 # Resolve symlinks.
 
 proc symlink_resolve {file} {