From FEDORA_PATCHES Mon Sep 17 00:00:00 2001
From: Andrew Burgess <aburgess@redhat.com>
Date: Wed, 31 Jul 2024 15:58:20 +0100
Subject: gdb-backport-buildid-related-changes.patch

;; Backport patches related to build-id usage when opening a core
;; file, and also improving general build-id based lookup.  These will
;; all drop out when rebasing to GDB 16.

backport the following patches related to build-id usage

This one patch backports the following upstream commits:

  * 195795b0fd4 gdb: fix a target: prefix issue in find_separate_debug_file
  * ea4c968ce54 gdb: avoid '//' in filenames when searching for debuginfo
  * 22836ca8859 gdb: check for multiple matching build-id files
  * 6d45af96ea5 gdbserver: add gdbserver support for vFile::stat packet
  * 3055e3d2f13 gdb: add GDB side target_ops::fileio_stat implementation
  * 08a115cc1c4 gdb: add target_fileio_stat, but no implementations yet
  * c7e38ee47c9 gdb: avoid duplicate search in build_id_to_bfd_suffix
  * 10ac7e80c01 gdb: remove find_and_open_solib so_list method
  * 3e35b4deae2 gdb/testsuite: tests for debug lookup within the sysroot
  * 6dfd07222c0 gdb: convert separate-debug-file to new(ish) debug scheme
  * 5cabc8098e6 gdb/python: implement Python find_exec_by_build_id hook
  * ef1a41f20b7 gdb: add extension hook ext_lang_find_objfile_from_buildid
  * 629bcc68d79 gdb: rename ext_lang_missing_debuginfo_result
  * 73d7312ff61 gdb: use mapped file information to improve debuginfod text
  * 5ddd0d7eefb gdb/python: avoid depending on the curses library
  * 8358d39b4f0 gdb: unify build-id to objfile lookup code
  * fa826a4bbe9 gdb: improve shared library build-id check for core-files
  * a47a679c769 gdb/corefile: improve file backed mapping handling
  * 522875f679b gdb/corefile: don't pretend unavailable sections are readable
  * 97832471899 gdb/build-id: protect against weirdly short build-ids
  * 9543c37620c gdb/testsuite: make gdb_gnu_strip_debug consistent
  * fc240bb143c gdb/fileio: fix errno for packets where an attachment is expected
  * df0445b370f gdb/testsuite: fix typo 'unsupport' to 'unsupported'

I've merged these into a single (large) patch as these all merged
without any changes (i.e. these represent exactly the upstream code),
and all of these will be included in GDB 16, and so should all drop
out together when we rebase to GDB 16 (the next rebase).

All of these patches relate to either build-id usage when opening a
core file, or improving GDB's build-id based debug lookup.

diff --git a/gdb/NEWS b/gdb/NEWS
--- a/gdb/NEWS
+++ b/gdb/NEWS
@@ -1,6 +1,36 @@
 		What has changed in GDB?
 	     (Organized release by release)
 
+*** Change since GDB 15
+
+* Python API
+
+  ** New module gdb.missing_objfile that facilitates dealing with
+     missing objfiles when opening a core-file.
+
+  ** New function gdb.missing_objfile.register_handler that can
+     register an instance of a sub-class of
+     gdb.missing_debug.MissingObjfileHandler as a handler for missing
+     objfiles.
+
+  ** New class gdb.missing_objfile.MissingObjfileHandler which can be
+     sub-classed to create handlers for missing objfiles.
+
+* New commands
+
+info missing-objfile-handlers
+  List all the registered missing-objfile handlers.
+
+enable missing-objfile-handler LOCUS HANDLER
+disable missing-objfile-handler LOCUS HANDLER
+  Enable or disable a missing-objfile handler with a name matching the
+  regular expression HANDLER, in LOCUS.
+
+  LOCUS can be 'global' to operate on global missing-objfile handler,
+  'progspace' to operate on handlers within the current program space,
+  or can be a regular expression which is matched against the filename
+  of the primary executable in each program space.
+
 *** Changes in GDB 15
 
 * The MPX commands "show/set mpx bound" have been deprecated, as Intel
@@ -212,6 +242,11 @@ qIsAddressTagged
   file is about, this new packet provides a more generic way to perform such
   a check.
 
+vFile:stat
+  Return information about files on the remote system.  Like
+  vFile:fstat but takes a filename rather than an open file
+  descriptor.
+
 *** Changes in GDB 14
 
 * GDB now supports the AArch64 Scalable Matrix Extension 2 (SME2), which
diff --git a/gdb/build-id.c b/gdb/build-id.c
--- a/gdb/build-id.c
+++ b/gdb/build-id.c
@@ -26,6 +26,9 @@
 #include "filenames.h"
 #include "gdbcore.h"
 #include "cli/cli-style.h"
+#include "gdbsupport/scoped_fd.h"
+#include "debuginfod-support.h"
+#include "extension.h"
 
 /* See build-id.h.  */
 
@@ -73,73 +76,177 @@ build_id_verify (bfd *abfd, size_t check_len, const bfd_byte *check)
   return retval;
 }
 
-/* Helper for build_id_to_debug_bfd.  LINK is a path to a potential
-   build-id-based separate debug file, potentially a symlink to the real file.
-   If the file exists and matches BUILD_ID, return a BFD reference to it.  */
+/* Helper for build_id_to_debug_bfd.  ORIGINAL_LINK with SUFFIX appended is
+   a path to a potential build-id-based separate debug file, potentially a
+   symlink to the real file.  If the file exists and matches BUILD_ID,
+   return a BFD reference to it.  */
 
 static gdb_bfd_ref_ptr
-build_id_to_debug_bfd_1 (const std::string &link, size_t build_id_len,
-			 const bfd_byte *build_id)
+build_id_to_debug_bfd_1 (const std::string &original_link,
+			 size_t build_id_len, const bfd_byte *build_id,
+			 const char *suffix)
 {
-  if (separate_debug_file_debug)
+  tribool supports_target_stat = TRIBOOL_UNKNOWN;
+
+  /* Drop the 'target:' prefix if the target filesystem is local.  */
+  std::string_view original_link_view (original_link);
+  if (is_target_filename (original_link) && target_filesystem_is_local ())
+    original_link_view
+      = original_link_view.substr (strlen (TARGET_SYSROOT_PREFIX));
+
+  /* The upper bound of '10' here is completely arbitrary.  The loop should
+     terminate via 'break' when either (a) a readable symlink is found, or
+     (b) a non-existing entry is found.
+
+     However, for remote targets, we rely on the remote returning sane
+     error codes.  If a remote sends back the wrong error code then it
+     might trick GDB into thinking that the symlink exists, but points to a
+     missing file, in which case GDB will try the next seqno.  We don't
+     want a broken remote to cause GDB to spin here forever, hence a fixed
+     upper bound.  */
+
+  for (unsigned seqno = 0; seqno < 10; seqno++)
     {
-      gdb_printf (gdb_stdlog, _("  Trying %s..."), link.c_str ());
-      gdb_flush (gdb_stdlog);
-    }
+      std::string link (original_link_view);
 
-  /* lrealpath() is expensive even for the usually non-existent files.  */
-  gdb::unique_xmalloc_ptr<char> filename_holder;
-  const char *filename = nullptr;
-  if (is_target_filename (link))
-    filename = link.c_str ();
-  else if (access (link.c_str (), F_OK) == 0)
-    {
-      filename_holder.reset (lrealpath (link.c_str ()));
-      filename = filename_holder.get ();
-    }
+      if (seqno > 0)
+	string_appendf (link, ".%u", seqno);
 
-  if (filename == NULL)
-    {
-      if (separate_debug_file_debug)
-	gdb_printf (gdb_stdlog,
-		    _(" no, unable to compute real path\n"));
+      link += suffix;
 
-      return {};
-    }
+      separate_debug_file_debug_printf ("Trying %s...", link.c_str ());
 
-  /* We expect to be silent on the non-existing files.  */
-  gdb_bfd_ref_ptr debug_bfd = gdb_bfd_open (filename, gnutarget);
+      gdb::unique_xmalloc_ptr<char> filename_holder;
+      const char *filename = nullptr;
+      if (is_target_filename (link))
+	{
+	  gdb_assert (link.length () >= strlen (TARGET_SYSROOT_PREFIX));
+	  const char *link_on_target
+	    = link.c_str () + strlen (TARGET_SYSROOT_PREFIX);
+
+	  fileio_error target_errno;
+	  if (supports_target_stat != TRIBOOL_FALSE)
+	    {
+	      struct stat sb;
+	      int res = target_fileio_stat (nullptr, link_on_target, &sb,
+					    &target_errno);
+
+	      if (res != 0 && target_errno != FILEIO_ENOSYS)
+		{
+		  separate_debug_file_debug_printf ("path doesn't exist");
+		  break;
+		}
+	      else if (res != 0 && target_errno == FILEIO_ENOSYS)
+		supports_target_stat = TRIBOOL_FALSE;
+	      else
+		{
+		  supports_target_stat = TRIBOOL_TRUE;
+		  filename = link.c_str ();
+		}
+	    }
+
+	  if (supports_target_stat == TRIBOOL_FALSE)
+	    {
+	      gdb_assert (filename == nullptr);
+
+	      /* Connecting to a target that doesn't support 'stat'.  Try
+		 'readlink' as an alternative.  This isn't ideal, but is
+		 maybe better than nothing.  Returns EINVAL if the path
+		 isn't a symbolic link, which hints that the path is
+		 available -- there are other errors e.g. ENOENT for when
+		 the path doesn't exist, but we just assume that anything
+		 other than EINVAL indicates the path doesn't exist.  */
+	      std::optional<std::string> link_target
+		= target_fileio_readlink (nullptr, link_on_target,
+					  &target_errno);
+	      if (link_target.has_value ()
+		  || target_errno == FILEIO_EINVAL)
+		filename = link.c_str ();
+	      else
+		{
+		  separate_debug_file_debug_printf ("path doesn't exist");
+		  break;
+		}
+	    }
+	}
+      else
+	{
+	  struct stat buf;
+
+	  /* The `access' call below automatically dereferences LINK, but
+	     we want to stop incrementing SEQNO once we find a symlink
+	     that doesn't exist.  */
+	  if (lstat (link.c_str (), &buf) != 0)
+	    {
+	      separate_debug_file_debug_printf ("path doesn't exist");
+	      break;
+	    }
+
+	  /* Can LINK be accessed, or if LINK is a symlink, can the file
+	     pointed too be accessed?  Do this as lrealpath() is
+	     expensive, even for the usually non-existent files.  */
+	  if (access (link.c_str (), F_OK) == 0)
+	    {
+	      filename_holder.reset (lrealpath (link.c_str ()));
+	      filename = filename_holder.get ();
+	    }
+	}
 
-  if (debug_bfd == NULL)
-    {
-      if (separate_debug_file_debug)
-	gdb_printf (gdb_stdlog, _(" no, unable to open.\n"));
+      if (filename == nullptr)
+	{
+	  separate_debug_file_debug_printf ("unable to compute real path");
+	  continue;
+	}
 
-      return {};
-    }
+      /* We expect to be silent on the non-existing files.  */
+      gdb_bfd_ref_ptr debug_bfd = gdb_bfd_open (filename, gnutarget);
 
-  if (!build_id_verify (debug_bfd.get(), build_id_len, build_id))
-    {
-      if (separate_debug_file_debug)
-	gdb_printf (gdb_stdlog, _(" no, build-id does not match.\n"));
+      if (debug_bfd == NULL)
+	{
+	  separate_debug_file_debug_printf ("unable to open `%s`", filename);
+	  continue;
+	}
 
-      return {};
-    }
+      if (!build_id_verify (debug_bfd.get(), build_id_len, build_id))
+	{
+	  separate_debug_file_debug_printf ("build-id does not match");
+	  continue;
+	}
 
-  if (separate_debug_file_debug)
-    gdb_printf (gdb_stdlog, _(" yes!\n"));
+      separate_debug_file_debug_printf ("found a match");
+      return debug_bfd;
+    }
 
-  return debug_bfd;
+  separate_debug_file_debug_printf ("no suitable file found");
+  return {};
 }
 
 /* Common code for finding BFDs of a given build-id.  This function
    works with both debuginfo files (SUFFIX == ".debug") and executable
-   files (SUFFIX == "").  */
+   files (SUFFIX == "").
+
+   The build-id will be split into a single byte sub-directory, followed by
+   the remaining build-id bytes as the filename, i.e. we use the lookup
+   format: `.build-id/xx/yy....zz`.  As a consequence, if BUILD_ID_LEN is
+   less than 2 (bytes), no results will be found as there are not enough
+   bytes to form the `yy....zz` part of the lookup filename.  */
 
 static gdb_bfd_ref_ptr
 build_id_to_bfd_suffix (size_t build_id_len, const bfd_byte *build_id,
 			const char *suffix)
 {
+  SEPARATE_DEBUG_FILE_SCOPED_DEBUG_ENTER_EXIT;
+
+  if (build_id_len < 2)
+    {
+      /* Zero length build-ids are ignored by bfd.  */
+      gdb_assert (build_id_len > 0);
+      separate_debug_file_debug_printf
+	("Ignoring short build-id `%s' for build-id based lookup",
+	 bin2hex (build_id, build_id_len).c_str ());
+      return {};
+    }
+
   /* Keep backward compatibility so that DEBUG_FILE_DIRECTORY being "" will
      cause "/.build-id/..." lookups.  */
 
@@ -158,30 +265,33 @@ build_id_to_bfd_suffix (size_t build_id_len, const bfd_byte *build_id,
       std::string link = debugdir.get ();
       link += "/.build-id/";
 
-      if (size > 0)
-	{
-	  size--;
-	  string_appendf (link, "%02x/", (unsigned) *data++);
-	}
+      gdb_assert (size > 1);
+      size--;
+      string_appendf (link, "%02x/", (unsigned) *data++);
 
       while (size-- > 0)
 	string_appendf (link, "%02x", (unsigned) *data++);
 
-      link += suffix;
-
       gdb_bfd_ref_ptr debug_bfd
-	= build_id_to_debug_bfd_1 (link, build_id_len, build_id);
+	= build_id_to_debug_bfd_1 (link, build_id_len, build_id, suffix);
       if (debug_bfd != NULL)
 	return debug_bfd;
 
       /* Try to look under the sysroot as well.  If the sysroot is
 	 "/the/sysroot", it will give
-	 "/the/sysroot/usr/lib/debug/.build-id/ab/cdef.debug".  */
+	 "/the/sysroot/usr/lib/debug/.build-id/ab/cdef.debug".
+
+	 If the sysroot is 'target:' and the target filesystem is local to
+	 GDB then 'target:/path/to/check' becomes '/path/to/check' which
+	 we just checked above.  */
 
-      if (!gdb_sysroot.empty ())
+      if (!gdb_sysroot.empty ()
+	  && (gdb_sysroot != TARGET_SYSROOT_PREFIX
+	      || !target_filesystem_is_local ()))
 	{
 	  link = gdb_sysroot + link;
-	  debug_bfd = build_id_to_debug_bfd_1 (link, build_id_len, build_id);
+	  debug_bfd = build_id_to_debug_bfd_1 (link, build_id_len, build_id,
+					       suffix);
 	  if (debug_bfd != NULL)
 	    return debug_bfd;
 	}
@@ -198,9 +308,11 @@ build_id_to_debug_bfd (size_t build_id_len, const bfd_byte *build_id)
   return build_id_to_bfd_suffix (build_id_len, build_id, ".debug");
 }
 
-/* See build-id.h.  */
+/* Find and open a BFD for an executable file given a build-id.  If no BFD
+   can be found, return NULL.  The returned reference to the BFD must be
+   released by the caller.  */
 
-gdb_bfd_ref_ptr
+static gdb_bfd_ref_ptr
 build_id_to_exec_bfd (size_t build_id_len, const bfd_byte *build_id)
 {
   return build_id_to_bfd_suffix (build_id_len, build_id, "");
@@ -217,10 +329,9 @@ find_separate_debug_file_by_buildid (struct objfile *objfile,
   build_id = build_id_bfd_get (objfile->obfd.get ());
   if (build_id != NULL)
     {
-      if (separate_debug_file_debug)
-	gdb_printf (gdb_stdlog,
-		    _("\nLooking for separate debug info (build-id) for "
-		      "%s\n"), objfile_name (objfile));
+      SEPARATE_DEBUG_FILE_SCOPED_DEBUG_START_END
+	("looking for separate debug info (build-id) for %s",
+	 objfile_name (objfile));
 
       gdb_bfd_ref_ptr abfd (build_id_to_debug_bfd (build_id->size,
 						   build_id->data));
@@ -229,9 +340,9 @@ find_separate_debug_file_by_buildid (struct objfile *objfile,
 	  && filename_cmp (bfd_get_filename (abfd.get ()),
 			   objfile_name (objfile)) == 0)
 	{
-	  if (separate_debug_file_debug)
-	    gdb_printf (gdb_stdlog, "\"%s\": separate debug info file has no "
-			"debug info", bfd_get_filename (abfd.get ()));
+	  separate_debug_file_debug_printf
+	    ("\"%s\": separate debug info file has no debug info",
+	     bfd_get_filename (abfd.get ()));
 	  warnings->warn (_("\"%ps\": separate debug info file has no "
 			    "debug info"),
 			  styled_string (file_name_style.style (),
@@ -243,3 +354,78 @@ find_separate_debug_file_by_buildid (struct objfile *objfile,
 
   return std::string ();
 }
+
+/* See build-id.h.  */
+
+gdb_bfd_ref_ptr
+find_objfile_by_build_id (program_space *pspace,
+			  const bfd_build_id *build_id,
+			  const char *expected_filename)
+{
+  gdb_bfd_ref_ptr abfd;
+
+  for (unsigned attempt = 0, max_attempts = 1;
+       attempt < max_attempts && abfd == nullptr;
+       ++attempt)
+    {
+      /* Try to find the executable (or shared object) by looking for a
+	 (sym)link on disk from the build-id to the object file.  */
+      abfd = build_id_to_exec_bfd (build_id->size, build_id->data);
+
+      if (abfd != nullptr || attempt > 0)
+	break;
+
+      /* Attempt to query debuginfod for the executable.  This will only
+	 get run during the first attempt, if an extension language hook
+	 (see below) asked for a second attempt then we will have already
+	 broken out of the loop above.  */
+      gdb::unique_xmalloc_ptr<char> path;
+      scoped_fd fd = debuginfod_exec_query (build_id->data, build_id->size,
+					    expected_filename, &path);
+      if (fd.get () >= 0)
+	{
+	  abfd = gdb_bfd_open (path.get (), gnutarget);
+
+	  if (abfd == nullptr)
+	    warning (_("\"%ps\" from debuginfod cannot be opened as bfd: %s"),
+		     styled_string (file_name_style.style (), path.get ()),
+		     gdb_bfd_errmsg (bfd_get_error (), nullptr).c_str ());
+	  else if (!build_id_verify (abfd.get (), build_id->size,
+				     build_id->data))
+	    abfd = nullptr;
+	}
+
+      if (abfd != nullptr)
+	break;
+
+      ext_lang_missing_file_result ext_result
+	= ext_lang_find_objfile_from_buildid (pspace, build_id,
+					      expected_filename);
+      if (!ext_result.filename ().empty ())
+	{
+	  /* The extension identified the file for us.  */
+	  abfd = gdb_bfd_open (ext_result.filename ().c_str (), gnutarget);
+	  if (abfd == nullptr)
+	    {
+	      warning (_("\"%ps\" from extension cannot be opened as bfd: %s"),
+		       styled_string (file_name_style.style (),
+				      ext_result.filename ().c_str ()),
+		       gdb_bfd_errmsg (bfd_get_error (), nullptr).c_str ());
+	      break;
+	    }
+
+	  /* If the extension gave us a path to a file then we always
+	     assume that it is the correct file, we do no additional check
+	     of its build-id.  */
+	}
+      else if (ext_result.try_again ())
+	{
+	  /* The extension might have installed the file in the expected
+	     location, we should try again.  */
+	  max_attempts = 2;
+	  continue;
+	}
+    }
+
+  return abfd;
+}
diff --git a/gdb/build-id.h b/gdb/build-id.h
--- a/gdb/build-id.h
+++ b/gdb/build-id.h
@@ -40,13 +40,6 @@ extern int build_id_verify (bfd *abfd,
 extern gdb_bfd_ref_ptr build_id_to_debug_bfd (size_t build_id_len,
 					      const bfd_byte *build_id);
 
-/* Find and open a BFD for an executable file given a build-id.  If no BFD
-   can be found, return NULL.  The returned reference to the BFD must be
-   released by the caller.  */
-
-extern gdb_bfd_ref_ptr build_id_to_exec_bfd (size_t build_id_len,
-					     const bfd_byte *build_id);
-
 /* Find the separate debug file for OBJFILE, by using the build-id
    associated with OBJFILE's BFD.  If successful, returns the file name for the
    separate debug file, otherwise, return an empty string.
@@ -60,6 +53,22 @@ extern gdb_bfd_ref_ptr build_id_to_exec_bfd (size_t build_id_len,
 extern std::string find_separate_debug_file_by_buildid
   (struct objfile *objfile, deferred_warnings *warnings);
 
+/* Find an objfile (executable or shared library) that matches BUILD_ID.
+   This is done by first checking in the debug-file-directory for a
+   suitable .build-id/ sub-directory, and looking for a file with the
+   required build-id (usually a symbolic link or hard link to the actual
+   file).
+
+   If that doesn't find us a file then we call to debuginfod to see if it
+   can provide the required file.
+
+   EXPECTED_FILENAME is used in output messages from debuginfod, this
+   should be the file we were looking for but couldn't find.  */
+
+extern gdb_bfd_ref_ptr find_objfile_by_build_id
+  (struct program_space *pspace, const bfd_build_id *build_id,
+   const char *expected_filename);
+
 /* Return an hex-string representation of BUILD_ID.  */
 
 static inline std::string
diff --git a/gdb/corelow.c b/gdb/corelow.c
--- a/gdb/corelow.c
+++ b/gdb/corelow.c
@@ -47,17 +47,130 @@
 #include "gdbsupport/pathstuff.h"
 #include "gdbsupport/scoped_fd.h"
 #include "gdbsupport/x86-xstate.h"
-#include "debuginfod-support.h"
 #include <unordered_map>
 #include <unordered_set>
 #include "cli/cli-cmds.h"
 #include "xml-tdesc.h"
 #include "memtag.h"
+#include "cli/cli-style.h"
 
 #ifndef O_LARGEFILE
 #define O_LARGEFILE 0
 #endif
 
+/* A mem_range and the build-id associated with the file mapped into the
+   given range.  */
+
+struct mem_range_and_build_id
+{
+  mem_range_and_build_id (mem_range &&r, const bfd_build_id *id)
+    : range (r),
+      build_id (id)
+  { /* Nothing.  */ }
+
+  /* A range of memory addresses.  */
+  mem_range range;
+
+  /* The build-id of the file mapped into RANGE.  */
+  const bfd_build_id *build_id;
+};
+
+/* An instance of this class is created within the core_target and is used
+   to hold all the information that relating to mapped files, their address
+   ranges, and their corresponding build-ids.  */
+
+struct mapped_file_info
+{
+  /* See comment on function definition.  */
+
+  void add (const char *soname, const char *expected_filename,
+	    const char *actual_filename, std::vector<mem_range> &&ranges,
+	    const bfd_build_id *build_id);
+
+  /* See comment on function definition.  */
+
+  std::optional <core_target_mapped_file_info>
+  lookup (const char *filename, const std::optional<CORE_ADDR> &addr);
+
+private:
+
+  /* Helper for ::lookup.  BUILD_ID is a build-id that was found in
+     one of the data structures within this class.  Lookup the
+     corresponding filename in m_build_id_to_filename_map and return a pair
+     containing the build-id and filename.
+
+     If no corresponding filename is found in m_build_id_to_filename_map
+     then the returned pair contains BUILD_ID and an empty string.
+
+     If BUILD_ID is nullptr then the returned pair contains nullptr and an
+     empty string.  */
+
+  struct core_target_mapped_file_info
+  make_result (const bfd_build_id *build_id)
+  {
+    if (build_id != nullptr)
+      {
+      auto it = m_build_id_to_filename_map.find (build_id);
+      if (it != m_build_id_to_filename_map.end ())
+	return { build_id, it->second };
+    }
+
+    return { build_id, {} };
+  }
+
+  /* A type that maps a string to a build-id.  */
+  using string_to_build_id_map
+    = std::unordered_map<std::string, const bfd_build_id *>;
+
+  /* A type that maps a build-id to a string.  */
+  using build_id_to_string_map
+    = std::unordered_map<const bfd_build_id *, std::string>;
+
+  /* When loading a core file, the build-ids are extracted based on the
+     file backed mappings.  This map associates the name of a file that was
+     mapped into the core file with the corresponding build-id.  The
+     build-id pointers in this map will never be nullptr as we only record
+     files if they have a build-id.  */
+
+  string_to_build_id_map m_filename_to_build_id_map;
+
+  /* Map a build-id pointer back to the name of the file that was mapped
+     into the inferior's address space.  If we lookup a matching build-id
+     using either a soname or an address then this map allows us to also
+     provide a full path to a file with a matching build-id.  */
+
+  build_id_to_string_map m_build_id_to_filename_map;
+
+  /* If the file that was mapped into the core file was a shared library
+     then it might have a DT_SONAME tag in its .dynamic section, this tag
+     contains the name of a shared object.  When opening a shared library,
+     if it's basename appears in this map then we can use the corresponding
+     build-id.
+
+     In the rare case that two different files have the same DT_SONAME
+     value then the build-id pointer in this map will be nullptr, this
+     indicates that it's not possible to find a build-id based on the given
+     DT_SONAME value.  */
+
+  string_to_build_id_map m_soname_to_build_id_map;
+
+  /* This vector maps memory ranges onto an associated build-id.  The
+     ranges are those of the files mapped into the core file.
+
+     Entries in this vector must not overlap, and are sorted be increasing
+     memory address.  Within each entry the build-id pointer will not be
+     nullptr.
+
+     While building this vector the entries are not sorted, they are
+     sorted once after the table has finished being built.  */
+
+  std::vector<mem_range_and_build_id> m_address_to_build_id_list;
+
+  /* False if address_to_build_id_list is unsorted, otherwise true.  */
+
+  bool m_address_to_build_id_list_sorted = false;
+};
+
 /* The core file target.  */
 
 static const target_info core_target_info = {
@@ -134,6 +247,34 @@ class core_target final : public process_stratum_target
   /* See definition.  */
   void info_proc_mappings (struct gdbarch *gdbarch);
 
+  std::optional <core_target_mapped_file_info>
+  lookup_mapped_file_info (const char *filename,
+			   const std::optional<CORE_ADDR> &addr)
+  {
+    return m_mapped_file_info.lookup (filename, addr);
+  }
+
+  /* Return a string containing the expected executable filename obtained
+     from the mapped file information within the core file.  The filename
+     returned will be for the mapped file whose ELF headers are mapped at
+     the lowest address (i.e. which GDB encounters first).
+
+     If no suitable filename can be found then the returned string will be
+     empty.
+
+     If there are no build-ids embedded into the core file then the
+     returned string will be empty.
+
+     If a non-empty string is returned then there is no guarantee that the
+     named file exists on disk, or if it does exist on disk, then the
+     on-disk file might have a different build-id to the desired
+     build-id.  */
+  const std::string &
+  expected_exec_filename () const
+  {
+    return m_expected_exec_filename;
+  }
+
 private: /* per-core data */
 
   /* Get rid of the core inferior.  */
@@ -156,19 +297,23 @@ class core_target final : public process_stratum_target
      still be useful.  */
   std::vector<mem_range> m_core_unavailable_mappings;
 
-  /* Build m_core_file_mappings.  Called from the constructor.  */
-  void build_file_mappings ();
+  /* Data structure that holds information mapping filenames and address
+     ranges to the corresponding build-ids as well as the reverse build-id
+     to filename mapping.  */
+  mapped_file_info m_mapped_file_info;
 
-  /* Helper method for xfer_partial.  */
-  enum target_xfer_status xfer_memory_via_mappings (gdb_byte *readbuf,
-						    const gdb_byte *writebuf,
-						    ULONGEST offset,
-						    ULONGEST len,
-						    ULONGEST *xfered_len);
+  /* Build m_core_file_mappings and m_mapped_file_info.  Called from the
+     constructor.  */
+  void build_file_mappings ();
 
   /* FIXME: kettenis/20031023: Eventually this field should
      disappear.  */
   struct gdbarch *m_core_gdbarch = NULL;
+
+  /* If not empty then this contains the name of the executable discovered
+     when processing the memory-mapped file information.  This will only
+     be set if we find a mapped with a suitable build-id.  */
+  std::string m_expected_exec_filename;
 };
 
 core_target::core_target ()
@@ -216,9 +361,51 @@ core_target::core_target ()
 void
 core_target::build_file_mappings ()
 {
+  /* Type holding information about a single file mapped into the inferior
+     at the point when the core file was created.  Associates a build-id
+     with the list of regions the file is mapped into.  */
+  struct mapped_file
+  {
+    /* Type for a region of a file that was mapped into the inferior when
+       the core file was generated.  */
+    struct region
+    {
+      /* Constructor.   See member variables for argument descriptions.  */
+      region (CORE_ADDR start_, CORE_ADDR end_, CORE_ADDR file_ofs_)
+	: start (start_),
+	  end (end_),
+	  file_ofs (file_ofs_)
+      { /* Nothing.  */ }
+
+      /* The inferior address for the start of the mapped region.  */
+      CORE_ADDR start;
+
+      /* The inferior address immediately after the mapped region.  */
+      CORE_ADDR end;
+
+      /* The offset within the mapped file for this content.  */
+      CORE_ADDR file_ofs;
+    };
+
+    /* If not nullptr, then this is the build-id associated with this
+       file.  */
+    const bfd_build_id *build_id = nullptr;
+
+    /* If true then we have seen multiple different build-ids associated
+       with the same filename.  The build_id field will have been set back
+       to nullptr, and we should not set build_id in future.  */
+    bool ignore_build_id_p = false;
+
+    /* All the mapped regions of this file.  */
+    std::vector<region> regions;
+  };
+
   std::unordered_map<std::string, struct bfd *> bfd_map;
   std::unordered_set<std::string> unavailable_paths;
 
+  /* All files mapped into the core file.  The key is the filename.  */
+  std::unordered_map<std::string, mapped_file> mapped_files;
+
   /* See linux_read_core_file_mappings() in linux-tdep.c for an example
      read_core_file_mappings method.  */
   gdbarch_read_core_file_mappings (m_core_gdbarch,
@@ -239,87 +426,206 @@ core_target::build_file_mappings ()
 	   weed out non-file-backed mappings.  */
 	gdb_assert (filename != nullptr);
 
-	if (unavailable_paths.find (filename) != unavailable_paths.end ())
-	  {
-	    /* We have already seen some mapping for FILENAME but failed to
-	       find/open the file.  There is no point in trying the same
-	       thing again so just record that the range [start, end) is
-	       unavailable.  */
-	    m_core_unavailable_mappings.emplace_back (start, end - start);
-	    return;
-	  }
-
-	struct bfd *bfd = bfd_map[filename];
-	if (bfd == nullptr)
+	/* Add this mapped region to the data for FILENAME.  */
+	mapped_file &file_data = mapped_files[filename];
+	file_data.regions.emplace_back (start, end, file_ofs);
+	if (build_id != nullptr && !file_data.ignore_build_id_p)
 	  {
-	    /* Use exec_file_find() to do sysroot expansion.  It'll
-	       also strip the potential sysroot "target:" prefix.  If
-	       there is no sysroot, an equivalent (possibly more
-	       canonical) pathname will be provided.  */
-	    gdb::unique_xmalloc_ptr<char> expanded_fname
-	      = exec_file_find (filename, NULL);
-
-	    if (expanded_fname == nullptr && build_id != nullptr)
-	      debuginfod_exec_query (build_id->data, build_id->size,
-				     filename, &expanded_fname);
-
-	    if (expanded_fname == nullptr)
+	    if (file_data.build_id == nullptr)
+	      file_data.build_id = build_id;
+	    else if (!build_id_equal (build_id, file_data.build_id))
 	      {
-		m_core_unavailable_mappings.emplace_back (start, end - start);
-		unavailable_paths.insert (filename);
-		warning (_("Can't open file %s during file-backed mapping "
-			   "note processing"),
-			 filename);
-		return;
+		warning (_("Multiple build-ids found for %ps"),
+			 styled_string (file_name_style.style (), filename));
+		file_data.build_id = nullptr;
+		file_data.ignore_build_id_p = true;
 	      }
+	  }
+      });
 
-	    bfd = bfd_openr (expanded_fname.get (), "binary");
+  /* Get the build-id of the core file.  */
+  const bfd_build_id *core_build_id
+    = build_id_bfd_get (current_program_space->core_bfd ());
 
-	    if (bfd == nullptr || !bfd_check_format (bfd, bfd_object))
-	      {
-		m_core_unavailable_mappings.emplace_back (start, end - start);
-		unavailable_paths.insert (filename);
-		warning (_("Can't open file %s which was expanded to %s "
+  for (const auto &iter : mapped_files)
+    {
+      const std::string &filename = iter.first;
+      const mapped_file &file_data = iter.second;
+
+      /* If this mapped file has the same build-id as was discovered for
+	 the core-file itself, then we assume this is the main
+	 executable.  Record the filename as we can use this later.  */
+      if (file_data.build_id != nullptr
+	  && m_expected_exec_filename.empty ()
+	  && build_id_equal (file_data.build_id, core_build_id))
+	m_expected_exec_filename = filename;
+
+      /* Use exec_file_find() to do sysroot expansion.  It'll
+	 also strip the potential sysroot "target:" prefix.  If
+	 there is no sysroot, an equivalent (possibly more
+	 canonical) pathname will be provided.  */
+      gdb::unique_xmalloc_ptr<char> expanded_fname
+	= exec_file_find (filename.c_str (), nullptr);
+
+      bool build_id_mismatch = false;
+      if (expanded_fname != nullptr && file_data.build_id != nullptr)
+	{
+	  /* We temporarily open the bfd as a structured target, this
+	     allows us to read the build-id from the bfd if there is one.
+	     For this task it's OK if we reuse an already open bfd object,
+	     so we make this call through GDB's bfd cache.  Once we've
+	     checked the build-id (if there is one) we'll drop this
+	     reference and re-open the bfd using the "binary" target.  */
+	  gdb_bfd_ref_ptr tmp_bfd
+	    = gdb_bfd_open (expanded_fname.get (), gnutarget);
+
+	  if (tmp_bfd != nullptr
+	      && bfd_check_format (tmp_bfd.get (), bfd_object)
+	      && build_id_bfd_get (tmp_bfd.get ()) != nullptr)
+	    {
+	      /* The newly opened TMP_BFD has a build-id, and this mapped
+		 file has a build-id extracted from the core-file.  Check
+		 the build-id's match, and if not, reject TMP_BFD.  */
+	      const struct bfd_build_id *found
+		= build_id_bfd_get (tmp_bfd.get ());
+	      if (!build_id_equal (found, file_data.build_id))
+		build_id_mismatch = true;
+	    }
+	}
+
+      gdb_bfd_ref_ptr abfd;
+      if (expanded_fname != nullptr && !build_id_mismatch)
+	{
+	  struct bfd *b = bfd_openr (expanded_fname.get (), "binary");
+	  abfd = gdb_bfd_ref_ptr::new_reference (b);
+	}
+
+      if ((expanded_fname == nullptr
+	   || abfd == nullptr
+	   || !bfd_check_format (abfd.get (), bfd_object))
+	  && file_data.build_id != nullptr)
+	{
+	  abfd = find_objfile_by_build_id (current_program_space,
+					   file_data.build_id,
+					   filename.c_str ());
+
+	  if (abfd != nullptr)
+	    {
+	      /* The find_objfile_by_build_id will have opened ABFD using
+		 the GNUTARGET global bfd type, however, we need the bfd
+		 opened as the binary type (see the function's header
+		 comment), so now we reopen ABFD with the desired binary
+		 type.  */
+	      expanded_fname
+		= make_unique_xstrdup (bfd_get_filename (abfd.get ()));
+	      struct bfd *b = bfd_openr (expanded_fname.get (), "binary");
+	      gdb_assert (b != nullptr);
+	      abfd = gdb_bfd_ref_ptr::new_reference (b);
+	    }
+	}
+
+      std::vector<mem_range> ranges;
+      for (const mapped_file::region &region : file_data.regions)
+	ranges.emplace_back (region.start, region.end - region.start);
+
+      if (expanded_fname == nullptr
+	  || abfd == nullptr
+	  || !bfd_check_format (abfd.get (), bfd_object))
+	{
+	  /* If ABFD was opened, but the wrong format, close it now.  */
+	  abfd = nullptr;
+
+	  /* Record all regions for this file as unavailable.  */
+	  for (const mapped_file::region &region : file_data.regions)
+	    m_core_unavailable_mappings.emplace_back (region.start,
+						      region.end
+						      - region.start);
+
+	  /* And give the user an appropriate warning.  */
+	  if (build_id_mismatch)
+	    {
+	      if (expanded_fname == nullptr
+		  || filename == expanded_fname.get ())
+		warning (_("File %ps doesn't match build-id from core-file "
+			   "during file-backed mapping processing"),
+			 styled_string (file_name_style.style (),
+					filename.c_str ()));
+	      else
+		warning (_("File %ps which was expanded to %ps, doesn't match "
+			   "build-id from core-file during file-backed "
+			   "mapping processing"),
+			 styled_string (file_name_style.style (),
+					filename.c_str ()),
+			 styled_string (file_name_style.style (),
+					expanded_fname.get ()));
+	    }
+	  else
+	    {
+	      if (expanded_fname == nullptr
+		  || filename == expanded_fname.get ())
+		warning (_("Can't open file %ps during file-backed mapping "
+			   "note processing"),
+			 styled_string (file_name_style.style (),
+					filename.c_str ()));
+	      else
+		warning (_("Can't open file %ps which was expanded to %ps "
 			   "during file-backed mapping note processing"),
-			 filename, expanded_fname.get ());
+			 styled_string (file_name_style.style (),
+					filename.c_str ()),
+			 styled_string (file_name_style.style (),
+					expanded_fname.get ()));
+	    }
+	}
+      else
+	{
+	  /* Ensure that the bfd will be closed when core_bfd is closed.
+	     This can be checked before/after a core file detach via "maint
+	     info bfds".  */
+	  gdb_bfd_record_inclusion (current_program_space->core_bfd (),
+				    abfd.get ());
+
+	  /* Create sections for each mapped region.  */
+	  for (const mapped_file::region &region : file_data.regions)
+	    {
+	      /* Make new BFD section.  All sections have the same name,
+		 which is permitted by bfd_make_section_anyway().  */
+	      asection *sec = bfd_make_section_anyway (abfd.get (), "load");
+	      if (sec == nullptr)
+		error (_("Can't make section"));
+	      sec->filepos = region.file_ofs;
+	      bfd_set_section_flags (sec, SEC_READONLY | SEC_HAS_CONTENTS);
+	      bfd_set_section_size (sec, region.end - region.start);
+	      bfd_set_section_vma (sec, region.start);
+	      bfd_set_section_lma (sec, region.start);
+	      bfd_set_section_alignment (sec, 2);
+
+	      /* Set target_section fields.  */
+	      m_core_file_mappings.emplace_back (region.start, region.end, sec);
+	    }
+	}
 
-		if (bfd != nullptr)
-		  bfd_close (bfd);
-		return;
-	      }
-	    /* Ensure that the bfd will be closed when core_bfd is closed. 
-	       This can be checked before/after a core file detach via
-	       "maint info bfds".  */
-	    gdb_bfd_record_inclusion (current_program_space->core_bfd (), bfd);
-	    bfd_map[filename] = bfd;
-	  }
+      /* If this is a bfd with a build-id then record the filename,
+	 optional soname (DT_SONAME .dynamic attribute), and the range of
+	 addresses at which this bfd is mapped.  This information can be
+	 used to perform build-id checking when loading the shared
+	 libraries.  */
+      if (file_data.build_id != nullptr)
+	{
+	  normalize_mem_ranges (&ranges);
 
-	/* Make new BFD section.  All sections have the same name,
-	   which is permitted by bfd_make_section_anyway().  */
-	asection *sec = bfd_make_section_anyway (bfd, "load");
-	if (sec == nullptr)
-	  error (_("Can't make section"));
-	sec->filepos = file_ofs;
-	bfd_set_section_flags (sec, SEC_READONLY | SEC_HAS_CONTENTS);
-	bfd_set_section_size (sec, end - start);
-	bfd_set_section_vma (sec, start);
-	bfd_set_section_lma (sec, start);
-	bfd_set_section_alignment (sec, 2);
-
-	/* Set target_section fields.  */
-	m_core_file_mappings.emplace_back (start, end, sec);
-
-	/* If this is a bfd of a shared library, record its soname
-	   and build id.  */
-	if (build_id != nullptr)
-	  {
-	    gdb::unique_xmalloc_ptr<char> soname
-	      = gdb_bfd_read_elf_soname (bfd->filename);
-	    if (soname != nullptr)
-	      set_cbfd_soname_build_id (current_program_space->cbfd,
-					soname.get (), build_id);
-	  }
-      });
+	  const char *actual_filename = nullptr;
+	  gdb::unique_xmalloc_ptr<char> soname;
+	  if (abfd != nullptr)
+	    {
+	      actual_filename = bfd_get_filename (abfd.get ());
+	      soname = gdb_bfd_read_elf_soname (actual_filename);
+	    }
+
+	  m_mapped_file_info.add (soname.get (), filename.c_str (),
+				  actual_filename, std::move (ranges),
+				  file_data.build_id);
+	}
+    }
 
   normalize_mem_ranges (&m_core_unavailable_mappings);
 }
@@ -564,36 +870,31 @@ rename_vmcore_idle_reg_sections (bfd *abfd, inferior *inf)
    BFD ABFD.  */
 
 static void
-locate_exec_from_corefile_build_id (bfd *abfd, int from_tty)
+locate_exec_from_corefile_build_id (bfd *abfd, core_target *target,
+				    int from_tty)
 {
   const bfd_build_id *build_id = build_id_bfd_get (abfd);
   if (build_id == nullptr)
     return;
 
-  gdb_bfd_ref_ptr execbfd
-    = build_id_to_exec_bfd (build_id->size, build_id->data);
+  /* The filename used for the find_objfile_by_build_id call.  */
+  std::string filename;
 
-  if (execbfd == nullptr)
+  if (!target->expected_exec_filename ().empty ())
+    filename = target->expected_exec_filename ();
+  else
     {
-      /* Attempt to query debuginfod for the executable.  */
-      gdb::unique_xmalloc_ptr<char> execpath;
-      scoped_fd fd = debuginfod_exec_query (build_id->data, build_id->size,
-					    abfd->filename, &execpath);
-
-      if (fd.get () >= 0)
-	{
-	  execbfd = gdb_bfd_open (execpath.get (), gnutarget);
-
-	  if (execbfd == nullptr)
-	    warning (_("\"%s\" from debuginfod cannot be opened as bfd: %s"),
-		     execpath.get (),
-		     gdb_bfd_errmsg (bfd_get_error (), nullptr).c_str ());
-	  else if (!build_id_verify (execbfd.get (), build_id->size,
-				     build_id->data))
-	    execbfd.reset (nullptr);
-	}
+      /* We didn't find an executable name from the mapped file
+	 information, so as a stand-in build a string based on the
+	 build-id.  */
+      std::string build_id_hex_str = bin2hex (build_id->data, build_id->size);
+      filename = string_printf ("with build-id %s", build_id_hex_str.c_str ());
     }
 
+  gdb_bfd_ref_ptr execbfd
+    = find_objfile_by_build_id (current_program_space, build_id,
+				filename.c_str ());
+
   if (execbfd != nullptr)
     {
       exec_file_attach (bfd_get_filename (execbfd.get ()), from_tty);
@@ -724,7 +1025,7 @@ core_target_open (const char *arg, int from_tty)
 
   if (current_program_space->exec_bfd () == nullptr)
     locate_exec_from_corefile_build_id (current_program_space->core_bfd (),
-					from_tty);
+					target, from_tty);
 
   post_create_inferior (from_tty);
 
@@ -963,55 +1264,6 @@ core_target::files_info ()
   print_section_info (&m_core_section_table, current_program_space->core_bfd ());
 }
 
-/* Helper method for core_target::xfer_partial.  */
-
-enum target_xfer_status
-core_target::xfer_memory_via_mappings (gdb_byte *readbuf,
-				       const gdb_byte *writebuf,
-				       ULONGEST offset, ULONGEST len,
-				       ULONGEST *xfered_len)
-{
-  enum target_xfer_status xfer_status;
-
-  xfer_status = (section_table_xfer_memory_partial
-		   (readbuf, writebuf,
-		    offset, len, xfered_len,
-		    m_core_file_mappings));
-
-  if (xfer_status == TARGET_XFER_OK || m_core_unavailable_mappings.empty ())
-    return xfer_status;
-
-  /* There are instances - e.g. when debugging within a docker
-     container using the AUFS storage driver - where the pathnames
-     obtained from the note section are incorrect.  Despite the path
-     being wrong, just knowing the start and end addresses of the
-     mappings is still useful; we can attempt an access of the file
-     stratum constrained to the address ranges corresponding to the
-     unavailable mappings.  */
-
-  ULONGEST memaddr = offset;
-  ULONGEST memend = offset + len;
-
-  for (const auto &mr : m_core_unavailable_mappings)
-    {
-      if (mr.contains (memaddr))
-	{
-	  if (!mr.contains (memend))
-	    len = mr.start + mr.length - memaddr;
-
-	  xfer_status = this->beneath ()->xfer_partial (TARGET_OBJECT_MEMORY,
-							NULL,
-							readbuf,
-							writebuf,
-							offset,
-							len,
-							xfered_len);
-	  break;
-	}
-    }
-
-  return xfer_status;
-}
 
 enum target_xfer_status
 core_target::xfer_partial (enum target_object object, const char *annex,
@@ -1039,26 +1291,72 @@ core_target::xfer_partial (enum target_object object, const char *annex,
 	if (xfer_status == TARGET_XFER_OK)
 	  return TARGET_XFER_OK;
 
-	/* Check file backed mappings.  If they're available, use
-	   core file provided mappings (e.g. from .note.linuxcore.file
-	   or the like) as this should provide a more accurate
-	   result.  If not, check the stratum beneath us, which should
-	   be the file stratum.
+	/* Check file backed mappings.  If they're available, use core file
+	   provided mappings (e.g. from .note.linuxcore.file or the like)
+	   as this should provide a more accurate result.  */
+	if (!m_core_file_mappings.empty ())
+	  {
+	    xfer_status = section_table_xfer_memory_partial
+			    (readbuf, writebuf, offset, len, xfered_len,
+			     m_core_file_mappings);
+	    if (xfer_status == TARGET_XFER_OK)
+	      return xfer_status;
+	  }
 
-	   We also check unavailable mappings due to Docker/AUFS driver
-	   issues.  */
-	if (!m_core_file_mappings.empty ()
-	    || !m_core_unavailable_mappings.empty ())
+	/* If the access is within an unavailable file mapping then we try
+	   to check in the stratum below (the executable stratum).  The
+	   thinking here is that if the mapping was read/write then the
+	   contents would have been written into the core file and the
+	   access would have been satisfied by m_core_section_table.
+
+	   But if the access has not yet been resolved then we can assume
+	   the access is read-only.  If the executable was not found
+	   during the mapped file check then we'll have an unavailable
+	   mapping entry, however, if the user has provided the executable
+	   (maybe in a different location) then we might be able to
+	   resolve the access from there.
+
+	   If that fails, but the access is within an unavailable region,
+	   then the access itself should fail.  */
+	for (const auto &mr : m_core_unavailable_mappings)
 	  {
-	    xfer_status = xfer_memory_via_mappings (readbuf, writebuf, offset,
+	    if (mr.contains (offset))
+	      {
+		if (!mr.contains (offset + len))
+		  len = mr.start + mr.length - offset;
+
+		xfer_status
+		  = this->beneath ()->xfer_partial (TARGET_OBJECT_MEMORY,
+						    nullptr, readbuf,
+						    writebuf, offset,
 						    len, xfered_len);
+		if (xfer_status == TARGET_XFER_OK)
+		  return TARGET_XFER_OK;
+
+		return TARGET_XFER_E_IO;
+	      }
+	  }
+
+	/* The following is acting as a fallback in case we encounter a
+	   situation where the core file is lacking and mapped file
+	   information.  Here we query the exec file stratum to see if it
+	   can resolve the access.  Doing this when we are missing mapped
+	   file information might be the best we can do, but there are
+	   certainly cases this will get wrong, e.g. if an inferior created
+	   a zero initialised mapping over the top of some data that exists
+	   within the executable then this will return the executable data
+	   rather than the zero data.  Maybe we should just drop this
+	   block?  */
+	if (m_core_file_mappings.empty ()
+	    && m_core_unavailable_mappings.empty ())
+	  {
+	    xfer_status
+	      = this->beneath ()->xfer_partial (object, annex, readbuf,
+						writebuf, offset, len,
+						xfered_len);
+	    if (xfer_status == TARGET_XFER_OK)
+	      return TARGET_XFER_OK;
 	  }
-	else
-	  xfer_status = this->beneath ()->xfer_partial (object, annex, readbuf,
-							writebuf, offset, len,
-							xfered_len);
-	if (xfer_status == TARGET_XFER_OK)
-	  return TARGET_XFER_OK;
 
 	/* Finally, attempt to access data in core file sections with
 	   no contents.  These will typically read as all zero.  */
@@ -1515,6 +1813,182 @@ maintenance_print_core_file_backed_mappings (const char *args, int from_tty)
     targ->info_proc_mappings (targ->core_gdbarch ());
 }
 
+/* Add more details discovered while processing the core-file's mapped file
+   information, we're building maps between filenames and the corresponding
+   build-ids, between address ranges and the corresponding build-ids, and
+   also a reverse map between build-id and the corresponding filename.
+
+   SONAME is the DT_SONAME attribute extracted from the .dynamic section of
+   a shared library that was mapped into the core file.  This can be
+   nullptr if the mapped files was not a shared library, or didn't have a
+   DT_SONAME attribute.
+
+   EXPECTED_FILENAME is the name of the file that was mapped into the
+   inferior as extracted from the core file, this should never be nullptr.
+
+   ACTUAL_FILENAME is the name of the actual file GDB found to provide the
+   mapped file information, this can be nullptr if GDB failed to find a
+   suitable file.  This might be different to EXPECTED_FILENAME, e.g. GDB
+   might have downloaded the file from debuginfod and so ACTUAL_FILENAME
+   will be a file in the debuginfod client cache.
+
+   RANGES is the list of memory ranges at which this file was mapped into
+   the inferior.
+
+   BUILD_ID is the build-id for this mapped file, this will never be
+   nullptr.  Not every mapped file will have a build-id, but there's no
+   point calling this function if we failed to find a build-id; this
+   structure only exists so we can lookup files based on their build-id.  */
+
+void
+mapped_file_info::add (const char *soname,
+		       const char *expected_filename,
+		       const char *actual_filename,
+		       std::vector<mem_range> &&ranges,
+		       const bfd_build_id *build_id)
+{
+  gdb_assert (build_id != nullptr);
+  gdb_assert (expected_filename != nullptr);
+
+  if (soname != nullptr)
+    {
+      /* If we already have an entry with this SONAME then this indicates
+	 that the inferior has two files mapped into memory with different
+	 file names (and most likely different build-ids), but with the
+	 same DT_SONAME attribute.  In this case we can't use the
+	 DT_SONAME to figure out the expected build-id of a shared
+	 library, so poison the entry for this SONAME by setting the entry
+	 to nullptr.  */
+      auto it = m_soname_to_build_id_map.find (soname);
+      if (it != m_soname_to_build_id_map.end ()
+	  && it->second != nullptr
+	  && !build_id_equal (it->second, build_id))
+	m_soname_to_build_id_map[soname] = nullptr;
+      else
+	m_soname_to_build_id_map[soname] = build_id;
+    }
+
+  /* When the core file is initially opened and the mapped files are
+     parsed, we group the build-id information based on the file name.  As
+     a consequence, we should see each EXPECTED_FILENAME value exactly
+     once.  This means that each insertion should always succeed.  */
+  const auto [it, inserted]
+    = m_filename_to_build_id_map.emplace (expected_filename, build_id);
+  gdb_assert (inserted);
+
+  /* Setup the reverse build-id to file name map.  */
+  if (actual_filename != nullptr)
+    m_build_id_to_filename_map.emplace (build_id, actual_filename);
+
+  /* Setup the list of memory range to build-id objects.  */
+  for (mem_range &r : ranges)
+    m_address_to_build_id_list.emplace_back (std::move (r), build_id);
+
+  /* At this point the m_address_to_build_id_list is unsorted (we just
+     added some entries to the end of the list).  All entries should be
+     added before any look-ups are performed, and the list is only sorted
+     when the first look-up is performed.  */
+  gdb_assert (!m_address_to_build_id_list_sorted);
+}
+
+/* FILENAME is the name of a file GDB is trying to load, and ADDR is
+   (optionally) an address within the file in the inferior's address space.
+
+   Search through the information gathered from the core-file's mapped file
+   information looking for a file named FILENAME, or for a file that covers
+   ADDR.  If a match is found then return the build-id for the file along
+   with the location where GDB found the mapped file.
+
+   The location of the mapped file might be the empty string if GDB was
+   unable to find the mapped file.
+
+   If no build-id can be found for FILENAME then GDB will return a pair
+   containing nullptr (for the build-id) and an empty string for the file
+   name.  */
+
+std::optional <core_target_mapped_file_info>
+mapped_file_info::lookup (const char *filename,
+			  const std::optional<CORE_ADDR> &addr)
+{
+  if (filename != nullptr)
+    {
+      /* If there's a matching entry in m_filename_to_build_id_map then the
+	 associated build-id will not be nullptr, and can be used to
+	 validate that FILENAME is correct.  */
+      auto it = m_filename_to_build_id_map.find (filename);
+      if (it != m_filename_to_build_id_map.end ())
+	return make_result (it->second);
+    }
+
+  if (addr.has_value ())
+    {
+      /* On the first lookup, sort the address_to_build_id_list.  */
+      if (!m_address_to_build_id_list_sorted)
+	{
+	  std::sort (m_address_to_build_id_list.begin (),
+		     m_address_to_build_id_list.end (),
+		     [] (const mem_range_and_build_id &a,
+			 const mem_range_and_build_id &b) {
+		       return a.range < b.range;
+		     });
+	  m_address_to_build_id_list_sorted = true;
+	}
+
+      /* Look for the first entry whose range's start address is not less
+	 than, or equal too, the address ADDR.  If we find such an entry,
+	 then the previous entry's range might contain ADDR.  If it does
+	 then that previous entry's build-id can be used.  */
+      auto it = std::lower_bound
+	(m_address_to_build_id_list.begin (),
+	 m_address_to_build_id_list.end (),
+	 *addr,
+	 [] (const mem_range_and_build_id &a,
+	     const CORE_ADDR &b) {
+	  return a.range.start <= b;
+	});
+
+      if (it != m_address_to_build_id_list.begin ())
+	{
+	  --it;
+
+	  if (it->range.contains (*addr))
+	    return make_result (it->build_id);
+	}
+    }
+
+  if (filename != nullptr)
+    {
+      /* If the basename of FILENAME appears in m_soname_to_build_id_map
+	 then when the mapped files were processed, we saw a file with a
+	 DT_SONAME attribute corresponding to FILENAME, use that build-id
+	 to validate FILENAME.
+
+	 However, the build-id in this map might be nullptr if we saw
+	 multiple mapped files with the same DT_SONAME attribute (though
+	 this should be pretty rare).  */
+      auto it
+	= m_soname_to_build_id_map.find (lbasename (filename));
+      if (it != m_soname_to_build_id_map.end ()
+	  && it->second != nullptr)
+	return make_result (it->second);
+    }
+
+  return {};
+}
+
+/* See gdbcore.h.  */
+
+std::optional <core_target_mapped_file_info>
+core_target_find_mapped_file (const char *filename,
+			      std::optional<CORE_ADDR> addr)
+{
+  core_target *targ = get_current_core_target ();
+  if (targ == nullptr || current_program_space->cbfd.get () == nullptr)
+    return {};
+
+  return targ->lookup_mapped_file_info (filename, addr);
+}
+
 void _initialize_corelow ();
 void
 _initialize_corelow ()
diff --git a/gdb/data-directory/Makefile.in b/gdb/data-directory/Makefile.in
--- a/gdb/data-directory/Makefile.in
+++ b/gdb/data-directory/Makefile.in
@@ -77,6 +77,8 @@ PYTHON_FILE_LIST = \
 	gdb/FrameIterator.py \
 	gdb/frames.py \
 	gdb/missing_debug.py \
+	gdb/missing_objfile.py \
+	gdb/missing_files.py \
 	gdb/printing.py \
 	gdb/prompt.py \
 	gdb/styling.py \
@@ -86,7 +88,7 @@ PYTHON_FILE_LIST = \
 	gdb/command/__init__.py \
 	gdb/command/explore.py \
 	gdb/command/frame_filters.py \
-	gdb/command/missing_debug.py \
+	gdb/command/missing_files.py \
 	gdb/command/pretty_printers.py \
 	gdb/command/prompt.py \
 	gdb/command/type_printers.py \
diff --git a/gdb/debuginfod-support.c b/gdb/debuginfod-support.c
--- a/gdb/debuginfod-support.c
+++ b/gdb/debuginfod-support.c
@@ -409,7 +409,7 @@ debuginfod_exec_query (const unsigned char *build_id,
   std::optional<target_terminal::scoped_restore_terminal_state> term_state;
 
   {
-    user_data data ("executable for", filename);
+    user_data data ("file", filename);
 
     debuginfod_set_user_data (c, &data);
     if (target_supports_terminal_ours ())
@@ -423,7 +423,7 @@ debuginfod_exec_query (const unsigned char *build_id,
     debuginfod_set_user_data (c, nullptr);
   }
 
-  print_outcome (fd.get (), "executable for", filename);
+  print_outcome (fd.get (), "file", filename);
 
   if (fd.get () >= 0)
     destname->reset (dname);
diff --git a/gdb/doc/gdb.texinfo b/gdb/doc/gdb.texinfo
--- a/gdb/doc/gdb.texinfo
+++ b/gdb/doc/gdb.texinfo
@@ -21786,6 +21786,7 @@
 @c (eg rooted in val of env var GDBSYMS) could exist for mappable symbol
 @c files.
 
+@anchor{core-file command}
 @kindex core-file
 @item core-file @r{[}@var{filename}@r{]}
 @itemx core
@@ -24534,6 +24535,10 @@
 @tab @code{vFile:fstat}
 @tab Host I/O
 
+@item @code{hostio-stat-packet}
+@tab @code{vFile:stat}
+@tab Host I/O
+
 @item @code{hostio-setfs-packet}
 @tab @code{vFile:setfs}
 @tab Host I/O
@@ -46289,6 +46294,13 @@
 If an error occurs the return value is -1.  The format of the
 returned binary attachment is as described in @ref{struct stat}.
 
+@item vFile:stat: @var{filename}
+Get information about the file @var{filename} on the target.
+On success the information is returned as a binary attachment
+and the return value is the size of this attachment in bytes.
+If an error occurs the return value is -1.  The format of the
+returned binary attachment is as described in @ref{struct stat}.
+
 @item vFile:unlink: @var{filename}
 Delete the file at @var{filename} on the target.  Return 0,
 or -1 if an error occurs.  The @var{filename} is a string.
diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi
--- a/gdb/doc/python.texi
+++ b/gdb/doc/python.texi
@@ -231,6 +231,7 @@
 * TUI Windows In Python::       Implementing new TUI windows.
 * Disassembly In Python::       Instruction Disassembly In Python
 * Missing Debug Info In Python:: Handle missing debug info from Python.
+* Missing Objfiles In Python::  Handle objfiles from Python.
 @end menu
 
 @node Basic Python
@@ -5370,10 +5371,11 @@
 objects.  @xref{Frame Filter API}, for more information.
 @end defvar
 
-@defvar Progspace.missing_debug_handlers
-The @code{missing_debug_handlers} attribute is a list of the missing
-debug handler objects for this program space.  @xref{Missing Debug
-Info In Python}, for more information.
+@defvar Progspace.missing_file_handlers
+The @code{missing_file_handlers} attribute is a list of tuples.  Each
+tuple holds a missing-file handler object for this program space.  For
+more information, @pxref{Missing Debug Info In Python}, and
+@ref{Missing Objfiles In Python}.
 @end defvar
 
 A program space has the following methods:
@@ -5549,6 +5551,7 @@
 @code{gdb.Objfile.add_separate_debug_file} method, described below.
 @end defvar
 
+@anchor{Objfile.build_id}
 @defvar Objfile.build_id
 The build ID of the objfile as a string.
 If the objfile does not have a build ID then the value is @code{None}.
@@ -8137,6 +8140,189 @@
 for this objfile.
 @end defun
 
+@node Missing Objfiles In Python
+@subsubsection Missing Objfiles In Python
+@cindex python, handle missing objfiles
+
+When @value{GDBN} opens a core file, for example with the
+@kbd{core-file} command (@pxref{core-file command}), @value{GDBN} will
+attempt to load the corresponding executable and shared libraries.
+Often these files can be found on the local machine, but sometimes
+these files cannot be found, in which case the debugging experience
+will be restricted.
+
+If @value{GDBN} fails to locate a particular file then there is an
+opportunity for a Python extension to step in.  A Python extension can
+potentially locate the missing file using some platform- or
+project-specific steps, and inform @value{GDBN} of its location.  Or a
+Python extension might provide some platform- or project-specific
+advice to the user about how to obtain the missing file.
+
+A missing-objfile Python extension consists of a handler object which
+has the @code{name} and @code{enabled} attributes, and implements the
+@code{__call__} method.  When @value{GDBN} encounters a situation
+where a file cannot be found, but the build-id (@pxref{build ID}) for
+the missing file is known, then the @code{__call__} method is invoked
+to try and find the file.  Full details of how handlers are written
+can be found below.
+
+@subheading The @code{gdb.missing_objfile} Module
+
+@value{GDBN} comes with a @code{gdb.missing_objfile} module which
+contains the following class and global function:
+
+@deftp{class} gdb.missing_objfile.MissingObjfileHandler
+
+@code{MissingObjfileHandler} is a base class from which user-created
+handlers can derive, though it is not required that handlers derive
+from this class, so long as any user created handler has the
+@code{name} and @code{enabled} attributes, and implements the
+@code{__call__} method.
+
+@defun MissingObjfileHandler.__init__ (name)
+The @var{name} is a string used to reference this missing-objfile
+handler within some @value{GDBN} commands.  Valid names consist of the
+characters @samp{[-_a-zA-Z0-9]}, creating a handler with an invalid
+name raises a @code{ValueError} exception.
+@end defun
+
+@defun MissingObjfileHandler.__call__ (pspace, build_id, filename)
+
+Sub-classes must override the @code{__call__} method.  The
+@var{pspace} argument will be a @code{gdb.Progspace}
+(@pxref{Progspaces In Python}), this is the program space in which
+@value{GDBN} is looking for the missing file.
+
+The @var{build_id} argument is a string containing the build-id of the
+file that is missing, this will be in the same format as returned by
+@code{Objfile.build_id} (@pxref{Objfile.build_id}).
+
+The @var{filename} argument contains the name of the file that
+@value{GDBN} is looking for.  This information is provided to allow
+handlers to generate informative messages for the user.  A handler is
+not required to place the missing file at this location.  There might
+already be a file present at this location, but it might not match the
+required build-id, in which case @value{GDBN} will have ignored it.
+In some limited cases @value{GDBN} might not be able to establish the
+@var{filename} of the file it is searching for, in this case
+@value{GDBN} will use a string @samp{with build-id @var{build_id}} as a
+replacement.
+
+The return value from the @code{__call__} method indicates what
+@value{GDBN} should do next.  The possible return values are:
+
+@itemize @bullet
+@item @code{None}
+
+This indicates that this handler could not locate the missing file and
+@value{GDBN} should call any other registered handlers.
+
+@item @code{True}
+
+This indicates that this handler has installed the missing file into a
+location where @value{GDBN} would normally expect to find it.  The
+only location in which @value{GDBN} will look is within the
+@file{.build-id} sub-directory within the @var{debug-file-directory}
+(@pxref{debug-file-directory}).
+
+@value{GDBN} will repeat the normal lookup process, which should now
+find the previously missing file.
+
+If @value{GDBN} still doesn't find file after this second attempt,
+then the Python missing-objfile handlers are not invoked a second
+time, this prevents a badly behaved handler causing @value{GDBN} to
+get stuck in a loop.  @value{GDBN} will continue without the missing
+file, though this will degrade the debugging experience.
+
+@item @code{False}
+
+This indicates that this handler has done everything that it intends
+to do but the missing file could not be found.  @value{GDBN} will not
+call any other registered handlers to look for the missing file.
+@value{GDBN} will continue without the missing file, though this will
+degrade the debugging experience.
+
+@item A string
+
+The returned string should contain a filename.  @value{GDBN} will not
+call any further registered handlers, and will instead use the
+returned filename as the missing file.
+@end itemize
+
+Invoking the @code{__call__} method from this base class will raise a
+@code{NotImplementedError} exception.
+@end defun
+
+@defvar MissingObjfileHandler.name
+A read-only attribute which is a string, the name of this handler
+passed to the @code{__init__} method.
+@end defvar
+
+@defvar MissingObjfileHandler.enabled
+A modifiable attribute containing a boolean; when @code{True}, the
+handler is enabled, and will be used by @value{GDBN}.  When
+@code{False}, the handler has been disabled, and will not be used.
+@end defvar
+@end deftp
+
+@defun gdb.missing_objfile.register_handler (locus, handler, replace=@code{False})
+Register a new missing-objfile handler with @value{GDBN}.
+
+@var{handler} is an instance of a sub-class of
+@code{MissingObjfileHandler}, or at least an instance of an object that
+has the same attributes and methods as @code{MissingObjfileHandler}.
+
+@var{locus} specifies to which handler list to prepend @var{handler}.
+It can be either a @code{gdb.Progspace} (@pxref{Progspaces In Python})
+or @code{None}, in which case the handler is registered globally.  The
+newly registered @var{handler} will be called before any other handler
+from the same locus.  Two handlers in the same locus cannot have the
+same name, an attempt to add a handler with an already existing name
+raises an exception unless @var{replace} is @code{True}, in which case
+the old handler is deleted and the new handler is prepended to the
+selected handler list.
+
+@value{GDBN} first calls the handlers for the current program space,
+and then the globally registered handlers.  As soon as a handler
+returns a value other than @code{None}, no further handlers are
+called.
+@end defun
+
+@subheading Managing Missing-Objfile Handlers
+
+@value{GDBN} defines the following commands to manage registered
+missing-objfile handlers:
+
+@table @code
+
+@kindex info missing-objfile-handlers
+@item info missing-objfile-handlers @r{[} @var{locus} @r{[} @var{name-regexp} @r{]} @r{]}
+Lists all registered missing-objfile handlers.  Arguments @var{locus}
+and @var{name-regexp} are both optional and can be used to filter
+which handlers are listed.
+
+The @var{locus} argument should be either @kbd{global},
+@kbd{progspace}, or the name of an object file.  Only handlers
+registered for the specified locus will be listed.
+
+The @var{name-regexp} is a regular expression used to match against
+handler names.
+
+@kindex disable missing-objfile-handler
+@item disable missing-objfile-handler @r{[} @var{locus} @r{[} @var{name-regexp} @r{]} @r{]}
+The @var{locus} and @var{name-regexp} are interpreted as in @kbd{info
+missing-objfile-handlers} above, but instead of listing the matching
+handlers, all of the matching handlers are disabled.  The
+@code{enabled} field of each matching handler is set to @code{False}.
+
+@kindex enable missing-objfile-handler
+@item enable missing-objfile-handler @r{[} @var{locus} @r{[} @var{name-regexp} @r{]} @r{]}
+The @var{locus} and @var{name-regexp} are interpreted as in @kbd{info
+missing-objfile-handlers} above, but instead of listing the matching
+handlers, all of the matching handlers are enabled.  The
+@code{enabled} field of each matching handler is set to @code{True}.
+@end table
+
 @node Python Auto-loading
 @subsection Python Auto-loading
 @cindex Python auto-loading
diff --git a/gdb/extension-priv.h b/gdb/extension-priv.h
--- a/gdb/extension-priv.h
+++ b/gdb/extension-priv.h
@@ -287,9 +287,22 @@ struct extension_language_ops
   /* Give extension languages a chance to deal with missing debug
      information.  OBJFILE is the file for which GDB was unable to find
      any debug information.  */
-  ext_lang_missing_debuginfo_result
+  ext_lang_missing_file_result
     (*handle_missing_debuginfo) (const struct extension_language_defn *,
 				 struct objfile *objfile);
+
+  /* Give extension languages a chance to deal with missing objfiles.
+     PSPACE is the program space in which GDB is searching for a missing
+     objfile, and will not be NULL.  BUILD_ID is the build-id of the
+     objfile we're looking for, and will not be NULL.  FILENAME is the name
+     of the file we're looking for, and will not be NULL.  See
+     ext_lang_find_objfile_from_buildid for some additional information
+     about the meaning of FILENAME.  */
+  ext_lang_missing_file_result
+    (*find_objfile_from_buildid) (const struct extension_language_defn *,
+				  program_space *pspace,
+				  const struct bfd_build_id *build_id,
+				  const char *filename);
 };
 
 /* State necessary to restore a signal handler to its previous value.  */
diff --git a/gdb/extension.c b/gdb/extension.c
--- a/gdb/extension.c
+++ b/gdb/extension.c
@@ -1040,7 +1040,7 @@ ext_lang_print_insn (struct gdbarch *gdbarch, CORE_ADDR address,
 
 /* See extension.h.  */
 
-ext_lang_missing_debuginfo_result
+ext_lang_missing_file_result
 ext_lang_handle_missing_debuginfo (struct objfile *objfile)
 {
   for (const struct extension_language_defn *extlang : extension_languages)
@@ -1048,7 +1048,7 @@ ext_lang_handle_missing_debuginfo (struct objfile *objfile)
       if (extlang->ops == nullptr
 	  || extlang->ops->handle_missing_debuginfo == nullptr)
 	continue;
-      ext_lang_missing_debuginfo_result result
+      ext_lang_missing_file_result result
 	= extlang->ops->handle_missing_debuginfo (extlang, objfile);
       if (!result.filename ().empty () || result.try_again ())
 	return result;
@@ -1057,6 +1057,28 @@ ext_lang_handle_missing_debuginfo (struct objfile *objfile)
   return {};
 }
 
+/* See extension.h.  */
+
+ext_lang_missing_file_result
+ext_lang_find_objfile_from_buildid (program_space *pspace,
+				    const struct bfd_build_id *build_id,
+				    const char *filename)
+{
+  for (const struct extension_language_defn *extlang : extension_languages)
+    {
+      if (extlang->ops == nullptr
+	  || extlang->ops->find_objfile_from_buildid == nullptr)
+	continue;
+      ext_lang_missing_file_result result
+	= extlang->ops->find_objfile_from_buildid (extlang, pspace, build_id,
+						   filename);
+      if (!result.filename ().empty () || result.try_again ())
+	return result;
+    }
+
+  return {};
+}
+
 /* Called via an observer before gdb prints its prompt.
    Iterate over the extension languages giving them a chance to
    change the prompt.  The first one to change the prompt wins,
diff --git a/gdb/extension.h b/gdb/extension.h
--- a/gdb/extension.h
+++ b/gdb/extension.h
@@ -36,6 +36,7 @@ struct ui_file;
 struct ui_out;
 struct value;
 struct value_print_options;
+struct program_space;
 
 /* A function to load and process a script file.
    The file has been opened and is ready to be read from the beginning.
@@ -358,23 +359,23 @@ extern std::optional<int> ext_lang_print_insn
    it.  And the third option is for the extension to just return a null
    result, indication there is nothing the extension can do to provide the
    missing debug information.  */
-struct ext_lang_missing_debuginfo_result
+struct ext_lang_missing_file_result
 {
   /* Default result.  The extension was unable to provide the missing debug
      info.  */
-  ext_lang_missing_debuginfo_result ()
+  ext_lang_missing_file_result ()
   { /* Nothing.  */ }
 
   /* When TRY_AGAIN is true GDB should try searching again, the extension
      may have installed the missing debug info into a suitable location.
      When TRY_AGAIN is false this is equivalent to the default, no
      argument, constructor.  */
-  ext_lang_missing_debuginfo_result (bool try_again)
+  ext_lang_missing_file_result (bool try_again)
     : m_try_again (try_again)
   { /* Nothing.  */ }
 
   /* Look in FILENAME for the missing debug info.  */
-  ext_lang_missing_debuginfo_result (std::string &&filename)
+  ext_lang_missing_file_result (std::string &&filename)
     : m_filename (std::move (filename))
   { /* Nothing.  */ }
 
@@ -404,9 +405,28 @@ struct ext_lang_missing_debuginfo_result
 
 /* Called when GDB failed to find any debug information for OBJFILE.  */
 
-extern ext_lang_missing_debuginfo_result ext_lang_handle_missing_debuginfo
+extern ext_lang_missing_file_result ext_lang_handle_missing_debuginfo
   (struct objfile *objfile);
 
+/* Called when GDB opens a core-file to find any object files for which a
+   build-id could be extracted from the core-file, but the matching file
+   could not otherwise be found by GDB.
+
+   PSPACE is the program space in which GDB is opening the core-file and
+   is looking for a missing object file.  BUILD_ID is the build-id of the
+   file being looked for, and will not be NULL.  FILENAME is the name of
+   the file GDB is looking for, this will not be NULL.  The FILENAME is
+   provided only for creating helpful messages for the user.  FILENAME
+   might already exist on disk but have the wrong build-id, of FILENAME
+   might not exist on disk.  If the missing objfile can be found then it
+   does not have to be placed at the location FILENAME.
+
+   The returned object indicates if the file could be found or not.  */
+
+extern ext_lang_missing_file_result ext_lang_find_objfile_from_buildid
+  (program_space *pspace, const struct bfd_build_id *build_id,
+   const char *filename);
+
 #if GDB_SELF_TEST
 namespace selftests {
 extern void (*hook_set_active_ext_lang) ();
diff --git a/gdb/gdbcore.h b/gdb/gdbcore.h
--- a/gdb/gdbcore.h
+++ b/gdb/gdbcore.h
@@ -196,4 +196,70 @@ class thread_section_name
   std::string m_storage;
 };
 
+/* Type returned from core_target_find_mapped_file.  Holds information
+   about a mapped file that was processed when a core file was initially
+   loaded.  */
+struct core_target_mapped_file_info
+{
+  /* Constructor.  BUILD_ID is not nullptr, and is the build-id for the
+     mapped file.  FILENAME is the location of the file that GDB loaded to
+     provide the mapped file.  This might be different from the name of the
+     mapped file mentioned in the core file, e.g. if GDB downloads a file
+     from debuginfod then FILENAME would point into the debuginfod client
+     cache.  The FILENAME can be the empty string if GDB was unable to find
+     a file to provide the mapped file.  */
+
+  core_target_mapped_file_info (const bfd_build_id *build_id,
+				const std::string filename)
+    : m_build_id (build_id),
+      m_filename (filename)
+  {
+    gdb_assert (m_build_id != nullptr);
+  }
+
+  /* The build-id for this mapped file.  */
+
+  const bfd_build_id *
+  build_id () const
+  {
+    return m_build_id;
+  }
+
+  /* The file GDB used to provide this mapped file.  */
+
+  const std::string &
+  filename () const
+  {
+    return m_filename;
+  }
+
+private:
+  const bfd_build_id *m_build_id = nullptr;
+  const std::string m_filename;
+};
+
+/* If the current inferior has a core_target for its process target, then
+   lookup information about a mapped file that was discovered when the
+   core file was loaded.
+
+   The FILENAME is the file we're looking for.  The ADDR, if provided, is a
+   mapped address within the inferior which is known to be part of the file
+   we are looking for.
+
+   As an example, when loading shared libraries this function can be
+   called, in that case FILENAME will be the name of the shared library
+   that GDB is trying to load and ADDR will be an inferior address which is
+   part of the shared library we are looking for.
+
+   This function looks for a mapped file which matches FILENAME and/or
+   which covers ADDR and returns information about that file.
+
+   The returned information includes the name of the mapped file if known
+   and the build-id for the mapped file if known.
+
+   */
+std::optional<core_target_mapped_file_info>
+core_target_find_mapped_file (const char *filename,
+			      std::optional<CORE_ADDR> addr);
+
 #endif /* !defined (GDBCORE_H) */
diff --git a/gdb/inf-child.c b/gdb/inf-child.c
--- a/gdb/inf-child.c
+++ b/gdb/inf-child.c
@@ -320,6 +320,21 @@ inf_child_target::fileio_fstat (int fd, struct stat *sb, fileio_error *target_er
   return ret;
 }
 
+/* Implementation of to_fileio_stat.  */
+
+int
+inf_child_target::fileio_stat (struct inferior *inf, const char *filename,
+			       struct stat *sb, fileio_error *target_errno)
+{
+  int ret;
+
+  ret = lstat (filename, sb);
+  if (ret == -1)
+    *target_errno = host_to_fileio_error (errno);
+
+  return ret;
+}
+
 /* Implementation of to_fileio_close.  */
 
 int
diff --git a/gdb/inf-child.h b/gdb/inf-child.h
--- a/gdb/inf-child.h
+++ b/gdb/inf-child.h
@@ -81,6 +81,8 @@ class inf_child_target
   int fileio_pread (int fd, gdb_byte *read_buf, int len,
 		    ULONGEST offset, fileio_error *target_errno) override;
   int fileio_fstat (int fd, struct stat *sb, fileio_error *target_errno) override;
+  int fileio_stat (struct inferior *inf, const char *filename,
+		   struct stat *sb, fileio_error *target_errno) override;
   int fileio_close (int fd, fileio_error *target_errno) override;
   int fileio_unlink (struct inferior *inf,
 		     const char *filename,
diff --git a/gdb/python/lib/gdb/__init__.py b/gdb/python/lib/gdb/__init__.py
--- a/gdb/python/lib/gdb/__init__.py
+++ b/gdb/python/lib/gdb/__init__.py
@@ -87,8 +87,9 @@ xmethods = []
 frame_filters = {}
 # Initial frame unwinders.
 frame_unwinders = []
-# Initial missing debug handlers.
-missing_debug_handlers = []
+# The missing file handlers.  Each item is a tuple with the form
+# (TYPE, HANDLER) where TYPE is a string either 'debug' or 'objfile'.
+missing_file_handlers = []
 
 
 def _execute_unwinders(pending_frame):
@@ -271,6 +272,61 @@ class Thread(threading.Thread):
             super().start()
 
 
+def _filter_missing_file_handlers(handlers, handler_type):
+    """Each list of missing file handlers is a list of tuples, the first
+    item in the tuple is a string either 'debug' or 'objfile' to
+    indicate what type of handler it is.  The second item in the tuple
+    is the actual handler object.
+
+    This function takes HANDLER_TYPE which is a string, either 'debug'
+    or 'objfile' and HANDLERS, a list of tuples.  The function returns
+    an iterable over all of the handler objects (extracted from the
+    tuples) which match HANDLER_TYPE.
+    """
+
+    return map(lambda t: t[1], filter(lambda t: t[0] == handler_type, handlers))
+
+
+def _handle_missing_files(pspace, handler_type, cb):
+    """Helper for _handle_missing_debuginfo and _handle_missing_objfile.
+
+    Arguments:
+        pspace: The gdb.Progspace in which we're operating.  Used to
+            lookup program space specific handlers.
+        handler_type: A string, either 'debug' or 'objfile', this is the
+            type of handler we're looking for.
+        cb: A callback which takes a handler and returns the result of
+            calling the handler.
+
+    Returns:
+        None: No suitable file could be found.
+        False: A handler has decided that the requested file cannot be
+                found, and no further searching should be done.
+        True: The file has been found and installed in a location
+                where GDB would normally look for it.  GDB should
+                repeat its lookup process, the file should now be in
+                place.
+        A string: This is the filename of where the missing file can
+                be found.
+    """
+
+    for handler in _filter_missing_file_handlers(
+        pspace.missing_file_handlers, handler_type
+    ):
+        if handler.enabled:
+            result = cb(handler)
+            if result is not None:
+                return result
+
+    for handler in _filter_missing_file_handlers(missing_file_handlers, handler_type):
+        if handler.enabled:
+            result = cb(handler)
+            if result is not None:
+                return result
+
+    return None
+
+
 def _handle_missing_debuginfo(objfile):
     """Internal function called from GDB to execute missing debug
     handlers.
@@ -293,18 +349,46 @@ def _handle_missing_debuginfo(objfile):
         A string: This is the filename of a file containing the
                   required debug information.
     """
+
     pspace = objfile.progspace
 
-    for handler in pspace.missing_debug_handlers:
-        if handler.enabled:
-            result = handler(objfile)
-            if result is not None:
-                return result
+    return _handle_missing_files(pspace, "debug", lambda h: h(objfile))
 
-    for handler in missing_debug_handlers:
-        if handler.enabled:
-            result = handler(objfile)
-            if result is not None:
-                return result
 
-    return None
+def _handle_missing_objfile(pspace, buildid, filename):
+    """Internal function called from GDB to execute missing objfile
+    handlers.
+
+    Run each of the currently registered, and enabled missing objfile
+    handler objects for the gdb.Progspace passed in as an argument,
+    and then from the global list.  Stop after the first handler that
+    returns a result other than None.
+
+    Arguments:
+        pspace: A gdb.Progspace for which the missing objfile handlers
+                should be run.  This is the program space in which an
+                objfile was found to be missing.
+        buildid: A string containing the build-id we're looking for.
+        filename: The filename of the file GDB tried to find but
+                  couldn't.  This is not where the file should be
+                  placed if found, in fact, this file might already
+                  exist on disk but have the wrong build-id.  This is
+                  mostly provided in order to be used in messages to
+                  the user.
+
+    Returns:
+        None: No objfile could be found for this build-id.
+        False: A handler has done all it can with for this build-id,
+               but no objfile could be found.
+        True: An objfile might have been installed by a handler, GDB
+              should check again.  The only place GDB checks is within
+              the .build-id sub-directory within the
+              debug-file-directory.  If the required file was not
+              installed there then GDB will not find it.
+        A string: This is the filename of a file containing the
+                  missing objfile.
+    """
+
+    return _handle_missing_files(
+        pspace, "objfile", lambda h: h(pspace, buildid, filename)
+    )
diff --git a/gdb/python/lib/gdb/command/missing_debug.py b/gdb/python/lib/gdb/command/missing_files.py
similarity index 54%
rename from gdb/python/lib/gdb/command/missing_debug.py
rename to gdb/python/lib/gdb/command/missing_files.py
--- a/gdb/python/lib/gdb/command/missing_debug.py
+++ b/gdb/python/lib/gdb/command/missing_files.py
@@ -1,4 +1,4 @@
-# Missing debug related commands.
+# Missing debug and objfile related commands.
 #
 # Copyright 2023-2024 Free Software Foundation, Inc.
 #
@@ -21,7 +21,7 @@ import gdb
 
 
 def validate_regexp(exp, idstring):
-    """Compile exp into a compiler regular expression object.
+    """Compile exp into a compiled regular expression object.
 
     Arguments:
         exp: The string to compile into a re.Pattern object.
@@ -33,14 +33,15 @@ def validate_regexp(exp, idstring):
     Raises:
         SyntaxError: If exp is an invalid regexp.
     """
+
     try:
         return re.compile(exp)
     except SyntaxError:
         raise SyntaxError("Invalid %s regexp: %s." % (idstring, exp))
 
 
-def parse_missing_debug_command_args(arg):
-    """Internal utility to parse missing debug handler command argv.
+def parse_missing_file_command_args(arg):
+    """Internal utility to parse missing file handler command argv.
 
     Arguments:
         arg: The arguments to the command. The format is:
@@ -52,6 +53,7 @@ def parse_missing_debug_command_args(arg):
     Raises:
         SyntaxError: an error processing ARG
     """
+
     argv = gdb.string_to_argv(arg)
     argc = len(argv)
     if argc > 2:
@@ -68,10 +70,10 @@ def parse_missing_debug_command_args(arg):
     )
 
 
-class InfoMissingDebugHanders(gdb.Command):
-    """GDB command to list missing debug handlers.
+class InfoMissingFileHandlers(gdb.Command):
+    """GDB command to list missing HTYPE handlers.
 
-    Usage: info missing-debug-handlers [LOCUS-REGEXP [NAME-REGEXP]]
+    Usage: info missing-HTYPE-handlers [LOCUS-REGEXP [NAME-REGEXP]]
 
     LOCUS-REGEXP is a regular expression matching the location of the
     handler.  If it is omitted, all registered handlers from all
@@ -79,38 +81,47 @@ class InfoMissingDebugHanders(gdb.Command):
     the handlers from the current progspace, or a regular expression
     matching filenames of progspaces.
 
-    NAME-REGEXP is a regular expression to filter missing debug
+    NAME-REGEXP is a regular expression to filter missing HTYPE
     handler names.  If this omitted for a specified locus, then all
     registered handlers in the locus are listed.
     """
 
-    def __init__(self):
-        super().__init__("info missing-debug-handlers", gdb.COMMAND_FILES)
+    def __init__(self, handler_type):
+        # Update the doc string before calling the parent constructor,
+        # replacing the string 'HTYPE' with the value of HANDLER_TYPE.
+        # The parent constructor will grab a copy of this string to
+        # use as the commands help text.
+        self.__doc__ = self.__doc__.replace("HTYPE", handler_type)
+        super().__init__(
+            "info missing-" + handler_type + "-handlers", gdb.COMMAND_FILES
+        )
+        self.handler_type = handler_type
 
     def list_handlers(self, title, handlers, name_re):
-        """Lists the missing debug handlers whose name matches regexp.
+        """Lists the missing file handlers whose name matches regexp.
 
         Arguments:
             title: The line to print before the list.
-            handlers: The list of the missing debug handlers.
+            handlers: The list of the missing file handlers.
             name_re: handler name filter.
         """
+
         if not handlers:
             return
         print(title)
-        for handler in handlers:
+        for handler in gdb._filter_missing_file_handlers(handlers, self.handler_type):
             if name_re.match(handler.name):
                 print(
                     "  %s%s" % (handler.name, "" if handler.enabled else " [disabled]")
                 )
 
     def invoke(self, arg, from_tty):
-        locus_re, name_re = parse_missing_debug_command_args(arg)
+        locus_re, name_re = parse_missing_file_command_args(arg)
 
         if locus_re.match("progspace") and locus_re.pattern != "":
             cp = gdb.current_progspace()
             self.list_handlers(
-                "Progspace %s:" % cp.filename, cp.missing_debug_handlers, name_re
+                "Progspace %s:" % cp.filename, cp.missing_file_handlers, name_re
             )
 
         for progspace in gdb.progspaces():
@@ -125,58 +136,71 @@ class InfoMissingDebugHanders(gdb.Command):
                     msg = "Progspace %s:" % filename
                 self.list_handlers(
                     msg,
-                    progspace.missing_debug_handlers,
+                    progspace.missing_file_handlers,
                     name_re,
                 )
 
         # Print global handlers last, as these are invoked last.
         if locus_re.match("global"):
-            self.list_handlers("Global:", gdb.missing_debug_handlers, name_re)
+            self.list_handlers("Global:", gdb.missing_file_handlers, name_re)
 
 
-def do_enable_handler1(handlers, name_re, flag):
-    """Enable/disable missing debug handlers whose names match given regex.
+def do_enable_handler1(handlers, name_re, flag, handler_type):
+    """Enable/disable missing file handlers whose names match given regex.
 
     Arguments:
-        handlers: The list of missing debug handlers.
+        handlers: The list of missing file handlers.
         name_re: Handler name filter.
         flag: A boolean indicating if we should enable or disable.
+        handler_type: A string, either 'debug' or 'objfile', use to control
+            which handlers are modified.
 
     Returns:
         The number of handlers affected.
     """
+
     total = 0
-    for handler in handlers:
+    for handler in gdb._filter_missing_file_handlers(handlers, handler_type):
         if name_re.match(handler.name) and handler.enabled != flag:
             handler.enabled = flag
             total += 1
     return total
 
 
-def do_enable_handler(arg, flag):
-    """Enable or disable missing debug handlers."""
-    (locus_re, name_re) = parse_missing_debug_command_args(arg)
+def do_enable_handler(arg, flag, handler_type):
+    """Enable or disable missing file handlers."""
+
+    (locus_re, name_re) = parse_missing_file_command_args(arg)
     total = 0
     if locus_re.match("global"):
-        total += do_enable_handler1(gdb.missing_debug_handlers, name_re, flag)
+        total += do_enable_handler1(
+            gdb.missing_file_handlers, name_re, flag, handler_type
+        )
     if locus_re.match("progspace") and locus_re.pattern != "":
         total += do_enable_handler1(
-            gdb.current_progspace().missing_debug_handlers, name_re, flag
+            gdb.current_progspace().missing_file_handlers, name_re, flag, handler_type
         )
     for progspace in gdb.progspaces():
         filename = progspace.filename or ""
         if locus_re.match(filename):
-            total += do_enable_handler1(progspace.missing_debug_handlers, name_re, flag)
+            total += do_enable_handler1(
+                progspace.missing_file_handlers, name_re, flag, handler_type
+            )
     print(
-        "%d missing debug handler%s %s"
-        % (total, "" if total == 1 else "s", "enabled" if flag else "disabled")
+        "%d missing %s handler%s %s"
+        % (
+            total,
+            handler_type,
+            "" if total == 1 else "s",
+            "enabled" if flag else "disabled",
+        )
     )
 
 
-class EnableMissingDebugHandler(gdb.Command):
-    """GDB command to enable missing debug handlers.
+class EnableMissingFileHandler(gdb.Command):
+    """GDB command to enable missing HTYPE handlers.
 
-    Usage: enable missing-debug-handler [LOCUS-REGEXP [NAME-REGEXP]]
+    Usage: enable missing-HTYPE-handler [LOCUS-REGEXP [NAME-REGEXP]]
 
     LOCUS-REGEXP is a regular expression specifying the handlers to
     enable.  It can be 'global', 'progspace' for the current
@@ -187,18 +211,26 @@ class EnableMissingDebugHandler(gdb.Command):
     in the locus are affected.
     """
 
-    def __init__(self):
-        super().__init__("enable missing-debug-handler", gdb.COMMAND_FILES)
+    def __init__(self, handler_type):
+        # Update the doc string before calling the parent constructor,
+        # replacing the string 'HTYPE' with the value of HANDLER_TYPE.
+        # The parent constructor will grab a copy of this string to
+        # use as the commands help text.
+        self.__doc__ = self.__doc__.replace("HTYPE", handler_type)
+        super().__init__(
+            "enable missing-" + handler_type + "-handler", gdb.COMMAND_FILES
+        )
+        self.handler_type = handler_type
 
     def invoke(self, arg, from_tty):
         """GDB calls this to perform the command."""
-        do_enable_handler(arg, True)
+        do_enable_handler(arg, True, self.handler_type)
 
 
-class DisableMissingDebugHandler(gdb.Command):
-    """GDB command to disable missing debug handlers.
+class DisableMissingFileHandler(gdb.Command):
+    """GDB command to disable missing HTYPE handlers.
 
-    Usage: disable missing-debug-handler [LOCUS-REGEXP [NAME-REGEXP]]
+    Usage: disable missing-HTYPE-handler [LOCUS-REGEXP [NAME-REGEXP]]
 
     LOCUS-REGEXP is a regular expression specifying the handlers to
     enable.  It can be 'global', 'progspace' for the current
@@ -209,19 +241,28 @@ class DisableMissingDebugHandler(gdb.Command):
     in the locus are affected.
     """
 
-    def __init__(self):
-        super().__init__("disable missing-debug-handler", gdb.COMMAND_FILES)
+    def __init__(self, handler_type):
+        # Update the doc string before calling the parent constructor,
+        # replacing the string 'HTYPE' with the value of HANDLER_TYPE.
+        # The parent constructor will grab a copy of this string to
+        # use as the commands help text.
+        self.__doc__ = self.__doc__.replace("HTYPE", handler_type)
+        super().__init__(
+            "disable missing-" + handler_type + "-handler", gdb.COMMAND_FILES
+        )
+        self.handler_type = handler_type
 
     def invoke(self, arg, from_tty):
         """GDB calls this to perform the command."""
-        do_enable_handler(arg, False)
+        do_enable_handler(arg, False, self.handler_type)
 
 
-def register_missing_debug_handler_commands():
-    """Installs the missing debug handler commands."""
-    InfoMissingDebugHanders()
-    EnableMissingDebugHandler()
-    DisableMissingDebugHandler()
+def register_missing_file_handler_commands():
+    """Installs the missing file handler commands."""
+    for handler_type in ["debug", "objfile"]:
+        InfoMissingFileHandlers(handler_type)
+        EnableMissingFileHandler(handler_type)
+        DisableMissingFileHandler(handler_type)
 
 
-register_missing_debug_handler_commands()
+register_missing_file_handler_commands()
diff --git a/gdb/python/lib/gdb/missing_debug.py b/gdb/python/lib/gdb/missing_debug.py
--- a/gdb/python/lib/gdb/missing_debug.py
+++ b/gdb/python/lib/gdb/missing_debug.py
@@ -17,48 +17,11 @@
 MissingDebugHandler base class, and register_handler function.
 """
 
-import sys
-
 import gdb
+from gdb.missing_files import MissingFileHandler
 
-if sys.version_info >= (3, 7):
-    # Functions str.isascii() and str.isalnum are available starting Python
-    # 3.7.
-    def isascii(ch):
-        return ch.isascii()
-
-    def isalnum(ch):
-        return ch.isalnum()
-
-else:
-    # Fall back to curses.ascii.isascii() and curses.ascii.isalnum() for
-    # earlier versions.
-    from curses.ascii import isalnum, isascii
-
-
-def _validate_name(name):
-    """Validate a missing debug handler name string.
-
-    If name is valid as a missing debug handler name, then this
-    function does nothing.  If name is not valid then an exception is
-    raised.
-
-    Arguments:
-        name: A string, the name of a missing debug handler.
-
-    Returns:
-        Nothing.
 
-    Raises:
-        ValueError: If name is invalid as a missing debug handler
-                    name.
-    """
-    for ch in name:
-        if not isascii(ch) or not (isalnum(ch) or ch in "_-"):
-            raise ValueError("invalid character '%s' in handler name: %s" % (ch, name))
-
-
-class MissingDebugHandler(object):
+class MissingDebugHandler(MissingFileHandler):
     """Base class for missing debug handlers written in Python.
 
     A missing debug handler has a single method __call__ along with
@@ -69,41 +32,8 @@ class MissingDebugHandler(object):
         enabled: When true this handler is enabled.
     """
 
-    def __init__(self, name):
-        """Constructor.
-
-        Args:
-            name: An identifying name for this handler.
-
-        Raises:
-            TypeError: name is not a string.
-            ValueError: name contains invalid characters.
-        """
-
-        if not isinstance(name, str):
-            raise TypeError("incorrect type for name: %s" % type(name))
-
-        _validate_name(name)
-
-        self._name = name
-        self._enabled = True
-
-    @property
-    def name(self):
-        return self._name
-
-    @property
-    def enabled(self):
-        return self._enabled
-
-    @enabled.setter
-    def enabled(self, value):
-        if not isinstance(value, bool):
-            raise TypeError("incorrect type for enabled attribute: %s" % type(value))
-        self._enabled = value
-
     def __call__(self, objfile):
-        """GDB handle missing debug information for an objfile.
+        """Handle missing debug information for an objfile.
 
         Arguments:
             objfile: A gdb.Objfile for which GDB could not find any
@@ -124,62 +54,5 @@ class MissingDebugHandler(object):
 
 
 def register_handler(locus, handler, replace=False):
-    """Register handler in given locus.
-
-    The handler is prepended to the locus's missing debug handlers
-    list. The name of handler should be unique (or replace must be
-    True).
-
-    Arguments:
-        locus: Either a progspace, or None (in which case the unwinder
-               is registered globally).
-        handler: An object of a gdb.MissingDebugHandler subclass.
-
-        replace: If True, replaces existing handler with the same name
-                 within locus.  Otherwise, raises RuntimeException if
-                 unwinder with the same name already exists.
-
-    Returns:
-        Nothing.
-
-    Raises:
-        RuntimeError: The name of handler is not unique.
-        TypeError: Bad locus type.
-        AttributeError: Required attributes of handler are missing.
-    """
-
-    if locus is None:
-        if gdb.parameter("verbose"):
-            gdb.write("Registering global %s handler ...\n" % handler.name)
-        locus = gdb
-    elif isinstance(locus, gdb.Progspace):
-        if gdb.parameter("verbose"):
-            gdb.write(
-                "Registering %s handler for %s ...\n" % (handler.name, locus.filename)
-            )
-    else:
-        raise TypeError("locus should be gdb.Progspace or None")
-
-    # Some sanity checks on HANDLER.  Calling getattr will raise an
-    # exception if the attribute doesn't exist, which is what we want.
-    # These checks are not exhaustive; we don't check the attributes
-    # have the correct types, or the method has the correct signature,
-    # but this should catch some basic mistakes.
-    getattr(handler, "name")
-    getattr(handler, "enabled")
-    call_method = getattr(handler, "__call__")
-    if not callable(call_method):
-        raise AttributeError(
-            "'%s' object's '__call__' attribute is not callable"
-            % type(handler).__name__
-        )
-
-    i = 0
-    for needle in locus.missing_debug_handlers:
-        if needle.name == handler.name:
-            if replace:
-                del locus.missing_debug_handlers[i]
-            else:
-                raise RuntimeError("Handler %s already exists." % handler.name)
-        i += 1
-    locus.missing_debug_handlers.insert(0, handler)
+    """See gdb.missing_files.register_handler."""
+    gdb.missing_files.register_handler("debug", locus, handler, replace)
diff --git a/gdb/python/lib/gdb/missing_files.py b/gdb/python/lib/gdb/missing_files.py
new file mode 100644
--- /dev/null
+++ b/gdb/python/lib/gdb/missing_files.py
@@ -0,0 +1,204 @@
+# Copyright (C) 2023-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/>.
+
+"""
+MissingFileHandler base class, and support functions used by the
+missing_debug.py and missing_objfile.py modules.
+"""
+
+import sys
+
+import gdb
+
+if sys.version_info >= (3, 7):
+    # Functions str.isascii() and str.isalnum are available starting Python
+    # 3.7.
+    def isascii(ch):
+        return ch.isascii()
+
+    def isalnum(ch):
+        return ch.isalnum()
+
+else:
+    # Older version of Python doesn't have str.isascii() and
+    # str.isalnum() so provide our own.
+    #
+    # We could import isalnum() and isascii() from the curses library,
+    # but that adds an extra dependency.  Given these functions are
+    # both small and trivial lets implement them here.
+    #
+    # These definitions are based on those in the curses library, but
+    # simplified as we know C will always be a single character 'str'.
+
+    def isdigit(c):
+        return 48 <= ord(c) <= 57
+
+    def islower(c):
+        return 97 <= ord(c) <= 122
+
+    def isupper(c):
+        return 65 <= ord(c) <= 90
+
+    def isalpha(c):
+        return isupper(c) or islower(c)
+
+    def isalnum(c):
+        return isalpha(c) or isdigit(c)
+
+    def isascii(c):
+        return 0 <= ord(c) <= 127
+
+
+def _validate_name(name):
+    """Validate a missing file handler name string.
+
+    If name is valid as a missing file handler name, then this
+    function does nothing.  If name is not valid then an exception is
+    raised.
+
+    Arguments:
+        name: A string, the name of a missing file handler.
+
+    Returns:
+        Nothing.
+
+    Raises:
+        ValueError: If name is invalid as a missing file handler
+                    name.
+    """
+
+    for ch in name:
+        if not isascii(ch) or not (isalnum(ch) or ch in "_-"):
+            raise ValueError("invalid character '%s' in handler name: %s" % (ch, name))
+
+
+class MissingFileHandler(object):
+    """Base class for missing file handlers written in Python.
+
+    A missing file handler has a single method __call__ along with the
+    read/write attribute enabled, and a read-only attribute name.  The
+    attributes are provided by this class while the __call__ method is
+    provided by a sub-class.  Each sub-classes __call__ method will
+    have a different signature.
+
+    Attributes:
+        name: Read-only attribute, the name of this handler.
+        enabled: When true this handler is enabled.
+    """
+
+    def __init__(self, name):
+        """Constructor.
+
+        Args:
+            name: An identifying name for this handler.
+
+        Raises:
+            TypeError: name is not a string.
+            ValueError: name contains invalid characters.
+        """
+
+        if not isinstance(name, str):
+            raise TypeError("incorrect type for name: %s" % type(name))
+
+        _validate_name(name)
+
+        self._name = name
+        self._enabled = True
+
+    @property
+    def name(self):
+        return self._name
+
+    @property
+    def enabled(self):
+        return self._enabled
+
+    @enabled.setter
+    def enabled(self, value):
+        if not isinstance(value, bool):
+            raise TypeError("incorrect type for enabled attribute: %s" % type(value))
+        self._enabled = value
+
+
+def register_handler(handler_type, locus, handler, replace=False):
+    """Register handler in given locus.
+
+    The handler is prepended to the locus's missing file handlers
+    list. The name of handler should be unique (or replace must be
+    True), and the name must pass the _validate_name check.
+
+    Arguments:
+        handler_type: A string, either 'debug' or 'objfile' indicating the
+            type of handler to be registered.
+        locus: Either a progspace, or None (in which case the unwinder
+               is registered globally).
+        handler: An object used as a missing file handler.  Usually a
+            sub-class of MissingFileHandler.
+        replace: If True, replaces existing handler with the same name
+                 within locus.  Otherwise, raises RuntimeException if
+                 unwinder with the same name already exists.
+
+    Returns:
+        Nothing.
+
+    Raises:
+        RuntimeError: The name of handler is not unique.
+        TypeError: Bad locus type.
+        AttributeError: Required attributes of handler are missing.
+        ValueError: If the name of the handler is invalid, or if
+            handler_type is neither 'debug' or 'objfile'.
+    """
+
+    if handler_type != "debug" and handler_type != "objfile":
+        raise ValueError("handler_type must be 'debug' or 'objfile'")
+
+    if locus is None:
+        if gdb.parameter("verbose"):
+            gdb.write("Registering global %s handler ...\n" % handler.name)
+        locus = gdb
+    elif isinstance(locus, gdb.Progspace):
+        if gdb.parameter("verbose"):
+            gdb.write(
+                "Registering %s handler for %s ...\n" % (handler.name, locus.filename)
+            )
+    else:
+        raise TypeError("locus should be gdb.Progspace or None")
+
+    # Some sanity checks on HANDLER.  Calling getattr will raise an
+    # exception if the attribute doesn't exist, which is what we want.
+    # These checks are not exhaustive; we don't check the attributes
+    # have the correct types, or the method has the correct signature,
+    # but this should catch some basic mistakes.
+    name = getattr(handler, "name")
+    _validate_name(name)
+
+    getattr(handler, "enabled")
+
+    call_method = getattr(handler, "__call__")
+    if not callable(call_method):
+        raise AttributeError(
+            "'%s' object's '__call__' attribute is not callable"
+            % type(handler).__name__
+        )
+
+    i = 0
+    for needle in locus.missing_file_handlers:
+        if needle[0] == handler_type and needle[1].name == handler.name:
+            if replace:
+                del locus.missing_file_handlers[i]
+            else:
+                raise RuntimeError("Handler %s already exists." % handler.name)
+        i += 1
+    locus.missing_file_handlers.insert(0, (handler_type, handler))
diff --git a/gdb/python/lib/gdb/missing_objfile.py b/gdb/python/lib/gdb/missing_objfile.py
new file mode 100644
--- /dev/null
+++ b/gdb/python/lib/gdb/missing_objfile.py
@@ -0,0 +1,67 @@
+# Copyright (C) 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/>.
+
+"""
+MissingObjfileHandler base class, and register_handler function.
+"""
+
+import gdb
+from gdb.missing_files import MissingFileHandler
+
+
+class MissingObjfileHandler(MissingFileHandler):
+    """Base class for missing objfile handlers written in Python.
+
+    A missing objfile handler has a single method __call__ along with
+    the read/write attribute enabled, and a read-only attribute name.
+
+    Attributes:
+        name: Read-only attribute, the name of this handler.
+        enabled: When true this handler is enabled.
+    """
+
+    def __call__(self, buildid, filename):
+        """Handle a missing objfile when GDB can knows the build-id.
+
+        Arguments:
+
+            buildid: A string containing the build-id for the objfile
+                GDB is searching for.
+            filename: A string containing the name of the file GDB is
+                searching for.  This is provided only for the purpose
+                of creating diagnostic messages.  If the file is found
+                it does not have to be placed here, and this file
+                might already exist but GDB has determined it is not
+                suitable for use, e.g. if the build-id doesn't match.
+
+        Returns:
+
+            True: GDB should try again to locate the missing objfile,
+                the handler may have installed the missing file.
+            False: GDB should move on without the objfile.  The
+                handler has determined that this objfile is not
+                available.
+            A string: GDB should load the file at the given path; it
+                contains the requested objfile.
+            None: This handler can't help with this objfile.  GDB
+                should try any other registered handlers.
+
+        """
+        raise NotImplementedError("MissingObjfileHandler.__call__()")
+
+
+def register_handler(locus, handler, replace=False):
+    """See gdb.missing_files.register_handler."""
+    gdb.missing_files.register_handler("objfile", locus, handler, replace)
diff --git a/gdb/python/py-progspace.c b/gdb/python/py-progspace.c
--- a/gdb/python/py-progspace.c
+++ b/gdb/python/py-progspace.c
@@ -55,8 +55,8 @@ struct pspace_object
   /* The debug method list.  */
   PyObject *xmethods;
 
-  /* The missing debug handler list.  */
-  PyObject *missing_debug_handlers;
+  /* The missing file handler list.  */
+  PyObject *missing_file_handlers;
 };
 
 extern PyTypeObject pspace_object_type
@@ -166,7 +166,7 @@ pspy_dealloc (PyObject *self)
   Py_XDECREF (ps_self->frame_unwinders);
   Py_XDECREF (ps_self->type_printers);
   Py_XDECREF (ps_self->xmethods);
-  Py_XDECREF (ps_self->missing_debug_handlers);
+  Py_XDECREF (ps_self->missing_file_handlers);
   Py_TYPE (self)->tp_free (self);
 }
 
@@ -202,8 +202,8 @@ pspy_initialize (pspace_object *self)
   if (self->xmethods == NULL)
     return 0;
 
-  self->missing_debug_handlers = PyList_New (0);
-  if (self->missing_debug_handlers == nullptr)
+  self->missing_file_handlers = PyList_New (0);
+  if (self->missing_file_handlers == nullptr)
     return 0;
 
   return 1;
@@ -349,18 +349,18 @@ pspy_get_xmethods (PyObject *o, void *ignore)
 /* Return the list of missing debug handlers for this program space.  */
 
 static PyObject *
-pspy_get_missing_debug_handlers (PyObject *o, void *ignore)
+pspy_get_missing_file_handlers (PyObject *o, void *ignore)
 {
   pspace_object *self = (pspace_object *) o;
 
-  Py_INCREF (self->missing_debug_handlers);
-  return self->missing_debug_handlers;
+  Py_INCREF (self->missing_file_handlers);
+  return self->missing_file_handlers;
 }
 
 /* Set this program space's list of missing debug handlers to HANDLERS.  */
 
 static int
-pspy_set_missing_debug_handlers (PyObject *o, PyObject *handlers,
+pspy_set_missing_file_handlers (PyObject *o, PyObject *handlers,
 				 void *ignore)
 {
   pspace_object *self = (pspace_object *) o;
@@ -380,9 +380,9 @@ pspy_set_missing_debug_handlers (PyObject *o, PyObject *handlers,
     }
 
   /* Take care in case the LHS and RHS are related somehow.  */
-  gdbpy_ref<> tmp (self->missing_debug_handlers);
+  gdbpy_ref<> tmp (self->missing_file_handlers);
   Py_INCREF (handlers);
-  self->missing_debug_handlers = handlers;
+  self->missing_file_handlers = handlers;
 
   return 0;
 }
@@ -779,8 +779,8 @@ static gdb_PyGetSetDef pspace_getset[] =
     "Type printers.", NULL },
   { "xmethods", pspy_get_xmethods, NULL,
     "Debug methods.", NULL },
-  { "missing_debug_handlers", pspy_get_missing_debug_handlers,
-    pspy_set_missing_debug_handlers, "Missing debug handlers.", NULL },
+  { "missing_file_handlers", pspy_get_missing_file_handlers,
+    pspy_set_missing_file_handlers, "Missing file handlers.", NULL },
   { NULL }
 };
 
diff --git a/gdb/python/python.c b/gdb/python/python.c
--- a/gdb/python/python.c
+++ b/gdb/python/python.c
@@ -35,6 +35,7 @@
 #include "location.h"
 #include "run-on-main-thread.h"
 #include "observable.h"
+#include "build-id.h"
 
 #if GDB_SELF_TEST
 #include "gdbsupport/selftest.h"
@@ -128,8 +129,11 @@ static std::optional<std::string> gdbpy_colorize
   (const std::string &filename, const std::string &contents);
 static std::optional<std::string> gdbpy_colorize_disasm
 (const std::string &content, gdbarch *gdbarch);
-static ext_lang_missing_debuginfo_result gdbpy_handle_missing_debuginfo
+static ext_lang_missing_file_result gdbpy_handle_missing_debuginfo
   (const struct extension_language_defn *extlang, struct objfile *objfile);
+static ext_lang_missing_file_result gdbpy_find_objfile_from_buildid
+  (const struct extension_language_defn *extlang, program_space *pspace,
+   const struct bfd_build_id *build_id, const char *missing_filename);
 
 /* The interface between gdb proper and loading of python scripts.  */
 
@@ -177,7 +181,8 @@ static const struct extension_language_ops python_extension_ops =
 
   gdbpy_print_insn,
 
-  gdbpy_handle_missing_debuginfo
+  gdbpy_handle_missing_debuginfo,
+  gdbpy_find_objfile_from_buildid
 };
 
 #endif /* HAVE_PYTHON */
@@ -1761,10 +1766,10 @@ gdbpy_get_current_objfile (PyObject *unused1, PyObject *unused2)
 /* Implement the 'handle_missing_debuginfo' hook for Python.  GDB has
    failed to find any debug information for OBJFILE.  The extension has a
    chance to record this, or even install the required debug information.
-   See the description of ext_lang_missing_debuginfo_result in
-   extension-priv.h for details of the return value.  */
+   See the description of ext_lang_missing_file_result in extension-priv.h
+   for details of the return value.  */
 
-static ext_lang_missing_debuginfo_result
+static ext_lang_missing_file_result
 gdbpy_handle_missing_debuginfo (const struct extension_language_defn *extlang,
 				struct objfile *objfile)
 {
@@ -1812,7 +1817,7 @@ gdbpy_handle_missing_debuginfo (const struct extension_language_defn *extlang,
   if (PyBool_Check (pyo_execute_ret.get ()))
     {
       bool try_again = PyObject_IsTrue (pyo_execute_ret.get ());
-      return ext_lang_missing_debuginfo_result (try_again);
+      return ext_lang_missing_file_result (try_again);
     }
 
   if (!gdbpy_is_string (pyo_execute_ret.get ()))
@@ -1832,7 +1837,108 @@ gdbpy_handle_missing_debuginfo (const struct extension_language_defn *extlang,
       return {};
     }
 
-  return ext_lang_missing_debuginfo_result (std::string (filename.get ()));
+  return ext_lang_missing_file_result (std::string (filename.get ()));
+}
+
+/* Implement the find_objfile_from_buildid hook for Python.  PSPACE is the
+   program space in which GDB is trying to find an objfile, BUILD_ID is the
+   build-id for the missing objfile, and EXPECTED_FILENAME is a non-NULL
+   string which can be used (if needed) in messages to the user, and
+   represents the file GDB is looking for.  */
+
+static ext_lang_missing_file_result
+gdbpy_find_objfile_from_buildid (const struct extension_language_defn *extlang,
+				 program_space *pspace,
+				 const struct bfd_build_id *build_id,
+				 const char *missing_filename)
+{
+  gdb_assert (pspace != nullptr);
+  gdb_assert (build_id != nullptr);
+  gdb_assert (missing_filename != nullptr);
+
+  /* Early exit if Python is not initialised.  */
+  if (!gdb_python_initialized || gdb_python_module == nullptr)
+    return {};
+
+  gdbpy_enter enter_py;
+
+  /* Convert BUILD_ID into a Python object.  */
+  std::string hex_form = bin2hex (build_id->data, build_id->size);
+  gdbpy_ref<> pyo_buildid = host_string_to_python_string (hex_form.c_str ());
+  if (pyo_buildid == nullptr)
+    {
+      gdbpy_print_stack ();
+      return {};
+    }
+
+  /* Convert MISSING_FILENAME to a Python object.  */
+  gdbpy_ref<> pyo_filename = host_string_to_python_string (missing_filename);
+  if (pyo_filename == nullptr)
+    {
+      gdbpy_print_stack ();
+      return {};
+    }
+
+  /* Convert PSPACE to a Python object.  */
+  gdbpy_ref<> pyo_pspace = pspace_to_pspace_object (pspace);
+  if (pyo_pspace == nullptr)
+    {
+      gdbpy_print_stack ();
+      return {};
+    }
+
+  /* Lookup the helper function within the GDB module.  */
+  gdbpy_ref<> pyo_handler
+    (PyObject_GetAttrString (gdb_python_module, "_handle_missing_objfile"));
+  if (pyo_handler == nullptr)
+    {
+      gdbpy_print_stack ();
+      return {};
+    }
+
+  /* Call the function, passing in the Python objfile object.  */
+  gdbpy_ref<> pyo_execute_ret
+    (PyObject_CallFunctionObjArgs (pyo_handler.get (), pyo_pspace.get (),
+				   pyo_buildid.get (), pyo_filename.get (),
+				   nullptr));
+  if (pyo_execute_ret == nullptr)
+    {
+      /* If the handler is cancelled due to a Ctrl-C, then propagate
+	 the Ctrl-C as a GDB exception instead of swallowing it.  */
+      gdbpy_print_stack_or_quit ();
+      return {};
+    }
+
+  /* Parse the result, and convert it back to the C++ object.  */
+  if (pyo_execute_ret == Py_None)
+    return {};
+
+  if (PyBool_Check (pyo_execute_ret.get ()))
+    {
+      /* We know the value is a bool, so it must be either Py_True or
+	 Py_False.  Anything else would not get past the above check.  */
+      bool try_again = pyo_execute_ret.get () == Py_True;
+      return ext_lang_missing_file_result (try_again);
+    }
+
+  if (!gdbpy_is_string (pyo_execute_ret.get ()))
+    {
+      PyErr_SetString (PyExc_ValueError,
+		       "return value from _find_objfile_by_buildid should "
+		       "be None, a bool, or a str");
+      gdbpy_print_stack ();
+      return {};
+    }
+
+  gdb::unique_xmalloc_ptr<char> filename
+    = python_string_to_host_string (pyo_execute_ret.get ());
+  if (filename == nullptr)
+    {
+      gdbpy_print_stack ();
+      return {};
+    }
+
+  return ext_lang_missing_file_result (std::string (filename.get ()));
 }
 
 /* Compute the list of active python type printers and store them in
diff --git a/gdb/remote.c b/gdb/remote.c
--- a/gdb/remote.c
+++ b/gdb/remote.c
@@ -246,6 +246,7 @@ enum {
   PACKET_vFile_unlink,
   PACKET_vFile_readlink,
   PACKET_vFile_fstat,
+  PACKET_vFile_stat,
   PACKET_qXfer_auxv,
   PACKET_qXfer_features,
   PACKET_qXfer_exec_file,
@@ -996,6 +997,9 @@ class remote_target : public process_stratum_target
 
   int fileio_fstat (int fd, struct stat *sb, fileio_error *target_errno) override;
 
+  int fileio_stat (struct inferior *inf, const char *filename,
+		   struct stat *sb, fileio_error *target_errno) override;
+
   int fileio_close (int fd, fileio_error *target_errno) override;
 
   int fileio_unlink (struct inferior *inf,
@@ -12661,6 +12665,9 @@ remote_target::remote_hostio_send_command (int command_bytes, int which_packet,
       return -1;
     }
 
+  if (*remote_errno != FILEIO_SUCCESS)
+    return -1;
+
   /* Make sure we saw an attachment if and only if we expected one.  */
   if ((attachment_tmp == NULL && attachment != NULL)
       || (attachment_tmp != NULL && attachment == NULL))
@@ -13027,6 +13034,41 @@ remote_target::fileio_readlink (struct inferior *inf, const char *filename,
   return ret;
 }
 
+/* Helper function to handle ::fileio_fstat and ::fileio_stat result
+   processing.  When this function is called the remote syscall has been
+   performed and we know we didn't get an error back.
+
+   ATTACHMENT and ATTACHMENT_LEN are the attachment data extracted from the
+   remote syscall reply.  EXPECTED_LEN is the length returned from the
+   fstat or stat call, this the length of the returned data (in ATTACHMENT)
+   once it has been decoded.  The fstat/stat result (from the ATTACHMENT
+   data) is to be placed in ST.  */
+
+static int
+fileio_process_fstat_and_stat_reply (const char *attachment,
+				     int attachment_len,
+				     int expected_len,
+				     struct stat *st)
+{
+  struct fio_stat fst;
+
+  int read_len
+    = remote_unescape_input ((gdb_byte *) attachment, attachment_len,
+			     (gdb_byte *) &fst, sizeof (fst));
+
+  if (read_len != expected_len)
+    error (_("vFile:fstat returned %d, but %d bytes."),
+	   expected_len, read_len);
+
+  if (read_len != sizeof (fst))
+    error (_("vFile:fstat returned %d bytes, but expecting %d."),
+	   read_len, (int) sizeof (fst));
+
+  remote_fileio_to_host_stat (&fst, st);
+
+  return 0;
+}
+
 /* Implementation of to_fileio_fstat.  */
 
 int
@@ -13037,8 +13079,6 @@ remote_target::fileio_fstat (int fd, struct stat *st, fileio_error *remote_errno
   int left = get_remote_packet_size ();
   int attachment_len, ret;
   const char *attachment;
-  struct fio_stat fst;
-  int read_len;
 
   remote_buffer_add_string (&p, &left, "vFile:fstat:");
 
@@ -13070,19 +13110,41 @@ remote_target::fileio_fstat (int fd, struct stat *st, fileio_error *remote_errno
       return 0;
     }
 
-  read_len = remote_unescape_input ((gdb_byte *) attachment, attachment_len,
-				    (gdb_byte *) &fst, sizeof (fst));
+  return fileio_process_fstat_and_stat_reply (attachment, attachment_len,
+					      ret, st);
+}
 
-  if (read_len != ret)
-    error (_("vFile:fstat returned %d, but %d bytes."), ret, read_len);
+/* Implementation of to_fileio_stat.  */
 
-  if (read_len != sizeof (fst))
-    error (_("vFile:fstat returned %d bytes, but expecting %d."),
-	   read_len, (int) sizeof (fst));
+int
+remote_target::fileio_stat (struct inferior *inf, const char *filename,
+			    struct stat *st, fileio_error *remote_errno)
+{
+  struct remote_state *rs = get_remote_state ();
+  char *p = rs->buf.data ();
+  int left = get_remote_packet_size () - 1;
 
-  remote_fileio_to_host_stat (&fst, st);
+  if (remote_hostio_set_filesystem (inf, remote_errno) != 0)
+    return {};
 
-  return 0;
+  remote_buffer_add_string (&p, &left, "vFile:stat:");
+
+  remote_buffer_add_bytes (&p, &left, (const gdb_byte *) filename,
+			   strlen (filename));
+
+  int attachment_len;
+  const char *attachment;
+  int ret = remote_hostio_send_command (p - rs->buf.data (), PACKET_vFile_stat,
+					remote_errno, &attachment,
+					&attachment_len);
+
+  /* Unlike ::fileio_fstat, the stat fileio call was added later on, and
+     has none of the legacy bfd issues, so we can just return the error.  */
+  if (ret < 0)
+    return ret;
+
+  return fileio_process_fstat_and_stat_reply (attachment, attachment_len,
+					      ret, st);
 }
 
 /* Implementation of to_filesystem_is_local.  */
@@ -16157,6 +16219,8 @@ Show the maximum size of the address (in bits) in a memory packet."), NULL,
 
   add_packet_config_cmd (PACKET_vFile_fstat, "vFile:fstat", "hostio-fstat", 0);
 
+  add_packet_config_cmd (PACKET_vFile_stat, "vFile:stat", "hostio-stat", 0);
+
   add_packet_config_cmd (PACKET_vAttach, "vAttach", "attach", 0);
 
   add_packet_config_cmd (PACKET_vRun, "vRun", "run", 0);
diff --git a/gdb/solib-aix.c b/gdb/solib-aix.c
--- a/gdb/solib-aix.c
+++ b/gdb/solib-aix.c
@@ -689,6 +689,11 @@ const solib_ops solib_aix_so_ops =
   solib_aix_open_symbol_file_object,
   solib_aix_in_dynsym_resolve_code,
   solib_aix_bfd_open,
+  nullptr,
+  nullptr,
+  nullptr,
+  nullptr,
+  default_find_solib_addr,
 };
 
 void _initialize_solib_aix ();
diff --git a/gdb/solib-darwin.c b/gdb/solib-darwin.c
--- a/gdb/solib-darwin.c
+++ b/gdb/solib-darwin.c
@@ -665,4 +665,9 @@ const solib_ops darwin_so_ops =
   open_symbol_file_object,
   darwin_in_dynsym_resolve_code,
   darwin_bfd_open,
+  nullptr,
+  nullptr,
+  nullptr,
+  nullptr,
+  default_find_solib_addr,
 };
diff --git a/gdb/solib-dsbt.c b/gdb/solib-dsbt.c
--- a/gdb/solib-dsbt.c
+++ b/gdb/solib-dsbt.c
@@ -914,6 +914,11 @@ const solib_ops dsbt_so_ops =
   open_symbol_file_object,
   dsbt_in_dynsym_resolve_code,
   solib_bfd_open,
+  nullptr,
+  nullptr,
+  nullptr,
+  nullptr,
+  default_find_solib_addr,
 };
 
 void _initialize_dsbt_solib ();
diff --git a/gdb/solib-frv.c b/gdb/solib-frv.c
--- a/gdb/solib-frv.c
+++ b/gdb/solib-frv.c
@@ -1086,4 +1086,9 @@ const solib_ops frv_so_ops =
   open_symbol_file_object,
   frv_in_dynsym_resolve_code,
   solib_bfd_open,
+  nullptr,
+  nullptr,
+  nullptr,
+  nullptr,
+  default_find_solib_addr,
 };
diff --git a/gdb/solib-svr4.c b/gdb/solib-svr4.c
--- a/gdb/solib-svr4.c
+++ b/gdb/solib-svr4.c
@@ -3353,6 +3353,15 @@ svr4_iterate_over_objfiles_in_search_order
     }
 }
 
+/* See solib_ops::find_solib_addr in solist.h.  */
+
+static std::optional<CORE_ADDR>
+svr4_find_solib_addr (solib &so)
+{
+  auto *li = gdb::checked_static_cast<lm_info_svr4 *> (so.lm_info.get ());
+  return li->l_addr_inferior;
+}
+
 const struct solib_ops svr4_so_ops =
 {
   svr4_relocate_section_addresses,
@@ -3363,11 +3372,11 @@ const struct solib_ops svr4_so_ops =
   open_symbol_file_object,
   svr4_in_dynsym_resolve_code,
   solib_bfd_open,
-  nullptr,
   svr4_same,
   svr4_keep_data_in_core,
   svr4_update_solib_event_breakpoints,
   svr4_handle_solib_event,
+  svr4_find_solib_addr,
 };
 
 void _initialize_svr4_solib ();
diff --git a/gdb/solib-target.c b/gdb/solib-target.c
--- a/gdb/solib-target.c
+++ b/gdb/solib-target.c
@@ -412,4 +412,9 @@ const solib_ops solib_target_so_ops =
   solib_target_open_symbol_file_object,
   solib_target_in_dynsym_resolve_code,
   solib_bfd_open,
+  nullptr,
+  nullptr,
+  nullptr,
+  nullptr,
+  default_find_solib_addr,
 };
diff --git a/gdb/solib.c b/gdb/solib.c
--- a/gdb/solib.c
+++ b/gdb/solib.c
@@ -45,7 +45,6 @@
 #include "gdb_bfd.h"
 #include "gdbsupport/filestuff.h"
 #include "gdbsupport/scoped_fd.h"
-#include "debuginfod-support.h"
 #include "source.h"
 #include "cli/cli-style.h"
 
@@ -113,7 +112,6 @@ show_solib_search_path (struct ui_file *file, int from_tty,
 static gdb::unique_xmalloc_ptr<char>
 solib_find_1 (const char *in_pathname, int *fd, bool is_solib)
 {
-  const solib_ops *ops = gdbarch_so_ops (current_inferior ()->arch ());
   int found_file = -1;
   gdb::unique_xmalloc_ptr<char> temp_pathname;
   const char *fskind = effective_target_file_system_kind ();
@@ -296,12 +294,6 @@ solib_find_1 (const char *in_pathname, int *fd, bool is_solib)
 			target_lbasename (fskind, in_pathname),
 			O_RDONLY | O_BINARY, &temp_pathname);
 
-  /* If not found, and we're looking for a solib, try to use target
-     supplied solib search method.  */
-  if (is_solib && found_file < 0 && ops->find_and_open_solib)
-    found_file = ops->find_and_open_solib (in_pathname, O_RDONLY | O_BINARY,
-					   &temp_pathname);
-
   /* If not found, next search the inferior's $PATH environment variable.  */
   if (found_file < 0 && sysroot == NULL)
     found_file = openp (current_inferior ()->environment.get ("PATH"),
@@ -475,58 +467,6 @@ solib_bfd_open (const char *pathname)
   return abfd;
 }
 
-/* Mapping of a core file's shared library sonames to their respective
-   build-ids.  Added to the registries of core file bfds.  */
-
-typedef std::unordered_map<std::string, std::string> soname_build_id_map;
-
-/* Key used to associate a soname_build_id_map to a core file bfd.  */
-
-static const struct registry<bfd>::key<soname_build_id_map>
-  cbfd_soname_build_id_data_key;
-
-/* See solib.h.  */
-
-void
-set_cbfd_soname_build_id (gdb_bfd_ref_ptr abfd, const char *soname,
-			  const bfd_build_id *build_id)
-{
-  gdb_assert (abfd.get () != nullptr);
-  gdb_assert (soname != nullptr);
-  gdb_assert (build_id != nullptr);
-
-  soname_build_id_map *mapptr
-    = cbfd_soname_build_id_data_key.get (abfd.get ());
-
-  if (mapptr == nullptr)
-    mapptr = cbfd_soname_build_id_data_key.emplace (abfd.get ());
-
-  (*mapptr)[soname] = build_id_to_string (build_id);
-}
-
-/* If SONAME had a build-id associated with it in ABFD's registry by a
-   previous call to set_cbfd_soname_build_id then return the build-id
-   as a NULL-terminated hex string.  */
-
-static gdb::unique_xmalloc_ptr<char>
-get_cbfd_soname_build_id (gdb_bfd_ref_ptr abfd, const char *soname)
-{
-  if (abfd.get () == nullptr || soname == nullptr)
-    return {};
-
-  soname_build_id_map *mapptr
-    = cbfd_soname_build_id_data_key.get (abfd.get ());
-
-  if (mapptr == nullptr)
-    return {};
-
-  auto it = mapptr->find (lbasename (soname));
-  if (it == mapptr->end ())
-    return {};
-
-  return make_unique_xstrdup (it->second.c_str ());
-}
-
 /* Given a pointer to one of the shared objects in our list of mapped
    objects, use the recorded name to open a bfd descriptor for the
    object, build a section table, relocate all the section addresses
@@ -546,36 +486,55 @@ solib_map_sections (solib &so)
 
   gdb::unique_xmalloc_ptr<char> filename (tilde_expand (so.so_name.c_str ()));
   gdb_bfd_ref_ptr abfd (ops->bfd_open (filename.get ()));
-  gdb::unique_xmalloc_ptr<char> build_id_hexstr
-    = get_cbfd_soname_build_id (current_program_space->cbfd,
-				so.so_name.c_str ());
+
+  /* If we have a core target then the core target might have some helpful
+     information (i.e. build-ids) about the shared libraries we are trying
+     to load.  Grab those hints now and use the below to validate or find
+     the shared libraries.
+
+     If we don't have a core target then this will return an empty struct
+     with no hint information, we then lookup the shared library based on
+     its filename.  */
+  std::optional<CORE_ADDR> solib_addr = ops->find_solib_addr (so);
+  std::optional <const core_target_mapped_file_info> mapped_file_info
+    = core_target_find_mapped_file (so.so_name.c_str (), solib_addr);
 
   /* If we already know the build-id of this solib from a core file, verify
      it matches ABFD's build-id.  If there is a mismatch or the solib wasn't
      found, attempt to query debuginfod for the correct solib.  */
-  if (build_id_hexstr.get () != nullptr)
+  if (mapped_file_info.has_value ())
     {
-      bool mismatch = false;
+      bool mismatch = (abfd != nullptr
+		       && build_id_bfd_get (abfd.get ()) != nullptr
+		       && !build_id_equal (mapped_file_info->build_id (),
+					   build_id_bfd_get (abfd.get ())));
 
-      if (abfd != nullptr && abfd->build_id != nullptr)
-	{
-	  std::string build_id = build_id_to_string (abfd->build_id);
-
-	  if (build_id != build_id_hexstr.get ())
-	    mismatch = true;
-	}
       if (abfd == nullptr || mismatch)
 	{
-	  scoped_fd fd = debuginfod_exec_query (
-	    (const unsigned char *) build_id_hexstr.get (), 0,
-	    so.so_name.c_str (), &filename);
-
-	  if (fd.get () >= 0)
-	    abfd = ops->bfd_open (filename.get ());
-	  else if (mismatch)
-	    warning (_ ("Build-id of %ps does not match core file."),
-		     styled_string (file_name_style.style (),
-				    filename.get ()));
+	  /* If GDB found a suitable file during the file mapping
+	     processing stage then lets use that.  We don't check the
+	     build-id after opening this file, either this file was found
+	     by build-id, in which case it's going to match, or this file
+	     doesn't have a build-id, so checking tells us nothing.
+	     However, if it was good enough during the mapped file
+	     processing, we assume it's good enough now.  */
+	  if (!mapped_file_info->filename ().empty ())
+	    abfd = ops->bfd_open (mapped_file_info->filename ().c_str ());
+	  else
+	    abfd = nullptr;
+
+	  if (abfd == nullptr)
+	    abfd = find_objfile_by_build_id (current_program_space,
+					     mapped_file_info->build_id (),
+					     so.so_name.c_str ());
+
+	  if (abfd == nullptr && mismatch)
+	    {
+	      warning (_ ("Build-id of %ps does not match core file."),
+		       styled_string (file_name_style.style (),
+				      filename.get ()));
+	      abfd = nullptr;
+	    }
 	}
     }
 
@@ -1705,6 +1664,14 @@ remove_user_added_objfile (struct objfile *objfile)
     }
 }
 
+/* See solist.h.  */
+
+std::optional<CORE_ADDR>
+default_find_solib_addr (solib &so)
+{
+  return {};
+}
+
 void _initialize_solib ();
 
 void
diff --git a/gdb/solib.h b/gdb/solib.h
--- a/gdb/solib.h
+++ b/gdb/solib.h
@@ -136,11 +136,4 @@ extern void update_solib_breakpoints (void);
 
 extern void handle_solib_event (void);
 
-/* Associate SONAME with BUILD_ID in ABFD's registry so that it can be
-   retrieved with get_cbfd_soname_build_id.  */
-
-extern void set_cbfd_soname_build_id (gdb_bfd_ref_ptr abfd,
-				      const char *soname,
-				      const bfd_build_id *build_id);
-
 #endif /* SOLIB_H */
diff --git a/gdb/solist.h b/gdb/solist.h
--- a/gdb/solist.h
+++ b/gdb/solist.h
@@ -132,14 +132,6 @@ struct solib_ops
   /* Find and open shared library binary file.  */
   gdb_bfd_ref_ptr (*bfd_open) (const char *pathname);
 
-  /* Optional extra hook for finding and opening a solib.
-     If TEMP_PATHNAME is non-NULL: If the file is successfully opened a
-     pointer to a malloc'd and realpath'd copy of SONAME is stored there,
-     otherwise NULL is stored there.  */
-  int (*find_and_open_solib) (const char *soname,
-			      unsigned o_flags,
-			      gdb::unique_xmalloc_ptr<char> *temp_pathname);
-
   /* Given two so_list objects, one from the GDB thread list
      and another from the list returned by current_sos, return 1
      if they represent the same library.
@@ -167,6 +159,23 @@ struct solib_ops
      NULL, in which case no specific preprocessing is necessary
      for this target.  */
   void (*handle_event) (void);
+
+  /* Return an address within the inferior's address space which is known
+     to be part of SO.  If there is no such address, or GDB doesn't know
+     how to figure out such an address then an empty optional is
+     returned.
+
+     The returned address can be used when loading the shared libraries
+     for a core file.  GDB knows the build-ids for (some) files mapped
+     into the inferior's address space, and knows the address ranges which
+     those mapped files cover.  If GDB can figure out a representative
+     address for the library then this can be used to match a library to a
+     mapped file, and thus to a build-id.  GDB can then use this
+     information to help locate the shared library objfile, if the objfile
+     is not in the expected place (as defined by the shared libraries file
+     name).  */
+
+  std::optional<CORE_ADDR> (*find_solib_addr) (solib &so);
 };
 
 /* A unique pointer to a so_list.  */
@@ -186,4 +195,9 @@ extern gdb_bfd_ref_ptr solib_bfd_fopen (const char *pathname, int fd);
 /* Find solib binary file and open it.  */
 extern gdb_bfd_ref_ptr solib_bfd_open (const char *in_pathname);
 
+/* A default implementation of the solib_ops::find_solib_addr callback.
+   This just returns an empty std::optional<CORE_ADDR> indicating GDB is
+   unable to find an address within the library SO.  */
+extern std::optional<CORE_ADDR> default_find_solib_addr (solib &so);
+
 #endif
diff --git a/gdb/symfile-debug.c b/gdb/symfile-debug.c
--- a/gdb/symfile-debug.c
+++ b/gdb/symfile-debug.c
@@ -637,7 +637,7 @@ objfile::find_and_add_separate_symbol_file (symfile_add_flags symfile_flags)
 	     the user a system specific message that guides them to finding
 	     the missing debug info.  */
 
-	  ext_lang_missing_debuginfo_result ext_result
+	  ext_lang_missing_file_result ext_result
 	    = ext_lang_handle_missing_debuginfo (this);
 	  if (!ext_result.filename ().empty ())
 	    {
diff --git a/gdb/symfile.c b/gdb/symfile.c
--- a/gdb/symfile.c
+++ b/gdb/symfile.c
@@ -1237,6 +1237,8 @@ separate_debug_file_exists (const std::string &name, unsigned long crc,
 			    struct objfile *parent_objfile,
 			    deferred_warnings *warnings)
 {
+  SEPARATE_DEBUG_FILE_SCOPED_DEBUG_ENTER_EXIT;
+
   unsigned long file_crc;
   int file_crc_p;
   struct stat parent_stat, abfd_stat;
@@ -1251,19 +1253,13 @@ separate_debug_file_exists (const std::string &name, unsigned long crc,
   if (filename_cmp (name.c_str (), objfile_name (parent_objfile)) == 0)
     return 0;
 
-  if (separate_debug_file_debug)
-    {
-      gdb_printf (gdb_stdlog, _("  Trying %s..."), name.c_str ());
-      gdb_flush (gdb_stdlog);
-    }
+  separate_debug_file_debug_printf ("Trying %s...", name.c_str ());
 
   gdb_bfd_ref_ptr abfd (gdb_bfd_open (name.c_str (), gnutarget));
 
   if (abfd == NULL)
     {
-      if (separate_debug_file_debug)
-	gdb_printf (gdb_stdlog, _(" no, unable to open.\n"));
-
+      separate_debug_file_debug_printf ("unable to open file");
       return 0;
     }
 
@@ -1285,10 +1281,7 @@ separate_debug_file_exists (const std::string &name, unsigned long crc,
       if (abfd_stat.st_dev == parent_stat.st_dev
 	  && abfd_stat.st_ino == parent_stat.st_ino)
 	{
-	  if (separate_debug_file_debug)
-	    gdb_printf (gdb_stdlog,
-			_(" no, same file as the objfile.\n"));
-
+	  separate_debug_file_debug_printf ("same file as the objfile");
 	  return 0;
 	}
       verified_as_different = 1;
@@ -1300,9 +1293,7 @@ separate_debug_file_exists (const std::string &name, unsigned long crc,
 
   if (!file_crc_p)
     {
-      if (separate_debug_file_debug)
-	gdb_printf (gdb_stdlog, _(" no, error computing CRC.\n"));
-
+      separate_debug_file_debug_printf ("error computing CRC");
       return 0;
     }
 
@@ -1318,20 +1309,18 @@ separate_debug_file_exists (const std::string &name, unsigned long crc,
 	{
 	  if (!gdb_bfd_crc (parent_objfile->obfd.get (), &parent_crc))
 	    {
-	      if (separate_debug_file_debug)
-		gdb_printf (gdb_stdlog,
-			    _(" no, error computing CRC.\n"));
-
+	      separate_debug_file_debug_printf ("error computing CRC");
 	      return 0;
 	    }
 	}
 
       if (verified_as_different || parent_crc != file_crc)
 	{
-	  if (separate_debug_file_debug)
-	    gdb_printf (gdb_stdlog, "the debug information found in \"%s\""
-			" does not match \"%s\" (CRC mismatch).\n",
-			name.c_str (), objfile_name (parent_objfile));
+	  separate_debug_file_debug_printf
+	    ("the debug information found in \"%s\" does not match "
+	     "\"%s\" (CRC mismatch).", name.c_str (),
+	     objfile_name (parent_objfile));
+
 	  warnings->warn (_("the debug information found in \"%ps\""
 			    " does not match \"%ps\" (CRC mismatch)."),
 			  styled_string (file_name_style.style (),
@@ -1343,8 +1332,7 @@ separate_debug_file_exists (const std::string &name, unsigned long crc,
       return 0;
     }
 
-  if (separate_debug_file_debug)
-    gdb_printf (gdb_stdlog, _(" yes!\n"));
+  separate_debug_file_debug_printf ("found a match");
 
   return 1;
 }
@@ -1385,23 +1373,18 @@ find_separate_debug_file (const char *dir,
 			  unsigned long crc32, struct objfile *objfile,
 			  deferred_warnings *warnings)
 {
-  if (separate_debug_file_debug)
-    gdb_printf (gdb_stdlog,
-		_("\nLooking for separate debug info (debug link) for "
-		  "%s\n"), objfile_name (objfile));
+  SEPARATE_DEBUG_FILE_SCOPED_DEBUG_START_END
+    ("looking for separate debug info (debug link) for %s",
+     objfile_name (objfile));
 
   /* First try in the same directory as the original file.  */
-  std::string debugfile = dir;
-  debugfile += debuglink;
+  std::string debugfile = path_join (dir, debuglink);
 
   if (separate_debug_file_exists (debugfile, crc32, objfile, warnings))
     return debugfile;
 
   /* Then try in the subdirectory named DEBUG_SUBDIRECTORY.  */
-  debugfile = dir;
-  debugfile += DEBUG_SUBDIRECTORY;
-  debugfile += "/";
-  debugfile += debuglink;
+  debugfile = path_join (dir, DEBUG_SUBDIRECTORY, debuglink);
 
   if (separate_debug_file_exists (debugfile, crc32, objfile, warnings))
     return debugfile;
@@ -1414,10 +1397,13 @@ find_separate_debug_file (const char *dir,
   bool target_prefix = is_target_filename (dir);
   const char *dir_notarget
     = target_prefix ? dir + strlen (TARGET_SYSROOT_PREFIX) : dir;
+  const char *target_prefix_str = target_prefix ? TARGET_SYSROOT_PREFIX : "";
   std::vector<gdb::unique_xmalloc_ptr<char>> debugdir_vec
     = dirnames_to_char_ptr_vec (debug_file_directory.c_str ());
-  gdb::unique_xmalloc_ptr<char> canon_sysroot
-    = gdb_realpath (gdb_sysroot.c_str ());
+  const char *sysroot_str = gdb_sysroot.c_str ();
+  if (is_target_filename (sysroot_str) && target_filesystem_is_local ())
+    sysroot_str += strlen (TARGET_SYSROOT_PREFIX);
+  gdb::unique_xmalloc_ptr<char> canon_sysroot = gdb_realpath (sysroot_str);
 
  /* MS-Windows/MS-DOS don't allow colons in file names; we must
     convert the drive letter into a one-letter directory, so that the
@@ -1442,12 +1428,8 @@ find_separate_debug_file (const char *dir,
 
   for (const gdb::unique_xmalloc_ptr<char> &debugdir : debugdir_vec)
     {
-      debugfile = target_prefix ? TARGET_SYSROOT_PREFIX : "";
-      debugfile += debugdir;
-      debugfile += "/";
-      debugfile += drive;
-      debugfile += dir_notarget;
-      debugfile += debuglink;
+      debugfile = path_join (target_prefix_str, debugdir.get (),
+			     drive.c_str (), dir_notarget, debuglink);
 
       if (separate_debug_file_exists (debugfile, crc32, objfile, warnings))
 	return debugfile;
@@ -1464,39 +1446,18 @@ find_separate_debug_file (const char *dir,
 	{
 	  /* If the file is in the sysroot, try using its base path in
 	     the global debugfile directory.  */
-	  debugfile = target_prefix ? TARGET_SYSROOT_PREFIX : "";
-	  debugfile += debugdir;
-	  debugfile += "/";
-	  debugfile += base_path;
-	  debugfile += "/";
-	  debugfile += debuglink;
+	  debugfile = path_join (target_prefix_str, debugdir.get (),
+				 base_path, debuglink);
 
 	  if (separate_debug_file_exists (debugfile, crc32, objfile, warnings))
 	    return debugfile;
 
 	  /* If the file is in the sysroot, try using its base path in
-	     the sysroot's global debugfile directory.  GDB_SYSROOT
-	     might refer to a target: path; we strip the "target:"
-	     prefix -- but if that would yield the empty string, we
-	     don't bother at all, because that would just give the
-	     same result as above.  */
+	     the sysroot's global debugfile directory.  */
 	  if (gdb_sysroot != TARGET_SYSROOT_PREFIX)
 	    {
-	      debugfile = target_prefix ? TARGET_SYSROOT_PREFIX : "";
-	      if (is_target_filename (gdb_sysroot))
-		{
-		  std::string root
-		    = gdb_sysroot.substr (strlen (TARGET_SYSROOT_PREFIX));
-		  gdb_assert (!root.empty ());
-		  debugfile += root;
-		}
-	      else
-		debugfile += gdb_sysroot;
-	      debugfile += debugdir;
-	      debugfile += "/";
-	      debugfile += base_path;
-	      debugfile += "/";
-	      debugfile += debuglink;
+	      debugfile = path_join (gdb_sysroot.c_str (), debugdir.get (),
+				     base_path, debuglink);
 
 	      if (separate_debug_file_exists (debugfile, crc32, objfile,
 					      warnings))
diff --git a/gdb/symfile.h b/gdb/symfile.h
--- a/gdb/symfile.h
+++ b/gdb/symfile.h
@@ -371,6 +371,25 @@ extern gdb_bfd_ref_ptr find_separate_debug_file_in_section (struct objfile *);
 
 extern bool separate_debug_file_debug;
 
+/* Print a "separate-debug-file" debug statement.  */
+
+#define separate_debug_file_debug_printf(fmt, ...)		\
+  debug_prefixed_printf_cond (separate_debug_file_debug,	\
+			      "separate-debug-file",		\
+			      fmt, ##__VA_ARGS__)
+
+/* Print "separate-debug-file" enter/exit debug statements.  */
+
+#define SEPARATE_DEBUG_FILE_SCOPED_DEBUG_ENTER_EXIT \
+  scoped_debug_enter_exit (separate_debug_file_debug,	\
+			   "separate-debug-file")
+
+/* Print "separate-debug-file" start/end debug statements.  */
+
+#define SEPARATE_DEBUG_FILE_SCOPED_DEBUG_START_END(fmt, ...) \
+  scoped_debug_start_end (separate_debug_file_debug,	     \
+			  "separate-debug-file", fmt, ##__VA_ARGS__)
+
 /* Read full symbols immediately.  */
 
 extern int readnow_symbol_files;
diff --git a/gdb/target.c b/gdb/target.c
--- a/gdb/target.c
+++ b/gdb/target.c
@@ -3196,6 +3196,14 @@ target_ops::fileio_fstat (int fd, struct stat *sb, fileio_error *target_errno)
   return -1;
 }
 
+int
+target_ops::fileio_stat (struct inferior *inf, const char *filename,
+			 struct stat *sb, fileio_error *target_errno)
+{
+  *target_errno = FILEIO_ENOSYS;
+  return -1;
+}
+
 int
 target_ops::fileio_close (int fd, fileio_error *target_errno)
 {
@@ -3315,6 +3323,29 @@ target_fileio_fstat (int fd, struct stat *sb, fileio_error *target_errno)
 
 /* See target.h.  */
 
+int
+target_fileio_stat (struct inferior *inf, const char *filename,
+		    struct stat *sb, fileio_error *target_errno)
+{
+  for (target_ops *t = default_fileio_target (); t != NULL; t = t->beneath ())
+    {
+      int ret = t->fileio_stat (inf, filename, sb, target_errno);
+
+      if (ret == -1 && *target_errno == FILEIO_ENOSYS)
+	continue;
+
+      target_debug_printf_nofunc ("target_fileio_stat (%s) = %d (%d)",
+				  filename, ret,
+				  ret != -1 ? 0 : *target_errno);
+      return ret;
+    }
+
+  *target_errno = FILEIO_ENOSYS;
+  return -1;
+}
+
+/* See target.h.  */
+
 int
 target_fileio_close (int fd, fileio_error *target_errno)
 {
diff --git a/gdb/target.h b/gdb/target.h
--- a/gdb/target.h
+++ b/gdb/target.h
@@ -1011,6 +1011,14 @@ struct target_ops
        *TARGET_ERRNO).  */
     virtual int fileio_fstat (int fd, struct stat *sb, fileio_error *target_errno);
 
+    /* Get information about the file FILENAME and put it in SB.  Look for
+       FILENAME in the filesystem as seen by INF.  If INF is NULL, use the
+       filesystem seen by the debugger (GDB or, for remote targets, the
+       remote stub).  Return 0 on success, or -1 if an error occurs (and
+       set *TARGET_ERRNO).  */
+    virtual int fileio_stat (struct inferior *inf, const char *filename,
+			     struct stat *sb, fileio_error *target_errno);
+
     /* Close FD on the target.  Return 0, or -1 if an error occurs
        (and set *TARGET_ERRNO).  */
     virtual int fileio_close (int fd, fileio_error *target_errno);
@@ -2220,6 +2228,14 @@ extern int target_fileio_pread (int fd, gdb_byte *read_buf, int len,
 extern int target_fileio_fstat (int fd, struct stat *sb,
 				fileio_error *target_errno);
 
+/* Get information about the file at FILENAME on the target and put it in
+   SB.  Look in the filesystem as seen by INF.  If INF is NULL, use the
+   filesystem seen by the debugger (GDB or, for remote targets, the remote
+   stub).  Return 0 on success, or -1 if an error occurs (and set
+   *TARGET_ERRNO).  */
+extern int target_fileio_stat (struct inferior *inf, const char *filename,
+			       struct stat *sb, fileio_error *target_errno);
+
 /* Close FD on the target.  Return 0, or -1 if an error occurs
    (and set *TARGET_ERRNO).  */
 extern int target_fileio_close (int fd, fileio_error *target_errno);
diff --git a/gdb/testsuite/gdb.base/build-id-seqno.c b/gdb/testsuite/gdb.base/build-id-seqno.c
new file mode 100644
--- /dev/null
+++ b/gdb/testsuite/gdb.base/build-id-seqno.c
@@ -0,0 +1,22 @@
+/* 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/>.  */
+
+int
+main (void)
+{
+  return 0;
+}
diff --git a/gdb/testsuite/gdb.base/build-id-seqno.exp b/gdb/testsuite/gdb.base/build-id-seqno.exp
new file mode 100644
--- /dev/null
+++ b/gdb/testsuite/gdb.base/build-id-seqno.exp
@@ -0,0 +1,133 @@
+# 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/>.
+
+# Setup a .build-id/ based debug directory containing multiple entries
+# for the same build-id, with each entry given a different sequence
+# number.
+#
+# Ensure that GDB will scan over broken symlinks for the same build-id
+# (but different sequence number) to find later working symlinks.
+#
+# This test only places debug information on the host, so it is always
+# local to GDB.
+
+require {!is_remote host}
+
+standard_testfile
+
+if {[build_executable "failed to prepare" $testfile $srcfile] == -1} {
+    return -1
+}
+
+# Split out BINFILE.debug.  Remove debug from BINFILE.
+if {[gdb_gnu_strip_debug $binfile] != 0} {
+    return -1
+}
+
+# Get the '.build-id/xx/xxx...xxx' part of the filename.
+set build_id_filename [build_id_debug_filename_get $binfile]
+
+# Hide (rename) BINFILE.debug, this should ensure GDB can't find it
+# directly but needs to look for the build-id based file in the debug
+# directory.
+set hidden_debuginfo [standard_output_file "hidden_$testfile.debug"]
+remote_exec build "mv ${binfile}.debug $hidden_debuginfo"
+
+# A filename that doesn't exist.  Some symlinks will point at this
+# file.
+set missing_debuginfo [host_standard_output_file "missing_debuginfo"]
+
+# Create the debug directory, and the .build-id directory structure
+# within it.
+set debugdir [host_standard_output_file "debug"]
+remote_exec host "mkdir -p $debugdir/[file dirname $build_id_filename]"
+
+set host_hidden_debuginfo [gdb_remote_download host $hidden_debuginfo]
+remote_exec host "ln -fs $host_hidden_debuginfo $debugdir/$build_id_filename"
+
+# Start GDB and load global BINFILE.  If FIND_DEBUGINFO is true then
+# we expect GDB to find the debug information matching BINFILE,
+# otherwise, we expect GDB not to find the debug information.
+proc load_binfile_check_debug_is_found { find_debuginfo testname } {
+    with_test_prefix "$testname" {
+	clean_restart
+
+	gdb_test_no_output "set debug-file-directory $::debugdir" \
+	    "set debug-file-directory"
+
+	gdb_file_cmd $::binfile
+
+	if { $find_debuginfo } {
+	    gdb_assert { [regexp [string_to_regexp \
+				      "Reading symbols from $::hidden_debuginfo..."] \
+			      $::gdb_file_cmd_msg] } \
+		"debuginfo was read via build-id"
+	} else {
+	    gdb_assert { [regexp "\\(No debugging symbols found in \[^\r\n\]+/$::testfile\\)" \
+			      $::gdb_file_cmd_msg] } \
+	    }
+    }
+}
+
+# Return a copy of FILENAME, which should end '.debug', with NUMBER
+# added, e.g. add_seqno 1 "foo.debug" --> "foo.1.debug".
+proc add_seqno { number filename } {
+    return [regsub "\.debug\$" $filename ".${number}.debug"]
+}
+
+load_binfile_check_debug_is_found true \
+    "find debuginfo with a single build-id file"
+
+remote_exec host "ln -fs $host_hidden_debuginfo \
+			$debugdir/[add_seqno 1 $build_id_filename]"
+remote_exec host "ln -fs $host_hidden_debuginfo \
+			$debugdir/[add_seqno 2 $build_id_filename]"
+remote_exec host "ln -fs $host_hidden_debuginfo \
+			$debugdir/[add_seqno 3 $build_id_filename]"
+
+load_binfile_check_debug_is_found true \
+    "find debuginfo with 4 build-id files"
+
+remote_exec host "ln -fs $missing_debuginfo $debugdir/$build_id_filename"
+
+load_binfile_check_debug_is_found true \
+    "find debuginfo, first build-id file is bad"
+
+remote_exec host "ln -fs $missing_debuginfo \
+			$debugdir/[add_seqno 1 $build_id_filename]"
+remote_exec host "ln -fs $missing_debuginfo \
+			$debugdir/[add_seqno 3 $build_id_filename]"
+
+load_binfile_check_debug_is_found true  \
+    "find debuginfo, first 2 build-id files are bad"
+
+remote_exec host "ln -fs $missing_debuginfo \
+			$debugdir/[add_seqno 2 $build_id_filename]"
+
+load_binfile_check_debug_is_found false  \
+    "cannot find debuginfo, all build-id files are bad"
+
+remote_exec host "ln -fs $host_hidden_debuginfo \
+			$debugdir/[add_seqno 3 $build_id_filename]"
+
+load_binfile_check_debug_is_found true  \
+    "find debuginfo, last build-id file is good"
+
+remote_exec host "rm -f $debugdir/[add_seqno 1 $build_id_filename]"
+
+load_binfile_check_debug_is_found false  \
+    "cannot find debuginfo, file with seqno 1 is missing"
diff --git a/gdb/testsuite/gdb.base/corefile-buildid.exp b/gdb/testsuite/gdb.base/corefile-buildid.exp
--- a/gdb/testsuite/gdb.base/corefile-buildid.exp
+++ b/gdb/testsuite/gdb.base/corefile-buildid.exp
@@ -172,11 +172,9 @@ proc locate_exec_from_core_build_id {corefile buildid suffix \
 	"mkdir -p [file join $debugdir [file dirname $buildid]]"
 
     set files_list {}
+    lappend files_list $binfile $buildid
     if {$sepdebug} {
-	lappend files_list "$binfile.stripped" $buildid
 	lappend files_list "$binfile.debug" "$buildid.debug"
-    } else {
-	lappend files_list $binfile $buildid
     }
     if {$shared} {
 	global sharedir
@@ -200,12 +198,7 @@ proc locate_exec_from_core_build_id {corefile buildid suffix \
     gdb_test "core-file $corefile" "Program terminated with .*" \
 	"load core file"
     if {$symlink} {
-	if {$sepdebug} {
-	    set expected_file [file join $builddir \
-				   [file tail "$binfile.stripped"]]
-	} else {
-	    set expected_file [file join $builddir [file tail $binfile]]
-	}
+	set expected_file [file join $builddir [file tail $binfile]]
     } else {
 	set expected_file $buildid
     }
@@ -245,15 +238,12 @@ proc do_corefile_buildid_tests {args} {
 
     if {$sepdebug} {
 	# Strip debuginfo into its own file.
-	if {[gdb_gnu_strip_debug [standard_output_file $program_to_run]] \
-		!= 0} {
+	if {[gdb_gnu_strip_debug [standard_output_file $program_to_run] \
+		 no-debuglink] != 0} {
 	    untested "could not strip executable  for [join $suffix \ ]"
 	    return
 	}
 
-	# Run the stripped program instead of the original.
-	set program_to_run [file join $builddir \
-				[file tail "$binfile.stripped"]]
 	lappend suffix "sepdebug"
     }
 
diff --git a/gdb/testsuite/gdb.base/corefile.exp b/gdb/testsuite/gdb.base/corefile.exp
--- a/gdb/testsuite/gdb.base/corefile.exp
+++ b/gdb/testsuite/gdb.base/corefile.exp
@@ -199,6 +199,45 @@ gdb_test "up" "#\[0-9\]* *(\[0-9xa-fH'\]* in)? .* \\(.*\\).*" "up (reinit)"
 
 gdb_test "core" "No core file now."
 
+# Temporarily move coremmap.data out of the way and reload the core
+# file.  We should still be able to read buf2 as the contents of this
+# are written into the core file.  In contrast buf2ro should no longer
+# be readable as the contents of this region are not within the core
+# file, GDB relies on reading this from the coremmap.data file, which
+# can no longer be found.
+set coremmap_data_filename \
+    [standard_output_file coredir.[getpid]/coremmap.data]
+set coremmap_data_backup_filename \
+    [standard_output_file coredir.[getpid]/coremmap.data.backup]
+remote_exec host "mv ${coremmap_data_filename} \
+		  ${coremmap_data_backup_filename}"
+
+clean_restart $binfile
+
+# Load the core file and check we get a warning about the
+# coremmap.data file being missing.
+gdb_test_multiple "core-file $corefile" "warn about coremmap.data missing" {
+    -re -wrap "warning: Can't open file \[^\r\n\]+/coremmap.data during file-backed mapping note processing\r\n.*" {
+	pass $gdb_test_name
+    }
+}
+
+# This xfail was just copied from earlier in the script where we also
+# read from buf2.
+setup_xfail "*-*-sunos*" "*-*-aix*"
+gdb_test "x/8bd buf2" \
+    ".*:.*0.*1.*2.*3.*4.*5.*6.*7.*" \
+    "accessing mmapped data in core file with coremmap.data removed"
+
+gdb_test "x/8bd buf2ro" \
+    "$hex\[^:\]*:\\s+Cannot access memory at address $hex" \
+    "accessing read-only mmapped data in core file with coremmap.data removed"
+
+# Restore the coremmap.data file so later tests don't give warnings
+# when the core file is reloaded.
+remote_exec host "mv ${coremmap_data_backup_filename} \
+		  ${coremmap_data_filename}"
+
 # Test that we can unload the core with the "detach" command.
 
 proc_with_prefix corefile_detach {} {
diff --git a/gdb/testsuite/gdb.base/solib-search.exp b/gdb/testsuite/gdb.base/solib-search.exp
--- a/gdb/testsuite/gdb.base/solib-search.exp
+++ b/gdb/testsuite/gdb.base/solib-search.exp
@@ -43,7 +43,11 @@ set right_binfile2_lib \
 set binfile1_lib [standard_output_file ${libname1}.so]
 set binfile2_lib [standard_output_file ${libname2}.so]
 
-set lib_flags [list debug ldflags=-Wl,-Bsymbolic]
+# When this test was written, GDB's ability to track down shared
+# libraries for a core file based on the build-id much poorer.  As GDB
+# has improved we now need to disable build-ids in order for this test
+# to function as expected.
+set lib_flags [list debug no-build-id ldflags=-Wl,-Bsymbolic]
 set wrong_lib_flags "$lib_flags additional_flags=-DARRAY_SIZE=1"
 set right_lib_flags "$lib_flags additional_flags=-DARRAY_SIZE=8192 additional_flags=-DRIGHT"
 
diff --git a/gdb/testsuite/gdb.base/sysroot-debug-lookup.exp b/gdb/testsuite/gdb.base/sysroot-debug-lookup.exp
new file mode 100644
--- /dev/null
+++ b/gdb/testsuite/gdb.base/sysroot-debug-lookup.exp
@@ -0,0 +1,184 @@
+# 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/>.
+
+# Test GDB's ability to find debug information by looking within the
+# sysroot.
+#
+# We compile a static binary (to reduce what we need to copy into the
+# sysroot), split the debug information from the binary, and setup a
+# sysroot.
+#
+# The debug-file-directory is set to just '/debug', but we're
+# expecting GDB to actually look in '$SYSROOT/debug'.
+#
+# There's a test for using .build-id based lookup, and a test for
+# gnu_debuglink based lookup.
+
+require {!is_remote host}
+
+standard_testfile main.c
+
+# Create a copy of BINFILE, split out the debug information, and then
+# setup a sysroot.  Hide (by moving) the actual debug information file
+# and create a symlink to the hidden debug information from within the
+# sysroot.
+#
+# Start GDB, set the sysroot, and then load the executable, ensure GDB
+# finds the debug information, which must have happened by lookin in
+# the sysroot.
+proc_with_prefix lookup_via_build_id {} {
+    set filename ${::binfile}_1
+    if { [build_executable "build exec" ${filename} $::srcfile \
+	      {additional_flags=-static debug build-id}] } {
+	return
+    }
+
+    # Split debug information into a .debug file, remove debug
+    # information from FILENAME.  Don't add a .gnu_debuglink to
+    # FILENAME, we rely on the build-id.
+    if {[gdb_gnu_strip_debug $filename { no-debuglink }] != 0} {
+	unsupported "cannot split debug information from executable"
+	return
+    }
+
+    set sysroot [standard_output_file "sysroot1"]
+    set debug_dir "/debug"
+
+    set debug_symlink \
+	${sysroot}${debug_dir}/[build_id_debug_filename_get $filename]
+
+    set build_id_dir [file dirname $debug_symlink]
+
+    set debug_filename ${filename}_hidden_debug
+
+    remote_exec build "mkdir -p $build_id_dir"
+    remote_exec build "mv $filename.debug $debug_filename"
+    remote_exec build "ln -sf $debug_filename $debug_symlink"
+
+    foreach_with_prefix sysroot_prefix { "" "target:" } {
+	clean_restart
+
+	gdb_test_no_output "set sysroot ${sysroot_prefix}$sysroot" "set sysroot"
+	gdb_test_no_output "set debug-file-directory $debug_dir"
+
+	gdb_file_cmd $filename
+
+	gdb_assert { $::gdb_file_cmd_debug_info eq "debug" } \
+	    "ensure debug information was found"
+
+	if { $sysroot_prefix eq "target:"
+	     && [target_info gdb_protocol] == "extended-remote"} {
+	    # Only when using the extended-remote board will we have
+	    # started a remote target by this point.  In this case GDB
+	    # will see the 'target:' prefix as remote, and so the
+	    # reported filename will include the 'target:' prefix.
+	    #
+	    # In all other cases we will still be using the default,
+	    # initial target, in which case GDB considers the
+	    # 'target:' prefix to indicate the local filesystem.
+	    set lookup_filename $sysroot_prefix$debug_symlink
+	} else {
+	    set lookup_filename $debug_filename
+	}
+	set re [string_to_regexp "Reading symbols from $lookup_filename..."]
+	gdb_assert {[regexp $re $::gdb_file_cmd_msg]} \
+	    "debug symbols read from correct file"
+    }
+}
+
+# Create a copy of BINFILE, split out the debug information, and then
+# setup a sysroot.  Hide (by moving) the actual debug information file
+# and create a symlink to the hidden debug information from within the
+# sysroot.
+#
+# Copy the executable into the sysroot and then start GDB, set the
+# sysroot, and load the executable.  Check that GDB finds the debug
+# information, which must have happened by lookin in the sysroot.
+proc_with_prefix lookup_via_debuglink {} {
+    set filename ${::binfile}_2
+    if { [build_executable "build exec" ${filename} $::srcfile \
+	      {additional_flags=-static debug no-build-id}] } {
+	return
+    }
+
+    # Split debug information into a .debug file, remove debug
+    # information from FILENAME.
+    if {[gdb_gnu_strip_debug $filename] != 0} {
+	unsupported "cannot split debug information from executable"
+	return
+    }
+
+    # We're going to setup the sysroot like this:
+    #
+    # sysroot2/
+    #    bin/
+    #      $FILENAME
+    #    debug/
+    #      bin/
+    #        $FILENAME.debug
+    #
+    # When looking up debug information via the debuglink, GDB will
+    # only search in the sysroot if the original objfile was in the
+    # sysroot.  And GDB will resolve symlinks, so if the objfile is
+    # symlinked to outside the sysroot, GDB will not search in the
+    # sysroot for the debug information.
+    #
+    # So we have to copy the executable into the sysroot.
+    #
+    # We are OK to symlink the debug information to a file outside the
+    # sysroot though.
+
+    set sysroot [standard_output_file "sysroot2"]
+
+    foreach path { bin debug/bin } {
+	remote_exec build "mkdir -p $sysroot/$path"
+    }
+
+    # Copy the executable into the sysroot.
+    set file_basename [file tail $filename]
+    set exec_in_sysroot ${sysroot}/bin/${file_basename}
+    remote_exec build "cp $filename $exec_in_sysroot"
+
+    # Rename the debug file outside of the sysroot, this should stop
+    # GDB finding this file "by accident".
+    set debug_filename ${filename}_hidden_debug
+    remote_exec build "mv $filename.debug $debug_filename"
+
+    # Symlink the debug information into the sysroot.
+    set debug_symlink \
+	${sysroot}/debug/bin/${file_basename}.debug
+    remote_exec build "ln -sf $debug_filename $debug_symlink"
+
+    foreach_with_prefix sysroot_prefix { "" "target:" } {
+	# Restart GDB and setup the sysroot and debug directory.
+	clean_restart
+	gdb_test_no_output "set sysroot ${sysroot_prefix}$sysroot" "set sysroot"
+	gdb_test_no_output "set debug-file-directory /debug"
+
+	# Load the executable, we expect GDB to find the debug information
+	# in the sysroot.
+	gdb_file_cmd ${sysroot_prefix}$exec_in_sysroot
+
+	gdb_assert { $::gdb_file_cmd_debug_info eq "debug" } \
+	    "ensure debug information was found"
+
+	set re [string_to_regexp "Reading symbols from ${sysroot_prefix}$debug_symlink..."]
+	gdb_assert {[regexp $re $::gdb_file_cmd_msg]} \
+	    "debug symbols read from correct file"
+    }
+}
+
+lookup_via_build_id
+lookup_via_debuglink
diff --git a/gdb/testsuite/gdb.debuginfod/build-id-no-debug-warning.exp b/gdb/testsuite/gdb.debuginfod/build-id-no-debug-warning.exp
--- a/gdb/testsuite/gdb.debuginfod/build-id-no-debug-warning.exp
+++ b/gdb/testsuite/gdb.debuginfod/build-id-no-debug-warning.exp
@@ -30,14 +30,11 @@ if {[build_executable "build executable" ${testfile} ${srcfile} \
     return -1
 }
 
-# Split BINFILE into BINFILE.stripped and BINFILE.debug, the first is
-# the executable with the debug information removed, and the second is
-# the debug information.
+# Split debug information from BINFILE into BINFILE.debug.
 #
-# However, by passing the "no-debuglink" flag we prevent this proc
-# from adding a .gnu_debuglink section to the executable.  Any lookup
-# of the debug information by GDB will need to be done based on the
-# build-id.
+# By passing the "no-debuglink" flag we prevent this proc from adding
+# a .gnu_debuglink section to BINFILE.  Any lookup of the debug
+# information by GDB will need to be done based on the build-id.
 if {[gdb_gnu_strip_debug $binfile no-debuglink]} {
     unsupported "cannot produce separate debug info files"
     return -1
@@ -59,12 +56,6 @@ set debuginfod_debugdir [standard_output_file "debug"]
 remote_exec build "mkdir $debuginfod_debugdir"
 remote_exec build "mv $debugfile $debuginfod_debugdir"
 
-# This is BINFILE with the debug information removed.  We are going to
-# place this in the BUILD_ID_DEBUG_FILE location, this would usually
-# represent a mistake by the user, and will trigger a warning from
-# GDB, this is the warning we are checking for.
-set stripped_binfile [standard_output_file "${binfile}.stripped"]
-
 # Create the .build-id/PREFIX directory name from
 # .build-id/PREFIX/SUFFIX.debug filename.
 set debugdir [file dirname ${build_id_debug_file}]
@@ -76,7 +67,7 @@ remote_exec build "mkdir -p $debugdir"
 # information, which will point back at this file, which also doesn't
 # have debug information, which could cause a loop.  But GDB will spot
 # this and give a warning.
-remote_exec build "mv ${stripped_binfile} ${build_id_debug_file}"
+remote_exec build "mv ${binfile} ${build_id_debug_file}"
 
 # Now start GDB.
 clean_restart
diff --git a/gdb/testsuite/gdb.debuginfod/corefile-mapped-file-1.c b/gdb/testsuite/gdb.debuginfod/corefile-mapped-file-1.c
new file mode 100644
--- /dev/null
+++ b/gdb/testsuite/gdb.debuginfod/corefile-mapped-file-1.c
@@ -0,0 +1,24 @@
+/* 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/>.  */
+
+/* This is in the shared library.  */
+extern int foo (void);
+
+int
+main (void)
+{
+  int res = foo ();
+  return res;
+}
diff --git a/gdb/testsuite/gdb.debuginfod/corefile-mapped-file-2.c b/gdb/testsuite/gdb.debuginfod/corefile-mapped-file-2.c
new file mode 100644
--- /dev/null
+++ b/gdb/testsuite/gdb.debuginfod/corefile-mapped-file-2.c
@@ -0,0 +1,22 @@
+/* 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/>.  */
+
+volatile int *library_ptr = (int *) POINTER_VALUE;
+
+int
+foo (void)
+{
+  return *library_ptr;
+}
diff --git a/gdb/testsuite/gdb.debuginfod/corefile-mapped-file-3.c b/gdb/testsuite/gdb.debuginfod/corefile-mapped-file-3.c
new file mode 100644
--- /dev/null
+++ b/gdb/testsuite/gdb.debuginfod/corefile-mapped-file-3.c
@@ -0,0 +1,45 @@
+/* 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/>.  */
+
+#include <assert.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <sys/mman.h>
+
+volatile void* library_base_address = 0;
+volatile int *ptr = 0;
+
+int
+main ()
+{
+  struct stat buf;
+  int res;
+
+  int fd = open (SHLIB_FILENAME, O_RDONLY);
+  assert (fd != -1);
+
+  res = fstat (fd, &buf);
+  assert (res == 0);
+
+  library_base_address
+    = mmap (NULL, buf.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
+
+  res = *ptr;	/* Undefined behaviour here.  */
+
+  return 0;
+}
diff --git a/gdb/testsuite/gdb.debuginfod/corefile-mapped-file.exp b/gdb/testsuite/gdb.debuginfod/corefile-mapped-file.exp
new file mode 100644
--- /dev/null
+++ b/gdb/testsuite/gdb.debuginfod/corefile-mapped-file.exp
@@ -0,0 +1,380 @@
+# 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/>.  */
+
+# This test checks GDB's ability to use build-ids when setting up file backed
+# mappings as part of reading a core-file.
+#
+# A core-file contains a list of the files that were mapped into the process
+# at the time of the core-file creation.  If the file was mapped read-only
+# then the file contents will not be present in the core-file, but instead GDB
+# is expected to open the mapped file and read the contents from there if
+# needed.  And this is what GDB does.
+#
+# GDB (via the BFD library) will also spot if a mapped looks like a valid ELF
+# and contains a build-id, this build-id is passed back to GDB so that GDB can
+# validate the on-disk file it finds matches the file that was mapped when the
+# core-file was created.
+#
+# In addition, if the on-disk file is found to have a non-matching build-id
+# then GDB can use debuginfod to (try) and download a suitable file.
+#
+# This test is about checking that this file backed mapping mechanism works
+# correctly; that GDB will spot when the build-ids fail to match and will
+# refuse to load an incorrect file.  Additionally we check that the correct
+# file can be downloaded from debuginfod.
+#
+# The test is rather contrived though.  Instead of relying on having a shared
+# library mapped at the time of crash we mmap a shared library into the
+# process and then check this mapping within the test.
+#
+# The problem with using a normal shared library load for this test is that
+# the shared library list is processed as part of a separate step when opening
+# the core file.  Right now this separate step doesn't check the build-ids
+# correctly, so GDB will potentially open the wrong shared library file.  The
+# sections of this incorrect shared library are then added to GDB's list of
+# target sections, and are used to satisfy memory reads, which can give the
+# wrong results.
+#
+# This obviously needs fixing, but is a separate problem from the one being
+# tested here, so this test deliberately checks the mapping using a file that
+# is mmaped rather than loaded as a shared library, as such the file is in the
+# core-files list of mapped files, but is not in the shared library list.
+#
+# Despite this test living in the gdb.debuginfod/ directory, only the last
+# part of this test actually uses debuginfod, everything up to that point is
+# pretty generic.
+
+require {!is_remote host}
+require {!is_remote target}
+
+load_lib debuginfod-support.exp
+
+require allow_shlib_tests
+
+standard_testfile -1.c -2.c -3.c
+
+# Compile an executable that loads the shared library as an actual
+# shared library, then use GDB to figure out the offset of the
+# variable 'library_ptr' within the library.
+set library_filename [standard_output_file "libfoo.so"]
+set binfile2 [standard_output_file "library_loader"]
+
+if {[prepare_for_testing_full "build exec which loads the shared library" \
+	 [list $library_filename \
+	      { debug shlib build-id \
+		    additional_flags=-DPOINTER_VALUE=0x12345678 } \
+	      $srcfile2 {}] \
+	 [list $binfile2 [list debug shlib=$library_filename ] \
+	      $srcfile { debug }]] != 0} {
+    return
+}
+
+if {![runto_main]} {
+    return
+}
+
+if { [is_address_zero_readable] } {
+    return
+}
+
+set ptr_address [get_hexadecimal_valueof "&library_ptr" "unknown"]
+
+set ptr_offset "unknown"
+gdb_test_multiple "info proc mappings" "" {
+    -re "^\\s+($hex)\\s+($hex)\\s+$hex\\s+($hex)\[^\r\n\]+$library_filename\r\n" {
+	set low_addr $expect_out(1,string)
+	set high_addr $expect_out(2,string)
+	set file_offset $expect_out(3,string)
+
+	if {[expr $ptr_address >= $low_addr] && [expr $ptr_address < $high_addr]} {
+	    set mapping_offset [expr $ptr_address - $low_addr]
+	    set ptr_offset [format 0x%x [expr $file_offset + $mapping_offset]]
+	}
+
+	exp_continue
+    }
+
+    -re "^$gdb_prompt $" {
+    }
+
+    -re "(^\[^\r\n\]*)\r\n" {
+	set tmp $expect_out(1,string)
+	exp_continue
+    }
+}
+
+gdb_assert { $ptr_offset ne "unknown" } \
+    "found pointer offset"
+
+set ptr_size [get_integer_valueof "sizeof (library_ptr)" "unknown"]
+set ptr_format_char ""
+if { $ptr_size == 2 } {
+    set ptr_format_char "b"
+} elseif { $ptr_size == 4 } {
+    set ptr_format_char "w"
+} elseif { $ptr_size == 8 } {
+    set ptr_format_char "g"
+}
+if { $ptr_format_char eq "" } {
+    untested "could not figure out size of library_ptr variable"
+    return
+}
+
+# Helper proc to read a value from inferior memory.  Reads at address held in
+# global PTR_ADDRESS, and use PTR_FORMAT_CHAR for the size of the read.
+proc read_ptr_value { } {
+    set value ""
+    gdb_test_multiple "x/1${::ptr_format_char}x ${::ptr_address}" "" {
+	-re -wrap "^${::hex}(?:\\s+<\[^>\]+>)?:\\s+($::hex)" {
+	    set value $expect_out(1,string)
+	}
+	-re -wrap "^${::hex}(?:\\s+<\[^>\]+>)?:\\s+Cannot access memory at address ${::hex}" {
+	    set value "unavailable"
+	}
+    }
+    return $value
+}
+
+set ptr_expected_value [read_ptr_value]
+if { $ptr_expected_value eq "" } {
+    untested "could not find expected value for library_ptr"
+    return
+}
+
+# Now compile a second executable.  This one doesn't load the shared
+# library as an actual shared library, but instead mmaps the library
+# into the executable.
+#
+# Load this executable within GDB and confirm that we can use the
+# offset we calculated previously to view the value of 'library_ptr'.
+set opts [list debug additional_flags=-DSHLIB_FILENAME=\"$library_filename\"]
+if {[prepare_for_testing "prepare second executable" $binfile \
+	 $srcfile3 $opts] != 0} {
+    return
+}
+
+if {![runto_main]} {
+    return
+}
+
+gdb_breakpoint [gdb_get_line_number "Undefined behaviour here" $srcfile3]
+gdb_continue_to_breakpoint "run to breakpoint"
+
+set library_base_address \
+    [get_hexadecimal_valueof "library_base_address" "unknown"]
+set ptr_address [format 0x%x [expr $library_base_address + $ptr_offset]]
+
+set ptr_value [read_ptr_value]
+gdb_assert { $ptr_value == $ptr_expected_value } \
+    "check value of pointer variable"
+
+# Now rerun the second executable outside of GDB.  The executable should crash
+# and generate a corefile.
+set corefile [core_find $binfile]
+if {$corefile eq ""} {
+    untested "could not generate core file"
+    return
+}
+
+# Load a core file from the global COREFILE.  Use TESTNAME as the name
+# of the test.
+#
+# If LINE_RE is not the empty string then this is a regexp for a line
+# that we expect to see in the output when loading the core file, if
+# the line is not present then this test will fail.
+#
+# Any lines beginning with 'warning: ' will cause this test to fail.
+#
+# A couple of other standard lines that are produced when loading a
+# core file are also checked for, just to make sure the core file
+# loading has progressed as expected.
+proc load_core_file { testname { line_re "" } } {
+    set code {}
+
+    if { $line_re ne "" } {
+	append code {
+	    -re "^$line_re\r\n" {
+		set saw_expected_line true
+		exp_continue
+	    }
+	}
+	set saw_expected_line false
+    } else {
+	set saw_expected_line true
+    }
+
+    set saw_unknown_warning false
+    set saw_generated_by_line false
+    set saw_prog_terminated_line false
+
+    append code {
+	-re "^warning: \[^\r\n\]+\r\n" {
+	    set saw_unknown_warning true
+	    exp_continue
+	}
+
+	-re "^Core was generated by \[^\r\n\]+\r\n" {
+	    set saw_generated_by_line true
+	    exp_continue
+	}
+
+	-re "^Program terminated with signal SIGSEGV, Segmentation fault\\.\r\n" {
+	    set saw_prog_terminated_line true
+	    exp_continue
+	}
+
+	-re "^$::gdb_prompt $" {
+	    gdb_assert {$saw_generated_by_line \
+			    && $saw_prog_terminated_line \
+			    && $saw_expected_line \
+			    && !$saw_unknown_warning} \
+		$gdb_test_name
+	}
+
+	-re "^\[^\r\n\]*\r\n" {
+	    exp_continue
+	}
+    }
+
+    set res [catch { return [gdb_test_multiple "core-file $::corefile" \
+				 "$testname" $code] } string]
+
+    if {$res == 1} {
+	global errorInfo errorCode
+	return -code error -errorinfo $errorInfo -errorcode $errorCode $string
+    } elseif {$res == 2} {
+	return $string
+    } else {
+	# We expect RES to be 2 (TCL_RETURN) or 1 (TCL_ERROR).  If we get
+	# here then somehow the 'catch' above finished without hitting
+	# either of those cases, which is .... weird.
+	perror "unexepcted return value, code = $res, value = $string"
+	return -1
+    }
+}
+
+# And now restart GDB, load the core-file and check that the library shows as
+# being mapped in, and that we can still read the library_ptr value from
+# memory.
+clean_restart $binfile
+
+load_core_file "load core file"
+
+set library_base_address [get_hexadecimal_valueof "library_base_address" \
+			      "unknown" "get library_base_address in core-file"]
+set ptr_address [format 0x%x [expr $library_base_address + $ptr_offset]]
+
+set ptr_value [read_ptr_value]
+gdb_assert { $ptr_value == $ptr_expected_value } \
+    "check value of pointer variable from core-file"
+
+# Now move the shared library file away and restart GDB.  This time when we
+# load the core-file we should see a warning that GDB has failed to map in the
+# library file.  An attempt to read the variable from the library file should
+# fail / give a warning.
+set library_backup_filename [standard_output_file "libfoo.so.backup"]
+remote_exec build "mv \"$library_filename\" \"$library_backup_filename\""
+
+clean_restart $binfile
+
+load_core_file "load corefile with library file missing" \
+    "warning: Can't open file [string_to_regexp $library_filename] during file-backed mapping note processing"
+
+set ptr_value [read_ptr_value]
+gdb_assert { $ptr_value eq "unavailable" } \
+    "check value of pointer is unavailable with library file missing"
+
+# Now symlink the .build-id/xx/xxx...xxx filename within the debug
+# directory to library we just moved aside.  Restart GDB and setup the
+# debug-file-directory before loading the core file.
+#
+# GDB should lookup the file to map via the build-id link in the
+# .build-id/ directory.
+set debugdir [standard_output_file "debugdir"]
+set build_id_filename \
+    $debugdir/[build_id_debug_filename_get $library_backup_filename ""]
+
+remote_exec build "mkdir -p [file dirname $build_id_filename]"
+remote_exec build "ln -sf $library_backup_filename $build_id_filename"
+
+clean_restart $binfile
+
+gdb_test_no_output "set debug-file-directory $debugdir" \
+    "set debug-file-directory"
+
+load_core_file "load corefile, lookup in debug-file-directory"
+
+set ptr_value [read_ptr_value]
+gdb_assert { $ptr_value == $ptr_expected_value } \
+    "check value of pointer variable from core-file, lookup in debug-file-directory"
+
+# Build a new version of the shared library, keep the library the same size,
+# but change the contents so the build-id changes.  Then restart GDB and load
+# the core-file again.  GDB should spot that the build-id for the shared
+# library is not as expected, and should refuse to map in the shared library.
+if {[build_executable "build second version of shared library" \
+	 $library_filename $srcfile2 \
+	 { debug shlib build-id \
+	       additional_flags=-DPOINTER_VALUE=0x11223344 }] != 0} {
+    return
+}
+
+clean_restart $binfile
+
+load_core_file "load corefile with wrong library in place" \
+    "warning: File [string_to_regexp $library_filename] doesn't match build-id from core-file during file-backed mapping processing"
+
+set ptr_value [read_ptr_value]
+gdb_assert { $ptr_value eq "unavailable" } \
+    "check value of pointer is unavailable with wrong library in place"
+
+# Setup a debuginfod server which can serve the original shared library file.
+# Then restart GDB and load the core-file.  GDB should download the original
+# shared library from debuginfod and use that to provide the file backed
+# mapping.
+if {![allow_debuginfod_tests]} {
+    untested "skippig debuginfod parts of this test"
+    return
+}
+
+set server_dir [standard_output_file "debuginfod.server"]
+file mkdir $server_dir
+file rename -force $library_backup_filename $server_dir
+
+prepare_for_debuginfod cache db
+
+set url [start_debuginfod $db $server_dir]
+if { $url eq "" } {
+    unresolved "failed to start debuginfod server"
+    return
+}
+
+with_debuginfod_env $cache {
+    setenv DEBUGINFOD_URLS $url
+
+    clean_restart
+    gdb_test_no_output "set debuginfod enabled on" \
+	"enabled debuginfod for initial test"
+    gdb_load $binfile
+
+    load_core_file "load corefile, download library from debuginfod" \
+	"Downloading\[^\r\n\]* file [string_to_regexp $library_filename]\\.\\.\\."
+
+    set ptr_value [read_ptr_value]
+    gdb_assert { $ptr_value == $ptr_expected_value } \
+	"check value of pointer variable after downloading library file"
+}
+
+stop_debuginfod
diff --git a/gdb/testsuite/gdb.debuginfod/solib-with-soname-1.c b/gdb/testsuite/gdb.debuginfod/solib-with-soname-1.c
new file mode 100644
--- /dev/null
+++ b/gdb/testsuite/gdb.debuginfod/solib-with-soname-1.c
@@ -0,0 +1,39 @@
+/* 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/>.  */
+
+#include <stdlib.h>
+
+/* It is important that these two variables have names of the same length
+   so that the debug information in the two library versions is laid out
+   the same.  If they differ then the .dynamic section might move, which
+   will trigger a different check within GDB than the one we actually want
+   to check.  */
+
+#if LIB_VERSION == 1
+volatile int *library_1_var = (volatile int *) 0x12345678;
+#elif LIB_VERSION == 2
+volatile int *library_2_var = (volatile int *) 0x11223344;
+#else
+# error Unknown library version
+#endif
+
+int
+foo (void)
+{
+  /* This should trigger a core dump.  */
+  abort ();
+
+  return 0;
+}
diff --git a/gdb/testsuite/gdb.debuginfod/solib-with-soname-2.c b/gdb/testsuite/gdb.debuginfod/solib-with-soname-2.c
new file mode 100644
--- /dev/null
+++ b/gdb/testsuite/gdb.debuginfod/solib-with-soname-2.c
@@ -0,0 +1,41 @@
+/* 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/>.  */
+
+#include <assert.h>
+#include <dlfcn.h>
+#include <stddef.h>
+
+/* This is in the shared library.  */
+extern int foo (void);
+
+/* This is updated by the .exp file.  */
+char *libname = "libfoo_2.so";
+
+int
+main (void)
+{
+  void *handle;
+  int res, tmp;
+
+  handle = dlopen (libname, RTLD_LAZY);
+  assert (handle != NULL);
+
+  res = foo ();
+
+  tmp = dlclose (handle);
+  assert (tmp == 0);
+
+  return res;
+}
diff --git a/gdb/testsuite/gdb.debuginfod/solib-with-soname.exp b/gdb/testsuite/gdb.debuginfod/solib-with-soname.exp
new file mode 100644
--- /dev/null
+++ b/gdb/testsuite/gdb.debuginfod/solib-with-soname.exp
@@ -0,0 +1,290 @@
+# 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/>.  */
+
+# This test exercises GDB's ability to validate build-ids when loading
+# shared libraries for a core file.
+#
+# The test creates two "versions" of a shared library, sets up a
+# symlink to point to one version of the library, and creates a core file.
+#
+# We then try re-loading the core file and executable and check that
+# GDB is able to correctly load the shared library.  To confuse things
+# we retarget the library symlink at the other version of the library.
+#
+# After that we repeat the test, but this time deleting the symlink
+# completely.
+#
+# Then we remove the version of the library completely, at this point
+# we do expect GDB to give a warning about being unable to load the library.
+#
+# And finally, we setup debuginfod and have it serve the missing
+# library file, GDB should correctly download the library file.
+#
+# Despite this test living in the gdb.debuginfod/ directory, only the last
+# part of this test actually uses debuginfod, everything up to that point is
+# pretty generic.
+
+load_lib debuginfod-support.exp
+
+require allow_shlib_tests
+require {istarget "*-linux*"}
+require {!is_remote host}
+require {!using_fission}
+
+standard_testfile -1.c -2.c
+
+# Build two similar, but slightly different versions of the shared
+# library.  Both libraries have DT_SONAME set to the generic
+# libfoo.so, we'll create a symlink with that name later.
+set library_1_filename [standard_output_file "libfoo_1.so"]
+set library_2_filename [standard_output_file "libfoo_2.so"]
+
+# The generic name for the library.
+set library_filename [standard_output_file "libfoo.so"]
+
+# When compiling a shared library the -Wl,-soname,NAME option is
+# automatically added based on the final name of the library.  We want
+# to compile libfoo_1.so, but set the soname to libfoo.so.  To achieve
+# this we first compile into libfoo.so, and then rename the library to
+# libfoo_1.so.
+if {[build_executable "build libfoo_1.so" $library_filename \
+	 $srcfile \
+	      { debug shlib build-id \
+		    additional_flags=-DLIB_VERSION=1 }] == -1} {
+    return
+}
+remote_exec build "mv ${library_filename} ${library_1_filename}"
+
+# See the comment above, but this time we rename to libfoo_2.so.
+if {[build_executable "build libfoo_2.so" $library_filename \
+	 $srcfile \
+	      { debug shlib build-id \
+		    additional_flags=-DLIB_VERSION=2 }] == -1} {
+    return
+}
+remote_exec build "mv ${library_filename} ${library_2_filename}"
+
+# Create libfoo.so symlink to the libfoo_1.so library.  If this
+# symlink creation fails then we assume we can't create symlinks on
+# this host.  If this succeeds then later symlink creation is required
+# to succeed, and will trigger an FAIL if it doesn't.
+set status \
+    [remote_exec build \
+	 "ln -sf ${library_1_filename} ${library_filename}"]
+if {[lindex $status 0] != 0} {
+    unsupported "host does not support symbolic links"
+    return
+}
+
+# Build the executable.  This links against libfoo.so, which is
+# poining at libfoo_1.so.  Just to confuse things even more, this
+# executable uses dlopen to load libfoo_2.so.  Weird!
+if { [build_executable "build executable" ${binfile} ${srcfile2} \
+	  [list debug shlib=${library_filename} shlib_load]] == -1 } {
+    return
+}
+
+# If the board file is automatically splitting the debug information
+# into a separate file (e.g. the cc-with-gnu-debuglink.exp board) then
+# this test isn't going to work.
+clean_restart
+gdb_file_cmd $binfile
+if {$gdb_file_cmd_debug_info ne "debug"} {
+    unsupported "failed to find debug information"
+    return
+}
+if {[regexp "${testfile}.debug" $gdb_file_cmd_msg]} {
+    unsupported "debug information has been split to a separate file"
+    return
+}
+
+# Run BINFILE which will generate a corefile.
+set corefile [core_find $binfile]
+if {$corefile eq ""} {
+    untested "could not generate core file"
+    return
+}
+
+# Helper proc to load global BINFILE and then load global COREFILE.
+#
+# If EXPECT_WARNING is true then we require a warning about being
+# unable to load the shared library symbols, otherwise, EXPECT_WARNING
+# is false and we require no warning.
+#
+# If EXPECT_DOWNLOAD is true then we require a line indicating that
+# the shared library is being downloaded from debuginfod, otherwise
+# the shared library should not be downloaded.
+#
+# If DEBUGDIR is not the empty string then 'debug-file-directory' is
+# set to the value of DEBUGDIR.
+proc load_exec_and_core_file { expect_warning expect_download testname \
+				   {debugdir ""} } {
+    with_test_prefix $testname {
+	clean_restart $::binfile
+
+	if { $debugdir ne "" } {
+	    gdb_test_no_output "set debug-file-directory $debugdir" \
+		"set debug directory"
+	}
+
+	set saw_warning false
+	set saw_download false
+	set saw_generated false
+	set saw_terminated false
+
+	gdb_test_multiple "core-file $::corefile" "load core file" {
+	    -re "^Core was generated by \[^\r\n\]+\r\n" {
+		set saw_generated true
+		exp_continue
+	    }
+	    -re "^Program terminated with signal \[^\r\n\]+\r\n" {
+		set saw_terminated true
+		exp_continue
+	    }
+	    -re "^warning: Can't open file \[^\r\n\]+ during file-backed mapping note processing\r\n" {
+		# Ignore warnings from the file backed mapping phase.
+		exp_continue
+	    }
+	    -re "^warning: Could not load shared library symbols for \[^\r\n\]+/libfoo\\.so\\.\r\n" {
+		set saw_warning true
+		exp_continue
+	    }
+	    -re "^Downloading file \[^\r\n\]+/libfoo_1\\.so\\.\\.\\.\r\n" {
+		set saw_download true
+		exp_continue
+	    }
+	    -re "^$::gdb_prompt $" {
+		gdb_assert { $saw_generated && $saw_terminated \
+				 && $saw_warning == $expect_warning \
+				 && $saw_download == $expect_download } \
+		    $gdb_test_name
+	    }
+	    -re "^\[^\r\n\]*\r\n" {
+		exp_continue
+	    }
+	}
+
+	# If we don't expect a warning then debug symbols from the
+	# shared library should be available.  Confirm we can read a
+	# variable from the shared library.  If we do expect a warning
+	# then the shared library debug symbols have not loaded, and
+	# the library variable should not be available.
+	if { !$expect_warning } {
+	    gdb_test "print/x library_1_var" " = 0x12345678" \
+		"check library_1_var can be read"
+	} else {
+	    gdb_test "print/x library_1_var" \
+		"^No symbol \"library_1_var\" in current context\\." \
+		"check library_1_var cannot be read"
+	}
+    }
+}
+
+# Initial test, just load the executable and core file.  At this point
+# everything should load fine as everything is where we expect to find
+# it.
+load_exec_and_core_file false false \
+    "load core file, all libraries as expected"
+
+# Update libfoo.so symlink to point at the second library then reload
+# the core file.  GDB should spot that the symlink points to the wrong
+# file, but should be able to figure out the correct file to load as
+# the right file will be in the mapped file list.
+set status [remote_exec build \
+		"ln -sf ${library_2_filename} ${library_filename}"]
+gdb_assert { [lindex $status 0] == 0 } \
+    "update library symlink to point to the wrong file"
+
+load_exec_and_core_file false false \
+    "load core file, symlink points to wrong file"
+
+# Remove libfoo.so symlink and reload the core file.  As in the
+# previous test GDB should be able to figure out the correct file to
+# load as the correct file will still appear in the mapped file list.
+set status [remote_exec build "rm -f ${library_filename}"]
+gdb_assert { [lindex $status 0] == 0 } "remove library symlink"
+
+load_exec_and_core_file false false \
+    "load core file, symlink removed"
+
+# Remove LIBRARY_1_FILENAME.  We'll now see a warning that the mapped
+# file can't be loaded (we ignore that warning), and we'll see a
+# warning that the shared library can't be loaded.
+set library_1_backup_filename ${library_1_filename}.backup
+set status \
+    [remote_exec build \
+	 "mv ${library_1_filename} ${library_1_backup_filename}"]
+gdb_assert { [lindex $status 0] == 0 } \
+    "remove libfoo_1.so"
+
+load_exec_and_core_file true false \
+    "load core file, libfoo_1.so removed"
+
+# Symlink the .build-id/xx/xxx...xxx filename within the debug
+# directory to LIBRARY_1_BACKUP_FILENAME, now when we restart GDB it
+# should find the missing library within the debug directory.
+set debugdir [standard_output_file "debugdir"]
+set build_id_filename \
+    $debugdir/[build_id_debug_filename_get $library_1_backup_filename ""]
+set status \
+    [remote_exec build \
+	 "mkdir -p [file dirname $build_id_filename]"]
+gdb_assert { [lindex $status 0] == 0 } \
+    "create sub-directory within the debug directory"
+set status \
+    [remote_exec build \
+	 "ln -sf $library_1_backup_filename $build_id_filename"]
+gdb_assert { [lindex $status 0] == 0 } \
+    "create symlink within the debug directory "
+
+load_exec_and_core_file false false \
+    "load core file, find libfoo_1.so through debug-file-directory" \
+    $debugdir
+
+# Setup a debuginfod server which can serve the original shared
+# library file.
+if {![allow_debuginfod_tests]} {
+    untested "skippig debuginfod parts of this test"
+    return
+}
+
+set server_dir [standard_output_file "debuginfod.server"]
+file mkdir $server_dir
+file rename -force $library_1_backup_filename $server_dir
+
+prepare_for_debuginfod cache db
+
+set url [start_debuginfod $db $server_dir]
+if { $url eq "" } {
+    unresolved "failed to start debuginfod server"
+    return
+}
+
+with_debuginfod_env $cache {
+    setenv DEBUGINFOD_URLS $url
+
+    save_vars { GDBFLAGS } {
+	append GDBFLAGS " -ex \"set debuginfod enabled on\""
+
+	# Reload the executable and core file.  GDB should download
+	# the file libfoo_1.so using debuginfod during the mapped file
+	# phase, but should then reuse that download during the shared
+	# library phase.
+	load_exec_and_core_file false true \
+	    "load core file, use debuginfod"
+    }
+}
+
+stop_debuginfod
diff --git a/gdb/testsuite/gdb.dwarf2/short-build-id.exp b/gdb/testsuite/gdb.dwarf2/short-build-id.exp
new file mode 100644
--- /dev/null
+++ b/gdb/testsuite/gdb.dwarf2/short-build-id.exp
@@ -0,0 +1,119 @@
+# 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/>.
+
+# Create a file with an artificially short (1-byte) build-id, and
+# check that GDB doesn't try to load debug information.  If we do try
+# then we end up loading from: `debug-directory/.build-id/xx/.debug`
+# which isn't right.
+
+load_lib dwarf.exp
+
+# This test can only be run on targets which support DWARF-2 and use gas.
+require dwarf2_support
+
+# No remote host testing either.
+require {!is_remote host}
+
+standard_testfile main.c
+
+# Create an assembler file which encodes BUILDID as the build-id.  Compile
+# this along with the global SRCFILE to create a test executable.
+#
+# Split the debug information out from the newly created executable and place
+# it into the debug file directory.
+#
+# Load the executable into GDB and check to see if the debug information was
+# loaded or not.  For this test we are expecting that the debug information
+# was not loaded.  The reason is that, with short values for BUILDID, GDB ends
+# up looking for the debug information in weird locations.
+proc run_test { buildid } {
+    set len [string length $buildid]
+
+    set asm_file [standard_output_file "$::testfile.$len.S"]
+    Dwarf::assemble $asm_file {
+	declare_labels int_label int_label2
+
+	upvar buildid buildid
+
+	build_id $buildid
+
+	cu { label cu_start } {
+	    compile_unit {{language @DW_LANG_C}} {
+		int_label2: base_type {
+		    {name int}
+		    {byte_size 4 sdata}
+		    {encoding @DW_ATE_signed}
+		}
+
+		constant {
+		    {name the_int}
+		    {type :$int_label2}
+		    {const_value 99 data1}
+		}
+	    }
+	}
+
+	aranges {} cu_start {
+	    arange {} 0 0
+	}
+    }
+
+    set execfile [standard_output_file $::testfile.$len]
+
+    if { [build_executable_from_specs "failed to build" \
+	      $execfile {debug no-build-id} \
+	      $::srcfile debug \
+	      $asm_file {}] } {
+	return
+    }
+
+    # Create the debug directory.
+    set debugdir [standard_output_file "debugdir.$len"]
+    set build_id_dir $debugdir/.build-id/$buildid
+    remote_exec host "mkdir -p $build_id_dir"
+
+    # Split out the debug information.
+    if {[gdb_gnu_strip_debug $execfile no-debuglink]} {
+	unresolved "failed to split out debug information"
+	return
+    }
+
+    # Move the debug information into the debug directory.  We place the debug
+    # information into a file called just '.debug'.  GDB should not check this
+    # file, but at one point GDB would check this file, even though this
+    # doesn't make much sense.
+    set execfile_debug ${execfile}.debug
+    remote_exec host "mv $execfile_debug $build_id_dir/.debug"
+
+    # Start GDB, set the debug-file-directory, and try loading the file.
+    clean_restart
+
+    gdb_test_no_output "set debug-file-directory $debugdir" \
+	"set debug-file-directory"
+
+    gdb_file_cmd $execfile
+
+    gdb_assert { $::gdb_file_cmd_debug_info eq "nodebug" } \
+	"no debug should be loaded"
+
+    # For sanity, read something that was encoded in the debug
+    # information, this should fail.
+    gdb_test "print the_int" \
+	"(?:No symbol table is loaded|No symbol \"the_int\" in current context).*"
+}
+
+foreach_with_prefix buildid { a4 "" } {
+    run_test $buildid
+}
diff --git a/gdb/testsuite/gdb.python/py-missing-objfile-lib.c b/gdb/testsuite/gdb.python/py-missing-objfile-lib.c
new file mode 100644
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-missing-objfile-lib.c
@@ -0,0 +1,35 @@
+/* This test program 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/>.  */
+
+struct lib_type
+{
+  int a;
+  int b;
+};
+
+volatile struct lib_type global_lib_var = { 0, 0 };
+
+int
+foo (void)
+{
+  int res = 0;
+
+  res += global_lib_var.a;
+  res += global_lib_var.b;
+
+  return 0;
+}
diff --git a/gdb/testsuite/gdb.python/py-missing-objfile.c b/gdb/testsuite/gdb.python/py-missing-objfile.c
new file mode 100644
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-missing-objfile.c
@@ -0,0 +1,49 @@
+/* This test program 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/>.  */
+
+#include <stdlib.h>
+
+struct exec_type
+{
+  int a;
+  int b;
+  int c;
+};
+
+volatile struct exec_type global_exec_var = { 0, 0, 0 };
+
+extern int foo (void);
+
+void
+dump_core (void)
+{
+  abort ();
+}
+
+int
+main (void)
+{
+  int res = foo ();
+
+  res += global_exec_var.a;
+  res += global_exec_var.b;
+  res += global_exec_var.c;
+
+  dump_core ();
+
+  return res;
+}
diff --git a/gdb/testsuite/gdb.python/py-missing-objfile.exp b/gdb/testsuite/gdb.python/py-missing-objfile.exp
new file mode 100644
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-missing-objfile.exp
@@ -0,0 +1,565 @@
+# Copyright (C) 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/>.
+
+load_lib gdb-python.exp
+
+require allow_python_tests
+require {!is_remote host}
+
+standard_testfile .c -lib.c
+
+# Build the library.
+set libname ${testfile}-lib
+set libfile [standard_output_file $libname]
+if { [build_executable "build shlib" $libfile $srcfile2 \
+	  {debug shlib build-id}] == -1} {
+    return
+}
+
+# Build the executable.
+set opts [list debug build-id shlib=${libfile}]
+if { [build_executable "build exec" $binfile $srcfile $opts] == -1} {
+    return
+}
+
+# The cc-with-gnu-debuglink board will split the debug out into the
+# .debug directory.  This test script relies on having GDB lookup the
+# objfile and debug via the build-id, which this test sets up.  Trying
+# to do that, while also supporting the cc-with-gnu-debuglink board is
+# just too complicated.
+if {[file isdirectory [standard_output_file ".debug"]]} {
+    unsupported "split debug testing not supported"
+    return
+}
+
+set remote_python_file \
+    [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py]
+
+# Generate a core file.
+set corefile [core_find $binfile {}]
+if {$corefile == ""} {
+    unsupported "core file not generated"
+    return 0
+}
+
+# Create a directory named DIRNAME for use as the
+# debug-file-directory.  Populate the directory with links (based on
+# the build-ids) to each file in the list FILES.
+#
+# Return the full filename of DIRNAME on the host.
+proc setup_debugdir { dirname files } {
+    set debugdir [host_standard_output_file $dirname]
+
+    # Create basic empty directory structure (in case FILES is empty).
+    remote_exec host "mkdir -p $debugdir/.build-id/"
+
+    foreach file $files {
+	set build_id_filename [build_id_debug_filename_get $file ""]
+
+	remote_exec host "mkdir -p $debugdir/[file dirname $build_id_filename]"
+	remote_exec host "ln -s $file $debugdir/$build_id_filename"
+    }
+
+    return $debugdir
+}
+
+# Query some symbols in the inferior to see if GDB managed to find the
+# executable (when EXEC_LOADED is true) and/or the library (when LIB_LOADED
+# is true).
+proc check_loaded_debug { exec_loaded lib_loaded } {
+    if { $exec_loaded } {
+	gdb_test "whatis global_exec_var" "^type = volatile struct exec_type"
+
+	if { $lib_loaded } {
+	    gdb_test "whatis global_lib_var" "^type = volatile struct lib_type"
+	} else {
+	    gdb_test "whatis global_lib_var" \
+		"^No symbol \"global_lib_var\" in current context\\."
+	}
+    } else {
+	gdb_test "whatis global_exec_var" \
+	    "^No symbol table is loaded\\.  Use the \"file\" command\\."
+	gdb_test "whatis global_lib_var" \
+	    "^No symbol table is loaded\\.  Use the \"file\" command\\."
+    }
+}
+
+# Load the global corefile.  The EXTRA_RE is checked for prior to GDB
+# announcing that the core-file has been loaded.
+proc load_core_file { {extra_re ".*"} } {
+    gdb_test "core-file $::corefile" \
+	[multi_line \
+	     "$extra_re" \
+	     "Core was generated by \[^\r\n\]+" \
+	     "Program terminated with signal SIGABRT, Aborted\\." \
+	     "\[^\r\n\]+(?:\r\n\[^\r\n\]+)?"] \
+	"loaded the core file"
+}
+
+# Set the debug-file-directory to DIRNAME.
+proc set_debug_file_dir { dirname } {
+    gdb_test_no_output "set debug-file-directory $dirname" \
+	"set debug-file-directory"
+}
+
+# Restart GDB and load the support Python script.
+proc clean_restart_load_python {} {
+    clean_restart
+    gdb_test "source $::remote_python_file" "^Success" \
+	"load python script"
+}
+
+# For sanity, lets check that we can load the specify the executable
+# and then load the core-file the easy way.
+with_test_prefix "initial sanity check" {
+    clean_restart $binfile
+    load_core_file
+    check_loaded_debug true true
+}
+
+# Move the executable and library into a location that the core-file
+# can't possibly know about.  After this the only way GDB can track
+# down these files will be by looking in the debug-file-directory.
+set hidden_dir [host_standard_output_file "hidden"]
+set hidden_binfile "$hidden_dir/$testfile"
+set hidden_libfile "$hidden_dir/$libname"
+remote_exec host "mkdir -p $hidden_dir"
+remote_exec host "mv $libfile $hidden_libfile"
+remote_exec host "mv $binfile $hidden_binfile"
+
+# If using the fission-dwp board then we'll have .dwp files that also
+# need to be moved.
+if {[remote_file host exists ${libfile}.dwp]} {
+    remote_exec host "mv ${libfile}.dwp ${hidden_libfile}.dwp"
+}
+
+if {[remote_file host exists ${binfile}.dwp]} {
+    remote_exec host "mv ${binfile}.dwp ${hidden_binfile}.dwp"
+}
+
+with_test_prefix "no objfiles, no debug-file-directory" {
+    clean_restart
+    load_core_file
+    check_loaded_debug false false
+}
+
+# Setup some debug-file-directories.
+set debugdir_no_lib \
+    [setup_debugdir "debugdir.no-lib" [list "$hidden_binfile"]]
+set debugdir_empty \
+    [setup_debugdir "debugdir.empty" {}]
+set debugdir_all \
+    [setup_debugdir "debugdir.all" [list "$hidden_libfile" \
+					"$hidden_binfile"]]
+
+with_test_prefix "no objfiles available" {
+    # Another sanity check that GDB can find the files via the
+    # debug-file-directory.
+    clean_restart
+    set_debug_file_dir $debugdir_empty
+    load_core_file
+    check_loaded_debug false false
+}
+
+with_test_prefix "all objfiles available" {
+    # Another sanity check that GDB can find the files via the
+    # debug-file-directory.
+    set_debug_file_dir $debugdir_all
+    load_core_file
+    check_loaded_debug true true
+}
+
+with_test_prefix "lib objfile missing" {
+    # Another sanity check that GDB can find the files via the
+    # debug-file-directory.
+    set_debug_file_dir $debugdir_no_lib
+    load_core_file
+    check_loaded_debug true false
+}
+
+with_test_prefix "all objfiles missing, handler returns None" {
+    clean_restart_load_python
+    gdb_test_no_output \
+	"python gdb.missing_objfile.register_handler(None, handler_obj)" \
+	"register initial handler"
+    load_core_file
+
+    check_loaded_debug false false
+
+    # The handler should be called three times, once for the
+    # mapped-file, once for the core-file's exec, and once for the
+    # shared library.
+    gdb_test "python print(handler_obj.call_count)" "^3" \
+	"check handler was called three times"
+}
+
+with_test_prefix "lib objfile missing, handler returns None" {
+    # Reset handler_obj.
+    gdb_test_no_output "python handler_obj.set_mode(Mode.RETURN_NONE)"
+
+    set_debug_file_dir $debugdir_no_lib
+    load_core_file
+    check_loaded_debug true false
+
+    # The handler will be called twice, once when GDB tries to
+    # load the shared library during the memory-mapped file phase,
+    # then again for the shared library loading.
+    gdb_test "python print(handler_obj.call_count)" "^2" \
+	"check handler was called three times"
+}
+
+with_test_prefix "handler installs lib objfile" {
+    set build_id_filename [build_id_debug_filename_get \
+				   $hidden_libfile ""]
+    remote_exec host \
+	"mkdir -p $debugdir_no_lib/[file dirname $build_id_filename]"
+    gdb_test_no_output "python handler_obj.set_mode(Mode.RETURN_TRUE, \
+    	\"$hidden_libfile\", \"$debugdir_no_lib/$build_id_filename\")" \
+	"configure handler"
+
+    load_core_file
+    check_loaded_debug true true
+
+    # Cleanup so the test can be reproduced again later if needed.
+    remote_exec host "rm $debugdir_no_lib/$build_id_filename"
+}
+
+with_test_prefix "handler points to lib objfile" {
+    set build_id_filename [build_id_debug_filename_get \
+			       $hidden_libfile ""]
+    remote_exec host \
+	"mkdir -p $debugdir_no_lib/[file dirname $build_id_filename]"
+    gdb_test_no_output "python handler_obj.set_mode(Mode.RETURN_STRING, \
+						\"$hidden_libfile\")" \
+	"configure handler"
+
+    load_core_file
+    check_loaded_debug true true
+
+    # Cleanup so the test can be reproduced again later if needed.
+    remote_exec host "rm $debugdir_no_lib/$build_id_filename"
+
+    # The handler will only have been called once when loading the
+    # memory-mapped file.  GDB is smart enough to reuse the previously
+    # discovered BFD object as the shared library.
+    gdb_test "python print(handler_obj.call_count)" "^1" \
+	"check good handler hasn't been called again"
+
+    # Validate the filename and build-id arguments passed to the handler.
+    set expected_buildid [get_build_id $hidden_libfile]
+    gdb_test "python print(handler_last_buildid)" "^$expected_buildid"
+    gdb_test "python print(handler_last_filename)" \
+	"^[string_to_regexp $libfile]"
+}
+
+# Register another global handler, this one raises an exception.  Reload the
+# core-file, the bad handler should be invoked first, which raises an
+# excetption, at which point GDB should skip further Python handlers.
+with_test_prefix "handler raises an exception" {
+    gdb_test_no_output \
+	"python gdb.missing_objfile.register_handler(None, rhandler)"
+
+    foreach_with_prefix exception_type {gdb.GdbError TypeError} {
+	gdb_test_no_output \
+	    "python rhandler.exception_type = $exception_type"
+
+	# Load the core file.  We expect the exception message to appear at
+	# least once in the output.
+	set re [string_to_regexp \
+		    "Python Exception <class '$exception_type'>: message"]
+	load_core_file "${re}.*"
+
+	# Our original handler is still registered, but should not have been
+	# called again (as the exception occurs first).
+	gdb_test "python print(handler_obj.call_count)" "^1" \
+	    "check good handler hasn't been called again"
+    }
+}
+
+# Re-start GDB.
+clean_restart_load_python
+
+# Attempt to register a missing-debug-handler with NAME.  The expectation is
+# that this should fail as NAME contains some invalid characters.
+proc check_bad_name {name} {
+    set name_re [string_to_regexp $name]
+    set re \
+	[multi_line \
+	     "ValueError.*: invalid character '.' in handler name: $name_re" \
+	     "Error occurred in Python.*"]
+
+    gdb_test "python register(\"$name\")" $re \
+	"check that '$name' is not accepted"
+}
+
+# We don't attempt to be exhaustive here, just check a few random examples
+# of invalid names.
+check_bad_name "!! Bad Name"
+check_bad_name "Bad Name"
+check_bad_name "(Bad Name)"
+check_bad_name "Bad \[Name\]"
+check_bad_name "Bad,Name"
+check_bad_name "Bad;Name"
+
+# Check that there are no handlers registered.
+gdb_test_no_output "info missing-objfile-handlers" \
+    "check no handlers are registered"
+
+# Grab the current program space object, used for registering handler later.
+gdb_test_no_output "python pspace = gdb.selected_inferior().progspace"
+
+# Now register some handlers.
+foreach hspec {{\"Foo\" None}
+    {\"-bar\" None}
+    {\"baz-\" pspace}
+    {\"abc-def\" pspace}} {
+    lassign $hspec name locus
+    gdb_test "python register($name, $locus)"
+}
+
+with_test_prefix "all handlers enabled" {
+    gdb_test "info missing-objfile-handlers" \
+	[multi_line \
+	     "Current Progspace:" \
+	     "  abc-def" \
+	     "  baz-" \
+	     "Global:" \
+	     "  -bar" \
+	     "  Foo"]
+
+    set_debug_file_dir $debugdir_no_lib
+    load_core_file
+
+    # As we perform two look ups, first for the mapped-file then for the
+    # shared library, each handler will be called twice.
+    gdb_test "python print(handler_call_log)" \
+	[string_to_regexp {['abc-def', 'baz-', '-bar', 'Foo', 'abc-def', 'baz-', '-bar', 'Foo']}]
+    gdb_test_no_output "python handler_call_log = \[\]" \
+	"reset call log"
+}
+
+with_test_prefix "disable 'baz-'" {
+    gdb_test "disable missing-objfile-handler progspace baz-" \
+	"^1 missing objfile handler disabled"
+
+    gdb_test "info missing-objfile-handlers" \
+	[multi_line \
+	     "Progspace \[^\r\n\]+:" \
+	     "  abc-def" \
+	     "  baz- \\\[disabled\\\]" \
+	     "Global:" \
+	     "  -bar" \
+	     "  Foo"]
+
+    load_core_file
+    gdb_test "python print(handler_call_log)" \
+	[string_to_regexp {['abc-def', '-bar', 'Foo', 'abc-def', '-bar', 'Foo']}]
+    gdb_test_no_output "python handler_call_log = \[\]" \
+	"reset call log"
+}
+
+with_test_prefix "disable 'Foo'" {
+    gdb_test "disable missing-objfile-handler .* Foo" \
+	"^1 missing objfile handler disabled"
+
+    gdb_test "info missing-objfile-handlers" \
+	[multi_line \
+	     "Progspace \[^\r\n\]+:" \
+	     "  abc-def" \
+	     "  baz- \\\[disabled\\\]" \
+	     "Global:" \
+	     "  -bar" \
+	     "  Foo \\\[disabled\\\]"]
+
+    load_core_file
+    gdb_test "python print(handler_call_log)" \
+	[string_to_regexp {['abc-def', '-bar', 'abc-def', '-bar']}]
+    gdb_test_no_output "python handler_call_log = \[\]" \
+	"reset call log"
+}
+
+with_test_prefix "disable everything" {
+    gdb_test "disable missing-objfile-handler .* .*" \
+	"^2 missing objfile handlers disabled"
+
+    gdb_test "info missing-objfile-handlers" \
+	[multi_line \
+	     "Progspace \[^\r\n\]+:" \
+	     "  abc-def \\\[disabled\\\]" \
+	     "  baz- \\\[disabled\\\]" \
+	     "Global:" \
+	     "  -bar \\\[disabled\\\]" \
+	     "  Foo \\\[disabled\\\]"]
+
+    load_core_file
+    gdb_test "python print(handler_call_log)" \
+	[string_to_regexp {[]}]
+    gdb_test_no_output "python handler_call_log = \[\]" \
+	"reset call log"
+}
+
+with_test_prefix "enable 'abc-def'" {
+    set re [string_to_regexp $hidden_binfile]
+
+    gdb_test "enable missing-objfile-handler \"$re\" abc-def" \
+	"^1 missing objfile handler enabled" \
+	"enable missing-objfile-handler"
+
+    gdb_test "info missing-objfile-handlers" \
+	[multi_line \
+	     "Progspace \[^\r\n\]+:" \
+	     "  abc-def" \
+	     "  baz- \\\[disabled\\\]" \
+	     "Global:" \
+	     "  -bar \\\[disabled\\\]" \
+	     "  Foo \\\[disabled\\\]"]
+
+    load_core_file
+    gdb_test "python print(handler_call_log)" \
+	[string_to_regexp {['abc-def', 'abc-def']}]
+    gdb_test_no_output "python handler_call_log = \[\]" \
+	"reset call log"
+}
+
+with_test_prefix "enable global handlers" {
+    gdb_test "enable missing-objfile-handler global" \
+	"^2 missing objfile handlers enabled"
+
+    gdb_test "info missing-objfile-handlers" \
+	[multi_line \
+	     "Progspace \[^\r\n\]+:" \
+	     "  abc-def" \
+	     "  baz- \\\[disabled\\\]" \
+	     "Global:" \
+	     "  -bar" \
+	     "  Foo"]
+
+    load_core_file
+    gdb_test "python print(handler_call_log)" \
+	[string_to_regexp {['abc-def', '-bar', 'Foo', 'abc-def', '-bar', 'Foo']}]
+    gdb_test_no_output "python handler_call_log = \[\]" \
+	"reset call log"
+}
+
+# Add handler_obj to the global handler list, and configure it to
+# return False.  We should call all of the program space specific
+# handlers (which return None), and then call handler_obj from the
+# global list, which returns False, at which point we shouldn't call
+# anyone else.
+with_test_prefix "return False handler in global list" {
+    gdb_test "enable missing-objfile-handler progspace" \
+	"^1 missing objfile handler enabled"
+
+    gdb_test_no_output \
+	"python gdb.missing_objfile.register_handler(None, handler_obj)" \
+	"register handler_obj in global list"
+
+    gdb_test "info missing-objfile-handlers" \
+	[multi_line \
+	     "Progspace \[^\r\n\]+:" \
+	     "  abc-def" \
+	     "  baz-" \
+	     "Global:" \
+	     "  handler" \
+	     "  -bar" \
+	     "  Foo"]
+
+    gdb_test_no_output "python handler_obj.set_mode(Mode.RETURN_FALSE)" \
+	"confirgure handler"
+
+    load_core_file
+    gdb_test "python print(handler_call_log)" \
+	[string_to_regexp {['abc-def', 'baz-', 'handler', 'abc-def', 'baz-', 'handler']}]
+    gdb_test_no_output "python handler_call_log = \[\]" \
+	"reset call log"
+}
+
+# Now add handler_obj to the current program space's handler list.  We
+# use the same handler object here, that's fine.  We should only see a
+# call to the first handler object in the call log.
+with_test_prefix "return False handler in progspace list" {
+    gdb_test_no_output \
+	"python gdb.missing_objfile.register_handler(pspace, handler_obj)" \
+	"register handler_obj in progspace list"
+
+    gdb_test "info missing-objfile-handlers" \
+	[multi_line \
+	     "Progspace \[^\r\n\]+:" \
+	     "  handler" \
+	     "  abc-def" \
+	     "  baz-" \
+	     "Global:" \
+	     "  handler" \
+	     "  -bar" \
+	     "  Foo"]
+
+    load_core_file
+    gdb_test "python print(handler_call_log)" \
+	[string_to_regexp {['handler', 'handler']}]
+    gdb_test_no_output "python handler_call_log = \[\]" \
+	"reset call log"
+}
+
+with_test_prefix "check handler replacement" {
+    # First, check we can have the same name appear in both program
+    # space and global lists without giving an error.
+    gdb_test_no_output "python register(\"Foo\", pspace)"
+
+    gdb_test "info missing-objfile-handlers" \
+	[multi_line \
+	     "Progspace \[^\r\n\]+:" \
+	     "  Foo" \
+	     "  handler" \
+	     "  abc-def" \
+	     "  baz-" \
+	     "Global:" \
+	     "  handler" \
+	     "  -bar" \
+	     "  Foo"]
+
+    # Now check that we get an error if we try to add a handler with
+    # the same name.
+    gdb_test "python gdb.missing_objfile.register_handler(pspace, log_handler(\"Foo\"))" \
+	[multi_line \
+	     "RuntimeError.*: Handler Foo already exists\\." \
+	     "Error occurred in Python.*"]
+
+    gdb_test "python gdb.missing_objfile.register_handler(handler=log_handler(\"Foo\"), locus=pspace)" \
+	[multi_line \
+	     "RuntimeError.*: Handler Foo already exists\\." \
+	     "Error occurred in Python.*"]
+
+    # And now try again, but this time with 'replace=True', we
+    # shouldn't get an error in this case.
+    gdb_test_no_output \
+	"python gdb.missing_objfile.register_handler(pspace, log_handler(\"Foo\"), replace=True)"
+
+    gdb_test_no_output \
+	"python gdb.missing_objfile.register_handler(handler=log_handler(\"Foo\"), locus=None, replace=True)"
+
+    # Now disable a handler and check we still need to use 'replace=True'.
+    gdb_test "disable missing-objfile-handler progspace Foo" \
+	"^1 missing objfile handler disabled"
+
+    gdb_test "python gdb.missing_objfile.register_handler(pspace, log_handler(\"Foo\"))" \
+	[multi_line \
+	     "RuntimeError.*: Handler Foo already exists\\." \
+	     "Error occurred in Python.*"] \
+	"still get an error when handler is disabled"
+
+    gdb_test_no_output \
+	"python gdb.missing_objfile.register_handler(pspace, log_handler(\"Foo\"), replace=True)" \
+	"can replace a disabled handler"
+}
diff --git a/gdb/testsuite/gdb.python/py-missing-objfile.py b/gdb/testsuite/gdb.python/py-missing-objfile.py
new file mode 100644
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-missing-objfile.py
@@ -0,0 +1,167 @@
+# Copyright (C) 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/>.
+
+import shutil
+import os
+from enum import Enum
+
+import gdb
+from gdb.missing_objfile import MissingObjfileHandler
+
+# A global log that is filled in by instances of the LOG_HANDLER class
+# when they are called.
+handler_call_log = []
+
+# A global holding a string, the build-id of the last missing objfile
+# which triggered the 'handler' class below.  This is set in the
+# __call__ method of the 'handler' class and then checked from the
+# expect script.
+handler_last_buildid = None
+
+
+# A global holding a string, the filename of the last missing objfile
+# which triggered the 'handler' class below.  This is set in the
+# __call__ method of the 'handler' class and then checked from the
+# expect script.
+handler_last_filename = None
+
+
+# A helper function that makes some assertions about the arguments
+# passed to a MissingObjfileHandler.__call__() method.
+def check_args(pspace, buildid, filename):
+    assert type(filename) == str
+    assert filename != ""
+    assert type(pspace) == gdb.Progspace
+    assert type(buildid) == str
+    assert buildid != ""
+
+
+# Enum used to configure the 'handler' class from the test script.
+class Mode(Enum):
+    RETURN_NONE = 0
+    RETURN_TRUE = 1
+    RETURN_FALSE = 2
+    RETURN_STRING = 3
+
+
+# A missing objfile handler which can be configured to return each of
+# the different possible return types.
+class handler(MissingObjfileHandler):
+    def __init__(self):
+        super().__init__("handler")
+        self._call_count = 0
+        self._mode = Mode.RETURN_NONE
+
+    def __call__(self, pspace, buildid, filename):
+        global handler_call_log, handler_last_buildid, handler_last_filename
+        check_args(pspace, buildid, filename)
+        handler_call_log.append(self.name)
+        handler_last_buildid = buildid
+        handler_last_filename = filename
+        self._call_count += 1
+        if self._mode == Mode.RETURN_NONE:
+            return None
+
+        if self._mode == Mode.RETURN_TRUE:
+            shutil.copy(self._src, self._dest)
+
+            # If we're using the fission-dwp board then there will
+            # also be a .dwp file that needs to be copied.
+            dwp_src = self._src + ".dwp"
+            if os.path.exists(dwp_src):
+                dwp_dest = self._dest + ".dwp"
+                shutil.copy(dwp_src, dwp_dest)
+
+            return True
+
+        if self._mode == Mode.RETURN_FALSE:
+            return False
+
+        if self._mode == Mode.RETURN_STRING:
+            return self._dest
+
+        assert False
+
+    @property
+    def call_count(self):
+        """Return a count, the number of calls to __call__ since the last
+        call to set_mode.
+        """
+        return self._call_count
+
+    def set_mode(self, mode, *args):
+        self._call_count = 0
+        self._mode = mode
+
+        if mode == Mode.RETURN_NONE:
+            assert len(args) == 0
+            return
+
+        if mode == Mode.RETURN_TRUE:
+            assert len(args) == 2
+            self._src = args[0]
+            self._dest = args[1]
+            return
+
+        if mode == Mode.RETURN_FALSE:
+            assert len(args) == 0
+            return
+
+        if mode == Mode.RETURN_STRING:
+            assert len(args) == 1
+            self._dest = args[0]
+            return
+
+        assert False
+
+
+# A missing objfile handler which raises an exception.  The type of
+# exception to be raised is configured from the test script.
+class exception_handler(MissingObjfileHandler):
+    def __init__(self):
+        super().__init__("exception_handler")
+        self.exception_type = None
+
+    def __call__(self, pspace, buildid, filename):
+        global handler_call_log
+        check_args(pspace, buildid, filename)
+        handler_call_log.append(self.name)
+        assert self.exception_type is not None
+        raise self.exception_type("message")
+
+
+# A very simple logging missing objfile handler.  Always returns None
+# so that GDB will try any other registered handlers, but first logs
+# the name of this handler into the global HANDLER_CALL_LOG, which can
+# then be checked from the test script.
+class log_handler(MissingObjfileHandler):
+    def __call__(self, pspace, buildid, filename):
+        global handler_call_log
+        check_args(pspace, buildid, filename)
+        handler_call_log.append(self.name)
+        return None
+
+
+# A basic helper function, this keeps lines shorter in the TCL script.
+def register(name, locus=None):
+    gdb.missing_objfile.register_handler(locus, log_handler(name))
+
+
+# Create instances of the handlers, but don't install any.  We install
+# these as needed from the TCL script.
+rhandler = exception_handler()
+handler_obj = handler()
+
+print("Success")
diff --git a/gdb/testsuite/gdb.server/build-id-seqno.c b/gdb/testsuite/gdb.server/build-id-seqno.c
new file mode 100644
--- /dev/null
+++ b/gdb/testsuite/gdb.server/build-id-seqno.c
@@ -0,0 +1,22 @@
+/* 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/>.  */
+
+int
+main (void)
+{
+  return 0;
+}
diff --git a/gdb/testsuite/gdb.server/build-id-seqno.exp b/gdb/testsuite/gdb.server/build-id-seqno.exp
new file mode 100644
--- /dev/null
+++ b/gdb/testsuite/gdb.server/build-id-seqno.exp
@@ -0,0 +1,198 @@
+# 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/>.
+
+# Setup a .build-id/ based debug directory containing multiple entries
+# for the same build-id, with each entry given a different sequence
+# number.
+#
+# Ensure that GDB will scan over broken symlinks for the same build-id
+# (but different sequence number) to find later working symlinks.
+#
+# This test places the build-id files within a directory next to where
+# gdbserver is started, and places a relative address in the
+# debug-file-directory, in this way we require GDB to find the debug
+# information via gdbserver.
+
+require {!is_remote host}
+
+load_lib gdbserver-support.exp
+
+standard_testfile
+
+if {[build_executable "failed to prepare" $testfile $srcfile] == -1} {
+    return -1
+}
+
+# Split out BINFILE.debug.  Remove debug from BINFILE.
+if {[gdb_gnu_strip_debug $binfile] != 0} {
+    return -1
+}
+
+# Get the '.build-id/xx/xxx...xxx' part of the filename.
+set build_id_filename [build_id_debug_filename_get $binfile]
+
+# Hide (rename) BINFILE.debug, this should ensure GDB can't find it
+# directly but needs to look for the build-id based file in the debug
+# directory.
+set hidden_debuginfo [standard_output_file "hidden_$testfile.debug"]
+remote_exec build "mv ${binfile}.debug $hidden_debuginfo"
+
+# A filename that doesn't exist.  Some symlinks will point at this
+# file.
+set missing_debuginfo "missing_debuginfo"
+
+# Helper called from gdb_finish when the 'target' is remote.  Ensure the
+# debug directory we create is deleted.
+proc cleanup_remote_target {} {
+    remote_exec target "rm -fr debug/"
+}
+
+if { ![is_remote target] } {
+    set gdbserver_dir [standard_output_file "gdbserver-dir"]/
+} else {
+    lappend gdb_finish_hooks cleanup_remote_target
+    set gdbserver_dir ""
+}
+
+# Copy files to the target (if needed).
+set target_binfile [gdb_remote_download target $binfile]
+set target_debuginfo [gdb_remote_download target $hidden_debuginfo]
+
+# Setup the debug information on the target.
+set debugdir "${gdbserver_dir}debug"
+remote_exec target \
+    "mkdir -p $debugdir/[file dirname $build_id_filename]"
+remote_exec target \
+    "ln -sf $target_debuginfo $debugdir/$build_id_filename"
+
+# Start GDB and load global BINFILE.  If DEBUGINFO_FILE is not the
+# empty string then this contains the '.build-id/xx/xxx....xxxx' part
+# of the filename which we expect GDB to read from the remote target.
+# If DEBUGINFO_FILE is the empty string then we don't expect GDB to
+# find any debug information.
+proc load_binfile_check_debug_is_found { debuginfo_file testname } {
+    with_test_prefix "$testname" {
+	with_timeout_factor 5 {
+	    # Probing for .build-id based debug files on remote
+	    # targets uses the vFile:stat packet by default, though
+	    # there is a work around that avoids this which can be
+	    # used if GDB is connected to an older gdbserver without
+	    # 'stat' support.
+	    #
+	    # Check the work around works by disabling use of the
+	    # vFile:stat packet.
+	    foreach_with_prefix stat_pkt {auto off} {
+		clean_restart
+
+		gdb_test_no_output "set debug-file-directory debug" \
+		    "set debug-file-directory"
+
+		gdb_test_no_output "set sysroot target:"
+
+		gdb_test "set remote hostio-stat-packet $stat_pkt"
+
+		# Make sure we're disconnected, in case we're testing with an
+		# extended-remote board, therefore already connected.
+		gdb_test "disconnect" ".*"
+
+		# Start gdbserver.  This needs to be done after starting GDB.  When
+		# gdbserver is running local to GDB, start gdbserver in a sub-directory,
+		# this prevents GDB from finding the debug information itself.
+		if { ![is_remote target] } {
+		    with_cwd $::gdbserver_dir {
+			set res [gdbserver_start "" $::target_binfile]
+		    }
+		} else {
+		    set res [gdbserver_start "" $::target_binfile]
+		}
+		set gdbserver_protocol [lindex $res 0]
+		set gdbserver_gdbport [lindex $res 1]
+
+		# Connect to gdbserver.  The output will be placed into the global
+		# GDB_TARGET_REMOTE_CMD_MSG, and we'll match against this below.
+		gdb_assert {[gdb_target_cmd $gdbserver_protocol $gdbserver_gdbport] == 0} \
+		    "connect to gdbserver"
+
+		if { $debuginfo_file ne "" } {
+		    gdb_assert { [regexp "Reading symbols from target:debug/[string_to_regexp $debuginfo_file]\\.\\.\\." \
+				      $::gdb_target_remote_cmd_msg] } \
+			"debuginfo was read via build-id"
+		    gdb_assert { [regexp "Reading debug/[string_to_regexp $debuginfo_file] from remote target\\.\\.\\." \
+				      $::gdb_target_remote_cmd_msg] } \
+			"debuginfo was read from remote target"
+		} else {
+		    gdb_assert { [regexp "\\(No debugging symbols found in \[^\r\n\]+/$::testfile\\)" \
+				      $::gdb_target_remote_cmd_msg] }
+		}
+	    }
+	}
+    }
+}
+
+# Return a copy of FILENAME, which should end '.debug', with NUMBER
+# added, e.g. add_seqno 1 "foo.debug" --> "foo.1.debug".
+proc add_seqno { number filename } {
+    return [regsub "\.debug\$" $filename ".${number}.debug"]
+}
+
+# Precompute sequence numbered build-id filenames.
+set build_id_1_filename [add_seqno 1 $build_id_filename]
+set build_id_2_filename [add_seqno 2 $build_id_filename]
+set build_id_3_filename [add_seqno 3 $build_id_filename]
+
+load_binfile_check_debug_is_found $build_id_filename \
+    "find debuginfo with a single build-id file"
+
+remote_exec target "ln -fs $target_debuginfo \
+			$debugdir/$build_id_1_filename"
+remote_exec target "ln -fs $target_debuginfo \
+			$debugdir/$build_id_2_filename"
+remote_exec target "ln -fs $target_debuginfo \
+			$debugdir/$build_id_3_filename"
+
+load_binfile_check_debug_is_found $build_id_filename \
+    "find debuginfo with 4 build-id files"
+
+remote_exec target "ln -fs $missing_debuginfo $debugdir/$build_id_filename"
+
+load_binfile_check_debug_is_found $build_id_1_filename \
+    "find debuginfo, first build-id file is bad"
+
+remote_exec target "ln -fs $missing_debuginfo \
+			$debugdir/$build_id_1_filename"
+remote_exec target "ln -fs $missing_debuginfo \
+			$debugdir/$build_id_3_filename"
+
+load_binfile_check_debug_is_found $build_id_2_filename  \
+    "find debuginfo, first 2 build-id files are bad"
+
+remote_exec target "ln -fs $missing_debuginfo \
+			$debugdir/$build_id_2_filename"
+
+load_binfile_check_debug_is_found ""  \
+    "cannot find debuginfo, all build-id files are bad"
+
+remote_exec target "ln -fs $target_debuginfo \
+			$debugdir/$build_id_3_filename"
+
+load_binfile_check_debug_is_found $build_id_3_filename  \
+    "find debuginfo, last build-id file is good"
+
+remote_exec target "rm -f $debugdir/$build_id_1_filename"
+
+load_binfile_check_debug_is_found ""  \
+    "cannot find debuginfo, file with seqno 1 is missing"
diff --git a/gdb/testsuite/lib/dwarf.exp b/gdb/testsuite/lib/dwarf.exp
--- a/gdb/testsuite/lib/dwarf.exp
+++ b/gdb/testsuite/lib/dwarf.exp
@@ -2993,25 +2993,32 @@ namespace eval Dwarf {
 
     proc _note {type name hexdata} {
 	set namelen [expr [string length $name] + 1]
+	set datalen [expr [string length $hexdata] / 2]
 
 	# Name size.
 	_op .4byte $namelen
 	# Data size.
-	_op .4byte [expr [string length $hexdata] / 2]
+	_op .4byte $datalen
 	# Type.
 	_op .4byte $type
 	# The name.
 	_op .ascii [_quote $name]
-	# Alignment.
+	# Alignment (to 4-byte boundary).
 	set align 2
 	set total [expr {($namelen + (1 << $align) - 1) & -(1 << $align)}]
 	for {set i $namelen} {$i < $total} {incr i} {
-	    _op .byte 0
+	    _op .byte 0 padding
 	}
 	# The data.
 	foreach {a b} [split $hexdata {}] {
 	    _op .byte 0x$a$b
 	}
+	# Alignment (to 4-byte boundary).
+	set align 2
+	set total [expr {($datalen + (1 << $align) - 1) & -(1 << $align)}]
+	for {set i $datalen} {$i < $total} {incr i} {
+	    _op .byte 0 padding
+	}
     }
 
     # Emit a note section holding the given build-id.
diff --git a/gdb/testsuite/lib/gdb.exp b/gdb/testsuite/lib/gdb.exp
--- a/gdb/testsuite/lib/gdb.exp
+++ b/gdb/testsuite/lib/gdb.exp
@@ -5337,6 +5337,7 @@ proc quote_for_host { args } {
 #     debug information
 #   - text_segment=addr: Tell the linker to place the text segment at ADDR.
 #   - build-id: Ensure the final binary includes a build-id.
+#   - no-build-id: Ensure the final binary does not include a build-id.
 #   - column-info/no-column-info: Enable/Disable generation of column table
 #     information.
 #
@@ -5448,6 +5449,18 @@ proc gdb_compile {source dest type options} {
 	lappend new_options "additional_flags=-Wl,--build-id"
     }
 
+    # If the 'no-build-id' option is used then disable the build-id.
+    if {[lsearch -exact $options no-build-id] > 0} {
+	lappend new_options "additional_flags=-Wl,--build-id=none"
+    }
+
+    # Sanity check.  If both 'build-id' and 'no-build-id' are used
+    # then what is expected from us!
+    if {[lsearch -exact $options build-id] > 0
+	&& [lsearch -exact $options no-build-id] > 0} {
+	error "cannot use build-id and no-build-id options"
+    }
+
     # Treating .c input files as C++ is deprecated in Clang, so
     # explicitly force C++ language.
     if { !$getting_compiler_info
@@ -8144,21 +8157,25 @@ proc get_build_id { filename } {
 
 # Return the build-id hex string (usually 160 bits as 40 hex characters)
 # converted to the form: .build-id/ab/cdef1234...89.debug
+#
+# The '.debug' suffix can be changed by passing the SUFFIX argument.
+#
 # Return "" if no build-id found.
-proc build_id_debug_filename_get { filename } {
+proc build_id_debug_filename_get { filename {suffix ".debug"} } {
     set data [get_build_id $filename]
     if { $data == "" } {
 	return ""
     }
     regsub {^..} $data {\0/} data
-    return ".build-id/${data}.debug"
+    return ".build-id/${data}${suffix}"
 }
 
 # DEST should be a file compiled with debug information.  This proc
-# creates two new files DEST.debug which contains the debug
-# information extracted from DEST, and DEST.stripped, which is a copy
-# of DEST with the debug information removed.  A '.gnu_debuglink'
-# section will be added to DEST.stripped that points to DEST.debug.
+# creates DEST.debug which contains the debug information extracted
+# from DEST, and DEST is updated with the debug information removed.
+#
+# By default a '.gnu_debuglink' section will be added to DEST that
+# points to DEST.debug.
 #
 # If ARGS is passed, it is a list of optional flags.  The currently
 # supported flags are:
@@ -8166,7 +8183,7 @@ proc build_id_debug_filename_get { filename } {
 #   - no-main : remove the symbol entry for main from the separate
 #               debug file DEST.debug,
 #   - no-debuglink : don't add the '.gnu_debuglink' section to
-#                    DEST.stripped.
+#                    DEST.
 #
 # Function returns zero on success.  Function will return non-zero failure code
 # on some targets not supporting separate debug info (such as i386-msdos).
@@ -8225,20 +8242,26 @@ proc gdb_gnu_strip_debug { dest args } {
     # Unless the "no-debuglink" flag is passed, then link the two
     # previous output files together, adding the .gnu_debuglink
     # section to the stripped_file, containing a pointer to the
-    # debug_file, save the new file in dest.
+    # debug_file.
     if {[lsearch -exact $args "no-debuglink"] == -1} {
-	set result [catch "exec $objcopy_program --add-gnu-debuglink=${debug_file} ${stripped_file} ${dest}" output]
+	set result [catch "exec $objcopy_program --add-gnu-debuglink=${debug_file} ${stripped_file} ${stripped_file}-tmp" output]
 	verbose "result is $result"
 	verbose "output is $output"
 	if {$result == 1} {
 	    return 1
 	}
+	file delete "${stripped_file}"
+	file rename "${stripped_file}-tmp" "${stripped_file}"
     }
 
     # Workaround PR binutils/10802:
     # Preserve the 'x' bit also for PIEs (Position Independent Executables).
-    set perm [file attributes ${stripped_file} -permissions]
-    file attributes ${dest} -permissions $perm
+    set perm [file attributes ${dest} -permissions]
+    file attributes ${stripped_file} -permissions $perm
+
+    # Move the stripped_file back into dest.
+    file delete ${dest}
+    file rename ${stripped_file} ${dest}
 
     return 0
 }
diff --git a/gdbserver/hostio.cc b/gdbserver/hostio.cc
--- a/gdbserver/hostio.cc
+++ b/gdbserver/hostio.cc
@@ -486,6 +486,42 @@ handle_fstat (char *own_buf, int *new_packet_len)
     write_enn (own_buf);
 }
 
+static void
+handle_stat (char *own_buf, int *new_packet_len)
+{
+  int bytes_sent;
+  char *p;
+  struct stat st;
+  struct fio_stat fst;
+  char filename[HOSTIO_PATH_MAX];
+
+  p = own_buf + strlen ("vFile:stat:");
+
+  if (require_filename (&p, filename)
+      || require_end (p))
+    {
+      hostio_packet_error (own_buf);
+      return;
+    }
+
+  if (lstat (filename, &st) == -1)
+    {
+      hostio_error (own_buf);
+      return;
+    }
+
+  host_to_fileio_stat (&st, &fst);
+
+  bytes_sent = hostio_reply_with_data (own_buf,
+				       (char *) &fst, sizeof (fst),
+				       new_packet_len);
+
+  /* If the response does not fit into a single packet, do not attempt
+     to return a partial response, but simply fail.  */
+  if (bytes_sent < sizeof (fst))
+    write_enn (own_buf);
+}
+
 static void
 handle_close (char *own_buf)
 {
@@ -603,6 +639,8 @@ handle_vFile (char *own_buf, int packet_len, int *new_packet_len)
     handle_pwrite (own_buf, packet_len);
   else if (startswith (own_buf, "vFile:fstat:"))
     handle_fstat (own_buf, new_packet_len);
+  else if (startswith (own_buf, "vFile:stat:"))
+    handle_stat (own_buf, new_packet_len);
   else if (startswith (own_buf, "vFile:close:"))
     handle_close (own_buf);
   else if (startswith (own_buf, "vFile:unlink:"))
diff --git a/gdbsupport/pathstuff.cc b/gdbsupport/pathstuff.cc
--- a/gdbsupport/pathstuff.cc
+++ b/gdbsupport/pathstuff.cc
@@ -198,11 +198,17 @@ path_join (gdb::array_view<const char *> paths)
     {
       const char *path = paths[i];
 
-      if (i > 0)
-	gdb_assert (strlen (path) == 0 || !IS_ABSOLUTE_PATH (path));
-
-      if (!ret.empty () && !IS_DIR_SEPARATOR (ret.back ()))
-	  ret += '/';
+      if (!ret.empty ())
+	{
+	  /* If RET doesn't already end with a separator then add one.  */
+	  if (!IS_DIR_SEPARATOR (ret.back ()))
+	    ret += '/';
+
+	  /* Now that RET ends with a separator, ignore any at the start of
+	     PATH.  */
+	  while (IS_DIR_SEPARATOR (path[0]))
+	    ++path;
+	}
 
       ret.append (path);
     }
diff --git a/gdbsupport/pathstuff.h b/gdbsupport/pathstuff.h
--- a/gdbsupport/pathstuff.h
+++ b/gdbsupport/pathstuff.h
@@ -64,8 +64,10 @@ extern const char *child_path (const char *parent, const char *child);
 
 /* Join elements in PATHS into a single path.
 
-   The first element can be absolute or relative.  All the others must be
-   relative.  */
+   The first element can be absolute or relative.  Only a single directory
+   separator will be placed between elements of PATHS, if one element ends
+   with a directory separator, or an element starts with a directory
+   separator, then these will be collapsed into a single separator.  */
 
 extern std::string path_join (gdb::array_view<const char *> paths);
 
