public inbox for gdb-patches@sourceware.org
 help / color / mirror / Atom feed
* [PATCH 0/4] Python API to find missing objfiles
@ 2024-09-10 16:56 Andrew Burgess
  2024-09-10 16:56 ` [PATCH 1/4] gdb: use mapped file information to improve debuginfod text Andrew Burgess
                   ` (4 more replies)
  0 siblings, 5 replies; 24+ messages in thread
From: Andrew Burgess @ 2024-09-10 16:56 UTC (permalink / raw)
  To: gdb-patches; +Cc: Andrew Burgess

This series provides a Python API which can be used to find missing
objfiles when opening a core file.  This is very similar in spirit to
the already existing code that provides a Python API for finding
missing debug information.

The first patch in this series cleans up the message that debuginfod
produces.  This is only relevant because the cleanup changes which
filenames are passed around, these same filenames are then passed into
the new Python API.

Patch #2 is a refactoring.

Patch #3 adds the new extension language API on the GDB core side.

Patch #4 adds the Python side of the same API.

For reference, see these commits for when the missing debug files
Python API was added:

  * 8f6c452b5a4 gdb: implement missing debug handler hook for Python
  * 661d98a3331 gdb: add an extension language hook for missing debug info

Thanks,
Andrew

---

Andrew Burgess (4):
  gdb: use mapped file information to improve debuginfod text
  gdb: rename ext_lang_missing_debuginfo_result
  gdb: add extension hook ext_lang_find_objfile_from_buildid
  gdb/python: implement Python find_exec_by_build_id hook

 gdb/NEWS                                      |  24 +
 gdb/build-id.c                                |  88 ++-
 gdb/build-id.h                                |   3 +-
 gdb/corelow.c                                 |  63 +-
 gdb/data-directory/Makefile.in                |   4 +-
 gdb/debuginfod-support.c                      |   4 +-
 gdb/doc/gdb.texinfo                           |   1 +
 gdb/doc/python.texi                           | 194 ++++++-
 gdb/extension-priv.h                          |  15 +-
 gdb/extension.c                               |  26 +-
 gdb/extension.h                               |  30 +-
 gdb/python/lib/gdb/__init__.py                | 110 +++-
 .../{missing_debug.py => missing_files.py}    | 135 +++--
 gdb/python/lib/gdb/missing_debug.py           | 161 +-----
 gdb/python/lib/gdb/missing_files.py           | 204 +++++++
 gdb/python/lib/gdb/missing_objfile.py         |  67 +++
 gdb/python/py-progspace.c                     |  26 +-
 gdb/python/python.c                           | 120 +++-
 gdb/solib.c                                   |   3 +-
 gdb/symfile-debug.c                           |   2 +-
 .../gdb.debuginfod/corefile-mapped-file.exp   |   2 +-
 .../gdb.debuginfod/solib-with-soname.exp      |   2 +-
 .../gdb.python/py-missing-objfile-lib.c       |  35 ++
 gdb/testsuite/gdb.python/py-missing-objfile.c |  49 ++
 .../gdb.python/py-missing-objfile.exp         | 544 ++++++++++++++++++
 .../gdb.python/py-missing-objfile.py          | 158 +++++
 26 files changed, 1787 insertions(+), 283 deletions(-)
 rename gdb/python/lib/gdb/command/{missing_debug.py => missing_files.py} (54%)
 create mode 100644 gdb/python/lib/gdb/missing_files.py
 create mode 100644 gdb/python/lib/gdb/missing_objfile.py
 create mode 100644 gdb/testsuite/gdb.python/py-missing-objfile-lib.c
 create mode 100644 gdb/testsuite/gdb.python/py-missing-objfile.c
 create mode 100644 gdb/testsuite/gdb.python/py-missing-objfile.exp
 create mode 100644 gdb/testsuite/gdb.python/py-missing-objfile.py


base-commit: dbddec2a8e0265b77fe331d9c173a4e99d1fccab
-- 
2.25.4


^ permalink raw reply	[flat|nested] 24+ messages in thread

* [PATCH 1/4] gdb: use mapped file information to improve debuginfod text
  2024-09-10 16:56 [PATCH 0/4] Python API to find missing objfiles Andrew Burgess
@ 2024-09-10 16:56 ` Andrew Burgess
  2024-10-01 17:24   ` Tom Tromey
  2024-09-10 16:56 ` [PATCH 2/4] gdb: rename ext_lang_missing_debuginfo_result Andrew Burgess
                   ` (3 subsequent siblings)
  4 siblings, 1 reply; 24+ messages in thread
From: Andrew Burgess @ 2024-09-10 16:56 UTC (permalink / raw)
  To: gdb-patches; +Cc: Andrew Burgess

When opening a core-file GDB is able to use debuginfod to download the
executable that matches the core-file if GDB can find a build-id for
the executable in the core-file.

In this case GDB calls debuginfod_exec_query to download the
executable and GDB prints a message like:

  Downloading executable for /path/to/core-file...

which makes sense in that case.

For a long time GDB has also had the ability to download memory-mapped
files and shared libraries when opening a core-file.  However, recent
commits have made these cases more likely to trigger, which is a good
thing, but the messaging from GDB in these cases is not ideal.  When
downloading a memory-mapped file GDB prints:

  Downloading executable for /path/to/memory-mapped-file

And for a shared library:

  Downloading executable for /path/to/libfoo.so

These last two messages could, I think, be improved.

I propose making two changes.  First, I suggest instead of using
/path/to/core-file in the first case, we use the name of the
executable that GDB is fetching.  This makes the messaging consistent
in that we print the name of the file we're fetching rather than the
name of the file we're fetching something for.

I further propose that we replace 'executable for' with the more
generic word 'file'.  The messages will then become:

  Downloading file /path/to/exec-file...
  Downloading file /path/to/memory-mapped-file...
  Downloading file /path/to/libfoo.so...

I think these messages are clearer than what we used to have, and they
are consistent in that we name the thing being downloaded in all
cases.

There is one tiny problem.  The first case relies on GDB knowing the
name of the executable it wants to download.  The only place we can
currently get that from is, I think, the memory-mapped file list.

[ ASIDE: There is `bfd_core_file_failing_command` which reports the
  executable and argument list from the core file, but this
  information is not ideal for this task.  First, the executable and
  arguments are merged into a single string, and second, the string is
  a relatively short, fixed length string, so the executable name is
  often truncated.  For these reasons I don't consider fetching the
  executable name using this bfd function as a solution. ]

We do have to consider the case that the core file does not have any
mapped file information.  This shouldn't ever be the case for a Linux
target, but it's worth considering.

[ ASIDE: I mention Linux specifically because this only becomes a
  problem if we try to do a lookup via debuginfod, which requires that
  we have build-ids available.  Linux has special support for
  embedding build-ids into the core file, but I'm not sure if other
  kernels do this. ]

For the unlikely edge case of a core-file that has build-ids, but
doesn't have any mapped file information then I propose that we
synthesis a filename like: 'with build-id xxxxxx'.  We would then see
a message like:

  Downloading file with build-id xxxxxx...

Where 'xxxxxx' would be replaced by the actual build-id.

This isn't ideal, but I think is good enough, and, as I said, I think
this case is not going to be hit very often, or maybe at all.

We already had some tests that emitted two of the above messages,
which I've updated, these cover the mapped-file and shared library
case.

The message about downloading the exec for the core-file is actually
really hard to trigger now as usually the exec will also appear in the
memory-mapped file list and GDB will download the file at this stage.
Then when GDB needs the executable for loading the symbols it'll ask
debuginfod, and debuginfod will find the file in its cache, and so no
message will be printed.

If anyone has any ideas about how to trigger this case then I'm happy
to add additional tests.
---
 gdb/corelow.c                                 | 59 ++++++++++++++++++-
 gdb/debuginfod-support.c                      |  4 +-
 .../gdb.debuginfod/corefile-mapped-file.exp   |  2 +-
 .../gdb.debuginfod/solib-with-soname.exp      |  2 +-
 4 files changed, 60 insertions(+), 7 deletions(-)

diff --git a/gdb/corelow.c b/gdb/corelow.c
index ab2fa746de8..d8a2c3f8bc4 100644
--- a/gdb/corelow.c
+++ b/gdb/corelow.c
@@ -255,6 +255,27 @@ class core_target final : public process_stratum_target
     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.  */
@@ -289,6 +310,11 @@ class core_target final : public process_stratum_target
   /* 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 ()
@@ -418,11 +444,23 @@ core_target::build_file_mappings ()
 	  }
       });
 
+  /* Get the build-id of the core file.  */
+  const bfd_build_id *core_build_id
+    = build_id_bfd_get (current_program_space->core_bfd ());
+
   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
@@ -832,14 +870,29 @@ 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;
 
+  /* The filename used for the find_objfile_by_build_id call.  */
+  std::string filename;
+
+  if (!target->expected_exec_filename ().empty ())
+    filename = target->expected_exec_filename ();
+  else
+    {
+      /* 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 (build_id, abfd->filename);
+    = find_objfile_by_build_id (build_id, filename.c_str ());
 
   if (execbfd != nullptr)
     {
@@ -972,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);
 
diff --git a/gdb/debuginfod-support.c b/gdb/debuginfod-support.c
index 841b6f2078c..9460ae18dd6 100644
--- 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/testsuite/gdb.debuginfod/corefile-mapped-file.exp b/gdb/testsuite/gdb.debuginfod/corefile-mapped-file.exp
index b5dee228ca0..e358e426d32 100644
--- a/gdb/testsuite/gdb.debuginfod/corefile-mapped-file.exp
+++ b/gdb/testsuite/gdb.debuginfod/corefile-mapped-file.exp
@@ -370,7 +370,7 @@ with_debuginfod_env $cache {
     gdb_load $binfile
 
     load_core_file "load corefile, download library from debuginfod" \
-	"Downloading\[^\r\n\]* executable for [string_to_regexp $library_filename]\\.\\.\\."
+	"Downloading\[^\r\n\]* file [string_to_regexp $library_filename]\\.\\.\\."
 
     set ptr_value [read_ptr_value]
     gdb_assert { $ptr_value == $ptr_expected_value } \
diff --git a/gdb/testsuite/gdb.debuginfod/solib-with-soname.exp b/gdb/testsuite/gdb.debuginfod/solib-with-soname.exp
index 9ef12041dc6..31ca7181af6 100644
--- a/gdb/testsuite/gdb.debuginfod/solib-with-soname.exp
+++ b/gdb/testsuite/gdb.debuginfod/solib-with-soname.exp
@@ -161,7 +161,7 @@ proc load_exec_and_core_file { expect_warning expect_download testname \
 		set saw_warning true
 		exp_continue
 	    }
-	    -re "^Downloading executable for \[^\r\n\]+/libfoo_1\\.so\\.\\.\\.\r\n" {
+	    -re "^Downloading file \[^\r\n\]+/libfoo_1\\.so\\.\\.\\.\r\n" {
 		set saw_download true
 		exp_continue
 	    }
-- 
2.25.4


^ permalink raw reply	[flat|nested] 24+ messages in thread

* [PATCH 2/4] gdb: rename ext_lang_missing_debuginfo_result
  2024-09-10 16:56 [PATCH 0/4] Python API to find missing objfiles Andrew Burgess
  2024-09-10 16:56 ` [PATCH 1/4] gdb: use mapped file information to improve debuginfod text Andrew Burgess
@ 2024-09-10 16:56 ` Andrew Burgess
  2024-10-01 17:25   ` Tom Tromey
  2024-09-10 16:56 ` [PATCH 3/4] gdb: add extension hook ext_lang_find_objfile_from_buildid Andrew Burgess
                   ` (2 subsequent siblings)
  4 siblings, 1 reply; 24+ messages in thread
From: Andrew Burgess @ 2024-09-10 16:56 UTC (permalink / raw)
  To: gdb-patches; +Cc: Andrew Burgess

In preparation for later commits in this series, rename
ext_lang_missing_debuginfo_result to ext_lang_missing_file_result.

A later commit will add additional Python APIs to handle different
types of missing files beyond just debuginfo.

This is just a rename commit, there should be no functional changes
after this commit.
---
 gdb/extension-priv.h |  2 +-
 gdb/extension.c      |  4 ++--
 gdb/extension.h      | 10 +++++-----
 gdb/python/python.c  | 12 ++++++------
 gdb/symfile-debug.c  |  2 +-
 5 files changed, 15 insertions(+), 15 deletions(-)

diff --git a/gdb/extension-priv.h b/gdb/extension-priv.h
index 653fd51e2f1..90810e4bbc8 100644
--- a/gdb/extension-priv.h
+++ b/gdb/extension-priv.h
@@ -292,7 +292,7 @@ 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);
 };
diff --git a/gdb/extension.c b/gdb/extension.c
index c488fc77494..f7b99d681eb 100644
--- a/gdb/extension.c
+++ b/gdb/extension.c
@@ -1053,7 +1053,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)
@@ -1061,7 +1061,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;
diff --git a/gdb/extension.h b/gdb/extension.h
index 5b0830b6675..2a749161508 100644
--- a/gdb/extension.h
+++ b/gdb/extension.h
@@ -361,23 +361,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.  */ }
 
@@ -407,7 +407,7 @@ 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);
 
 #if GDB_SELF_TEST
diff --git a/gdb/python/python.c b/gdb/python/python.c
index a2ce1f6545a..978fc0c4cac 100644
--- a/gdb/python/python.c
+++ b/gdb/python/python.c
@@ -128,7 +128,7 @@ 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);
 
 /* The interface between gdb proper and loading of python scripts.  */
@@ -1755,10 +1755,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)
 {
@@ -1806,7 +1806,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 ()))
@@ -1826,7 +1826,7 @@ 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 ()));
 }
 
 /* Compute the list of active python type printers and store them in
diff --git a/gdb/symfile-debug.c b/gdb/symfile-debug.c
index 0eccda21f39..473528fc9a2 100644
--- a/gdb/symfile-debug.c
+++ b/gdb/symfile-debug.c
@@ -628,7 +628,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 ())
 	    {
-- 
2.25.4


^ permalink raw reply	[flat|nested] 24+ messages in thread

* [PATCH 3/4] gdb: add extension hook ext_lang_find_objfile_from_buildid
  2024-09-10 16:56 [PATCH 0/4] Python API to find missing objfiles Andrew Burgess
  2024-09-10 16:56 ` [PATCH 1/4] gdb: use mapped file information to improve debuginfod text Andrew Burgess
  2024-09-10 16:56 ` [PATCH 2/4] gdb: rename ext_lang_missing_debuginfo_result Andrew Burgess
@ 2024-09-10 16:56 ` Andrew Burgess
  2024-10-01 17:35   ` Tom Tromey
  2024-09-10 16:56 ` [PATCH 4/4] gdb/python: implement Python find_exec_by_build_id hook Andrew Burgess
  2024-10-27  9:10 ` [PATCHv2 0/4] Python API to find missing objfiles Andrew Burgess
  4 siblings, 1 reply; 24+ messages in thread
From: Andrew Burgess @ 2024-09-10 16:56 UTC (permalink / raw)
  To: gdb-patches; +Cc: Andrew Burgess

Add a new ext_lang_find_objfile_from_buildid function which is called
from find_objfile_by_build_id and gives extension languages a chance
to find missing objfiles.

This commit adds the ext_lang_find_objfile_from_buildid function and
the extension_language_ops::find_objfile_from_buildid() hook, but does
not implement the hook for any extension languages, that will come in
the next commit.

This commit does rewrite find_objfile_by_build_id (build-id.c) to call
the new hook though.  The basic steps of find_objfile_by_build_id are
now this:

  1. Try to find the missing objfile using the build-id by looking in
  the debug-file-directory's .build-id/ sub-directory.  If we find the
  file then we're done.

  2. Ask debuginfod to download the missing file for us.  If we
  download the file successfully then we're done.

  3. Ask the extension language hook to find the file for us.  If the
  extension language asks us to try again then we repeat step (1) only
  and if we still don't have the file, we move to step (4).  If the
  extension language told us where the file is then we use that file
  and we're done.

  4. We didn't find the file.  Carry on without it.

Only step (3) is new in this logic, everything else was already done.

There are no tests added here as we can't currently write an extension
language callback.  The next commit will add the tests.
---
 gdb/build-id.c       | 88 ++++++++++++++++++++++++++++++++------------
 gdb/build-id.h       |  3 +-
 gdb/corelow.c        |  6 ++-
 gdb/extension-priv.h | 13 +++++++
 gdb/extension.c      | 22 +++++++++++
 gdb/extension.h      | 20 ++++++++++
 gdb/solib.c          |  3 +-
 7 files changed, 128 insertions(+), 27 deletions(-)

diff --git a/gdb/build-id.c b/gdb/build-id.c
index 6266be1bd6d..9d4b005489d 100644
--- a/gdb/build-id.c
+++ b/gdb/build-id.c
@@ -28,6 +28,7 @@
 #include "cli/cli-style.h"
 #include "gdbsupport/scoped_fd.h"
 #include "debuginfod-support.h"
+#include "extension.h"
 
 /* See build-id.h.  */
 
@@ -343,32 +344,73 @@ find_separate_debug_file_by_buildid (struct objfile *objfile,
 /* See build-id.h.  */
 
 gdb_bfd_ref_ptr
-find_objfile_by_build_id (const bfd_build_id *build_id,
+find_objfile_by_build_id (program_space *pspace,
+			  const bfd_build_id *build_id,
 			  const char *expected_filename)
 {
-  /* Try to find the executable (or shared object) by looking for a
-     (sym)link on disk from the build-id to the object file.  */
-  gdb_bfd_ref_ptr abfd = build_id_to_exec_bfd (build_id->size,
-					       build_id->data);
-
-  if (abfd != nullptr)
-    return abfd;
-
-  /* Attempt to query debuginfod for the executable.  */
-  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)
+  gdb_bfd_ref_ptr abfd;
+
+  for (unsigned attempt = 0, max_attempts = 1;
+       attempt < max_attempts && abfd == nullptr;
+       ++attempt)
     {
-      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;
+      /* 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
index 1d2e7891f34..0174412c5bd 100644
--- a/gdb/build-id.h
+++ b/gdb/build-id.h
@@ -66,7 +66,8 @@ extern std::string find_separate_debug_file_by_buildid
    should be the file we were looking for but couldn't find.  */
 
 extern gdb_bfd_ref_ptr find_objfile_by_build_id
-  (const bfd_build_id *build_id, const char *expected_filename);
+  (struct program_space *pspace, const bfd_build_id *build_id,
+   const char *expected_filename);
 
 /* Return an hex-string representation of BUILD_ID.  */
 
diff --git a/gdb/corelow.c b/gdb/corelow.c
index d8a2c3f8bc4..4c2d485089d 100644
--- a/gdb/corelow.c
+++ b/gdb/corelow.c
@@ -506,7 +506,8 @@ core_target::build_file_mappings ()
 	   || !bfd_check_format (abfd.get (), bfd_object))
 	  && file_data.build_id != nullptr)
 	{
-	  abfd = find_objfile_by_build_id (file_data.build_id,
+	  abfd = find_objfile_by_build_id (current_program_space,
+					   file_data.build_id,
 					   filename.c_str ());
 
 	  if (abfd != nullptr)
@@ -892,7 +893,8 @@ locate_exec_from_corefile_build_id (bfd *abfd, core_target *target,
     }
 
   gdb_bfd_ref_ptr execbfd
-    = find_objfile_by_build_id (build_id, filename.c_str ());
+    = find_objfile_by_build_id (current_program_space, build_id,
+				filename.c_str ());
 
   if (execbfd != nullptr)
     {
diff --git a/gdb/extension-priv.h b/gdb/extension-priv.h
index 90810e4bbc8..a31283f831c 100644
--- a/gdb/extension-priv.h
+++ b/gdb/extension-priv.h
@@ -295,6 +295,19 @@ struct extension_language_ops
   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
index f7b99d681eb..d96d8a79a0b 100644
--- a/gdb/extension.c
+++ b/gdb/extension.c
@@ -1070,6 +1070,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
index 2a749161508..ad3b4e6102d 100644
--- 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.
@@ -410,6 +411,25 @@ struct ext_lang_missing_file_result
 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/solib.c b/gdb/solib.c
index b123be302bc..6fa26faef42 100644
--- a/gdb/solib.c
+++ b/gdb/solib.c
@@ -525,7 +525,8 @@ solib_map_sections (solib &so)
 	    abfd = nullptr;
 
 	  if (abfd == nullptr)
-	    abfd = find_objfile_by_build_id (mapped_file_info->build_id (),
+	    abfd = find_objfile_by_build_id (current_program_space,
+					     mapped_file_info->build_id (),
 					     so.so_name.c_str ());
 
 	  if (abfd == nullptr && mismatch)
-- 
2.25.4


^ permalink raw reply	[flat|nested] 24+ messages in thread

* [PATCH 4/4] gdb/python: implement Python find_exec_by_build_id hook
  2024-09-10 16:56 [PATCH 0/4] Python API to find missing objfiles Andrew Burgess
                   ` (2 preceding siblings ...)
  2024-09-10 16:56 ` [PATCH 3/4] gdb: add extension hook ext_lang_find_objfile_from_buildid Andrew Burgess
@ 2024-09-10 16:56 ` Andrew Burgess
  2024-09-10 18:13   ` Eli Zaretskii
  2024-10-01 18:29   ` Tom Tromey
  2024-10-27  9:10 ` [PATCHv2 0/4] Python API to find missing objfiles Andrew Burgess
  4 siblings, 2 replies; 24+ messages in thread
From: Andrew Burgess @ 2024-09-10 16:56 UTC (permalink / raw)
  To: gdb-patches; +Cc: Andrew Burgess

Implement extension_language_ops::find_objfile_from_buildid within
GDB's Python API.  Doing this allows users to write Python extensions
that can help locate missing objfiles when GDB opens a core file.  A
handler might perform some project- or site-specific actions to find a
missing objfile.  Or might provide some project- or site-specific
advice to the user on how they can obtain the missing objfile.

The implementation is very similar to the approach taken in:

  commit 8f6c452b5a4e50fbb55ff1d13328b392ad1fd416
  Date:   Sun Oct 15 22:48:42 2023 +0100

      gdb: implement missing debug handler hook for Python

The following new commands are added as commands implemented in
Python, this is similar to how the Python missing debug and unwinder
commands are implemented:

  info missing-objfile-handlers
  enable missing-objfile-handler LOCUS HANDLER
  disable missing-objfile-handler LOCUS HANDLER

To make use of this extension hook a user will create missing objfile
handler objects, and registers these handlers with GDB.  When GDB
opens a core file and encounters a missing objfile each handler is
called in turn until one is able to help.  Here is a minimal handler
that does nothing useful:

  import gdb
  import gdb.missing_objfile

  class MyFirstHandler(gdb.missing_objfile.MissingObjfileHandler):
      def __init__(self):
          super().__init__("my_first_handler")

      def __call__(self, pspace, build_id, filename):
          # This handler does nothing useful.
          return None

  gdb.missing_objfile.register_handler(None, MyFirstHandler())

Returning None from the __call__ method tells GDB that this handler
was unable to find the missing objfile, and GDB should ask any other
registered handlers.

Possible return values from a handler:

  - None: This means the handler couldn't help.  GDB will call other
          registered handlers to see if they can help instead.

  - False: The handler has done all it can, but the objfile couldn't
            be found.  GDB will not call any other handlers, and will
	    continue without the objfile.

  - True: The handler has installed the objfile into a location where
          GDB would normally expect to find it.  GDB should repeat its
	  normal lookup process and the objfile should now be found.

  - A string: The handler can return a filename, which is the missing
              objfile.  GDB will load this file.

Handlers can be registered globally, or per program space.  GDB checks
the handlers for the current program space first, and then all of the
global handles.  The first handler that returns a value that is not
None, has "handled" the missing objfile, at which point GDB continues.

The implementation of this feature is mostly straight forward.  I have
reworked some of the missing debug file related code so that it can be
shared with this feature.  E.g. gdb/python/lib/gdb/missing_files.py is
mostly content moved from gdb/python/lib/gdb/missing_debug.py, but
updated to be more generic.  Now gdb/python/lib/gdb/missing_debug.py
and the new file gdb/python/lib/gdb/missing_objfile.py both call into
the missing_files.py file.

For gdb/python/lib/gdb/command/missing_files.py this is even more
extreme, gdb/python/lib/gdb/command/missing_debug.py is completely
gone now and gdb/python/lib/gdb/command/missing_files.py provides all
of the new commands in a generic way.

I have made one change to the existing Python API, I renamed the
attribute Progspace.missing_debug_handlers to
Progspace.missing_file_handlers.  I don't see this as too
problematic.  This attribute was only used to implement the missing
debug feature and was never documented beyond the fact that it
existed.  There was no reason for users to be touching this attribute.
---
 gdb/NEWS                                      |  24 +
 gdb/data-directory/Makefile.in                |   4 +-
 gdb/doc/gdb.texinfo                           |   1 +
 gdb/doc/python.texi                           | 194 ++++++-
 gdb/python/lib/gdb/__init__.py                | 110 +++-
 .../{missing_debug.py => missing_files.py}    | 135 +++--
 gdb/python/lib/gdb/missing_debug.py           | 161 +-----
 gdb/python/lib/gdb/missing_files.py           | 204 +++++++
 gdb/python/lib/gdb/missing_objfile.py         |  67 +++
 gdb/python/py-progspace.c                     |  26 +-
 gdb/python/python.c                           | 108 +++-
 .../gdb.python/py-missing-objfile-lib.c       |  35 ++
 gdb/testsuite/gdb.python/py-missing-objfile.c |  49 ++
 .../gdb.python/py-missing-objfile.exp         | 544 ++++++++++++++++++
 .../gdb.python/py-missing-objfile.py          | 158 +++++
 15 files changed, 1585 insertions(+), 235 deletions(-)
 rename gdb/python/lib/gdb/command/{missing_debug.py => missing_files.py} (54%)
 create mode 100644 gdb/python/lib/gdb/missing_files.py
 create mode 100644 gdb/python/lib/gdb/missing_objfile.py
 create mode 100644 gdb/testsuite/gdb.python/py-missing-objfile-lib.c
 create mode 100644 gdb/testsuite/gdb.python/py-missing-objfile.c
 create mode 100644 gdb/testsuite/gdb.python/py-missing-objfile.exp
 create mode 100644 gdb/testsuite/gdb.python/py-missing-objfile.py

diff --git a/gdb/NEWS b/gdb/NEWS
index cfc9cb05f77..2572336c902 100644
--- a/gdb/NEWS
+++ b/gdb/NEWS
@@ -42,6 +42,17 @@
   ** Added gdb.record.clear.  Clears the trace data of the current recording.
      This forces re-decoding of the trace for successive commands.
 
+  ** 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.
+
 * Debugger Adapter Protocol changes
 
   ** The "scopes" request will now return a scope holding global
@@ -64,6 +75,19 @@ maintenance info blocks [ADDRESS]
   are listed starting at the inner global block out to the most inner
   block.
 
+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.
+
 * Changed commands
 
 remove-symbol-file
diff --git a/gdb/data-directory/Makefile.in b/gdb/data-directory/Makefile.in
index f9472f49ee9..f3a99f63cf0 100644
--- 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/ptwrite.py \
@@ -87,7 +89,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/doc/gdb.texinfo b/gdb/doc/gdb.texinfo
index 780eaf41f16..cd6c3b606eb 100644
--- a/gdb/doc/gdb.texinfo
+++ b/gdb/doc/gdb.texinfo
@@ -21800,6 +21800,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
diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi
index bb1f205d4fe..c61aa48a1f0 100644
--- 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
@@ -5388,10 +5389,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 objects for this program space.
+See @ref{Missing Debug Info In Python} and @ref{Missing Objfiles In
+Python} for more information.
 @end defvar
 
 A program space has the following methods:
@@ -5567,6 +5569,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}.
@@ -8155,6 +8158,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 debug
+handler within some @value{GDBN} commands.  Valid names consist of the
+characters @code{[-_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/python/lib/gdb/__init__.py b/gdb/python/lib/gdb/__init__.py
index 6c3e2419a40..146a963044f 100644
--- 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
index 313b88cf8c3..463853b8cec 100644
--- 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
index 7ccc4fe496d..2c2cebacaf8 100644
--- a/gdb/python/lib/gdb/missing_debug.py
+++ b/gdb/python/lib/gdb/missing_debug.py
@@ -17,72 +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:
-    # 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 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
@@ -93,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
@@ -148,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
index 00000000000..5f2df88c728
--- /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
index 00000000000..ace0e1315b5
--- /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
index 5bc0015d728..829bb0a0e3e 100644
--- 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
index 978fc0c4cac..4f28d051c74 100644
--- 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"
@@ -130,6 +131,9 @@ static std::optional<std::string> gdbpy_colorize_disasm
 (const std::string &content, gdbarch *gdbarch);
 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.  */
 
@@ -179,7 +183,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 */
@@ -1829,6 +1834,107 @@ gdbpy_handle_missing_debuginfo (const struct extension_language_defn *extlang,
   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_flilename)
+{
+  gdb_assert (pspace != nullptr);
+  gdb_assert (build_id != nullptr);
+  gdb_assert (missing_flilename != 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 ()).release ());
+  if (pyo_buildid == nullptr)
+    {
+      gdbpy_print_stack ();
+      return {};
+    }
+
+  /* Convert MISSING_FILENAME to a Python object.  */
+  gdbpy_ref<> pyo_filename;
+  pyo_filename = host_string_to_python_string (missing_flilename);
+  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 ()))
+    {
+      bool try_again = PyObject_IsTrue (pyo_execute_ret.get ());
+      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 String");
+      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
    EXT_PRINTERS->py_type_printers.  The product of this function is used by
    gdbpy_apply_type_printers, and freed by gdbpy_free_type_printers.
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
index 00000000000..8d740b4bb71
--- /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
index 00000000000..953e1c0fb5b
--- /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
index 00000000000..629cc56dbff
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-missing-objfile.exp
@@ -0,0 +1,544 @@
+# 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
+
+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
+}
+
+set remote_python_file \
+    [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py]
+
+# Generate a core file.
+set corefile [core_find $binfile {}]
+if {$corefile == ""} {
+    unsupport "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\]+"] \
+	"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"
+
+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
index 00000000000..0e5f91e9b32
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-missing-objfile.py
@@ -0,0 +1,158 @@
+# 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
+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)
+            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")
-- 
2.25.4


^ permalink raw reply	[flat|nested] 24+ messages in thread

* Re: [PATCH 4/4] gdb/python: implement Python find_exec_by_build_id hook
  2024-09-10 16:56 ` [PATCH 4/4] gdb/python: implement Python find_exec_by_build_id hook Andrew Burgess
@ 2024-09-10 18:13   ` Eli Zaretskii
  2024-09-22 17:11     ` Andrew Burgess
  2024-10-01 18:29   ` Tom Tromey
  1 sibling, 1 reply; 24+ messages in thread
From: Eli Zaretskii @ 2024-09-10 18:13 UTC (permalink / raw)
  To: Andrew Burgess; +Cc: gdb-patches

> From: Andrew Burgess <aburgess@redhat.com>
> Cc: Andrew Burgess <aburgess@redhat.com>
> Date: Tue, 10 Sep 2024 17:56:20 +0100
> 
>  gdb/NEWS                                      |  24 +
>  gdb/data-directory/Makefile.in                |   4 +-
>  gdb/doc/gdb.texinfo                           |   1 +
>  gdb/doc/python.texi                           | 194 ++++++-
>  gdb/python/lib/gdb/__init__.py                | 110 +++-
>  .../{missing_debug.py => missing_files.py}    | 135 +++--
>  gdb/python/lib/gdb/missing_debug.py           | 161 +-----
>  gdb/python/lib/gdb/missing_files.py           | 204 +++++++
>  gdb/python/lib/gdb/missing_objfile.py         |  67 +++
>  gdb/python/py-progspace.c                     |  26 +-
>  gdb/python/python.c                           | 108 +++-
>  .../gdb.python/py-missing-objfile-lib.c       |  35 ++
>  gdb/testsuite/gdb.python/py-missing-objfile.c |  49 ++
>  .../gdb.python/py-missing-objfile.exp         | 544 ++++++++++++++++++
>  .../gdb.python/py-missing-objfile.py          | 158 +++++
>  15 files changed, 1585 insertions(+), 235 deletions(-)
>  rename gdb/python/lib/gdb/command/{missing_debug.py => missing_files.py} (54%)
>  create mode 100644 gdb/python/lib/gdb/missing_files.py
>  create mode 100644 gdb/python/lib/gdb/missing_objfile.py
>  create mode 100644 gdb/testsuite/gdb.python/py-missing-objfile-lib.c
>  create mode 100644 gdb/testsuite/gdb.python/py-missing-objfile.c
>  create mode 100644 gdb/testsuite/gdb.python/py-missing-objfile.exp
>  create mode 100644 gdb/testsuite/gdb.python/py-missing-objfile.py

Thanks.

> +info missing-objfile-handlers
> +  List all the registered missing objfile handlers.

"registered missing objfile handlers" is confusing wording.  I suggest
to say "registered missing-objfile handlers" instead to make it less
confusing.

> +enable missing-objfile-handler LOCUS HANDLER
> +disable missing-objfile-handler LOCUS HANDLER
> +  Enable or disable a missing objfile handler with a name matching the
                         ^^^^^^^^^^^^^^^^^^^^^^^
Same here: "missing-objfile handler".

> +  regular expression HANDLER, in LOCUS.
> +
> +  LOCUS can be 'global' to operate on global missing objfile handler,
                                                ^^^^^^^^^^^^^^^^^^^^^^^
And here.

> +The @code{missing_file_handlers} attribute is a list of tuples.  Each
> +tuple holds a missing file handler objects for this program space.
                 ^^^^^^^^^^^^^^^^^^^^
Same here.

Also, "a missing-file handler object", without "s".

> +See @ref{Missing Debug Info In Python} and @ref{Missing Objfiles In
> +Python} for more information.

Are we sure all the supported versions of makeinfo can cope with
references that don't have punctuation characters after the closing
brace?

> +A missing objfile Python extension consists of a handler object which
     ^^^^^^^^^^^^^^^
"missing-objfile"

> +@defun MissingObjfileHandler.__init__ (name)
> +The @var{name} is a string used to reference this missing debug
> +handler within some @value{GDBN} commands.        ^^^^^^^^^^^^^

"missing-objfile handler", right?

>                                             Valid names consist of the
> +characters @code{[-_a-zA-Z0-9]}, creating a handler with an invalid
              ^^^^^^^^^^^^^^^^^^^^
@samp is better here.
> +@defun gdb.missing_objfile.register_handler (locus, handler, replace=@code{False})
> +Register a new missing objfile handler with @value{GDBN}.
                  ^^^^^^^^^^^^^^^^^^^^^^^
"missing-objfile handler"

> +@value{GDBN} defines the following commands to manage registered
> +missing objfile handlers:
   ^^^^^^^^^^^^^^^
Same here.

> +@item info missing-objfile-handlers @r{[} @var{locus} @r{[} @var{name-regexp} @r{]} @r{]}
> +Lists all registered missing objfile handlers.  Arguments @var{locus}
                        ^^^^^^^^^^^^^^^^^^^^^^^^
And here.

Reviewed-By: Eli Zaretskii <eliz@gnu.org>

^ permalink raw reply	[flat|nested] 24+ messages in thread

* Re: [PATCH 4/4] gdb/python: implement Python find_exec_by_build_id hook
  2024-09-10 18:13   ` Eli Zaretskii
@ 2024-09-22 17:11     ` Andrew Burgess
  2024-09-22 17:58       ` Eli Zaretskii
  0 siblings, 1 reply; 24+ messages in thread
From: Andrew Burgess @ 2024-09-22 17:11 UTC (permalink / raw)
  To: Eli Zaretskii; +Cc: gdb-patches


Eli,

Thanks for the feedback.  I've included the updated patch below.

Eli Zaretskii <eliz@gnu.org> writes:

>> From: Andrew Burgess <aburgess@redhat.com>
>> Cc: Andrew Burgess <aburgess@redhat.com>
>> Date: Tue, 10 Sep 2024 17:56:20 +0100
>> 
>>  gdb/NEWS                                      |  24 +
>>  gdb/data-directory/Makefile.in                |   4 +-
>>  gdb/doc/gdb.texinfo                           |   1 +
>>  gdb/doc/python.texi                           | 194 ++++++-
>>  gdb/python/lib/gdb/__init__.py                | 110 +++-
>>  .../{missing_debug.py => missing_files.py}    | 135 +++--
>>  gdb/python/lib/gdb/missing_debug.py           | 161 +-----
>>  gdb/python/lib/gdb/missing_files.py           | 204 +++++++
>>  gdb/python/lib/gdb/missing_objfile.py         |  67 +++
>>  gdb/python/py-progspace.c                     |  26 +-
>>  gdb/python/python.c                           | 108 +++-
>>  .../gdb.python/py-missing-objfile-lib.c       |  35 ++
>>  gdb/testsuite/gdb.python/py-missing-objfile.c |  49 ++
>>  .../gdb.python/py-missing-objfile.exp         | 544 ++++++++++++++++++
>>  .../gdb.python/py-missing-objfile.py          | 158 +++++
>>  15 files changed, 1585 insertions(+), 235 deletions(-)
>>  rename gdb/python/lib/gdb/command/{missing_debug.py => missing_files.py} (54%)
>>  create mode 100644 gdb/python/lib/gdb/missing_files.py
>>  create mode 100644 gdb/python/lib/gdb/missing_objfile.py
>>  create mode 100644 gdb/testsuite/gdb.python/py-missing-objfile-lib.c
>>  create mode 100644 gdb/testsuite/gdb.python/py-missing-objfile.c
>>  create mode 100644 gdb/testsuite/gdb.python/py-missing-objfile.exp
>>  create mode 100644 gdb/testsuite/gdb.python/py-missing-objfile.py
>
> Thanks.
>
>> +info missing-objfile-handlers
>> +  List all the registered missing objfile handlers.
>
> "registered missing objfile handlers" is confusing wording.  I suggest
> to say "registered missing-objfile handlers" instead to make it less
> confusing.

As suggested I've converted to "missing-objfile" throughout.

>
>> +enable missing-objfile-handler LOCUS HANDLER
>> +disable missing-objfile-handler LOCUS HANDLER
>> +  Enable or disable a missing objfile handler with a name matching the
>                          ^^^^^^^^^^^^^^^^^^^^^^^
> Same here: "missing-objfile handler".
>
>> +  regular expression HANDLER, in LOCUS.
>> +
>> +  LOCUS can be 'global' to operate on global missing objfile handler,
>                                                 ^^^^^^^^^^^^^^^^^^^^^^^
> And here.
>
>> +The @code{missing_file_handlers} attribute is a list of tuples.  Each
>> +tuple holds a missing file handler objects for this program space.
>                  ^^^^^^^^^^^^^^^^^^^^
> Same here.
>
> Also, "a missing-file handler object", without "s".
>
>> +See @ref{Missing Debug Info In Python} and @ref{Missing Objfiles In
>> +Python} for more information.
>
> Are we sure all the supported versions of makeinfo can cope with
> references that don't have punctuation characters after the closing
> brace?

I've rewritten this sentence based on an example taken from the texinfo
manual for @ref, I now say:

  For more information, @pxref{Missing Debug Info In Python}, and
  @ref{Missing Objfiles In Python}.

which hopefully should be OK.

>
>> +A missing objfile Python extension consists of a handler object which
>      ^^^^^^^^^^^^^^^
> "missing-objfile"
>
>> +@defun MissingObjfileHandler.__init__ (name)
>> +The @var{name} is a string used to reference this missing debug
>> +handler within some @value{GDBN} commands.        ^^^^^^^^^^^^^
>
> "missing-objfile handler", right?

Yup.  Fixed.

>
>>                                             Valid names consist of the
>> +characters @code{[-_a-zA-Z0-9]}, creating a handler with an invalid
>               ^^^^^^^^^^^^^^^^^^^^
> @samp is better here.

Fixed.

>> +@defun gdb.missing_objfile.register_handler (locus, handler, replace=@code{False})
>> +Register a new missing objfile handler with @value{GDBN}.
>                   ^^^^^^^^^^^^^^^^^^^^^^^
> "missing-objfile handler"
>
>> +@value{GDBN} defines the following commands to manage registered
>> +missing objfile handlers:
>    ^^^^^^^^^^^^^^^
> Same here.
>
>> +@item info missing-objfile-handlers @r{[} @var{locus} @r{[} @var{name-regexp} @r{]} @r{]}
>> +Lists all registered missing objfile handlers.  Arguments @var{locus}
>                         ^^^^^^^^^^^^^^^^^^^^^^^^
> And here.
>
> Reviewed-By: Eli Zaretskii <eliz@gnu.org>

New patch is below.

Thanks,
Andrew

---

commit 65da14953194f0002118ea4672d1d56ecd586e35
Author: Andrew Burgess <aburgess@redhat.com>
Date:   Wed Jul 31 15:55:57 2024 +0100

    gdb/python: implement Python find_exec_by_build_id hook
    
    Implement extension_language_ops::find_objfile_from_buildid within
    GDB's Python API.  Doing this allows users to write Python extensions
    that can help locate missing objfiles when GDB opens a core file.  A
    handler might perform some project- or site-specific actions to find a
    missing objfile.  Or might provide some project- or site-specific
    advice to the user on how they can obtain the missing objfile.
    
    The implementation is very similar to the approach taken in:
    
      commit 8f6c452b5a4e50fbb55ff1d13328b392ad1fd416
      Date:   Sun Oct 15 22:48:42 2023 +0100
    
          gdb: implement missing debug handler hook for Python
    
    The following new commands are added as commands implemented in
    Python, this is similar to how the Python missing debug and unwinder
    commands are implemented:
    
      info missing-objfile-handlers
      enable missing-objfile-handler LOCUS HANDLER
      disable missing-objfile-handler LOCUS HANDLER
    
    To make use of this extension hook a user will create missing objfile
    handler objects, and registers these handlers with GDB.  When GDB
    opens a core file and encounters a missing objfile each handler is
    called in turn until one is able to help.  Here is a minimal handler
    that does nothing useful:
    
      import gdb
      import gdb.missing_objfile
    
      class MyFirstHandler(gdb.missing_objfile.MissingObjfileHandler):
          def __init__(self):
              super().__init__("my_first_handler")
    
          def __call__(self, pspace, build_id, filename):
              # This handler does nothing useful.
              return None
    
      gdb.missing_objfile.register_handler(None, MyFirstHandler())
    
    Returning None from the __call__ method tells GDB that this handler
    was unable to find the missing objfile, and GDB should ask any other
    registered handlers.
    
    Possible return values from a handler:
    
      - None: This means the handler couldn't help.  GDB will call other
              registered handlers to see if they can help instead.
    
      - False: The handler has done all it can, but the objfile couldn't
                be found.  GDB will not call any other handlers, and will
                continue without the objfile.
    
      - True: The handler has installed the objfile into a location where
              GDB would normally expect to find it.  GDB should repeat its
              normal lookup process and the objfile should now be found.
    
      - A string: The handler can return a filename, which is the missing
                  objfile.  GDB will load this file.
    
    Handlers can be registered globally, or per program space.  GDB checks
    the handlers for the current program space first, and then all of the
    global handles.  The first handler that returns a value that is not
    None, has "handled" the missing objfile, at which point GDB continues.
    
    The implementation of this feature is mostly straight forward.  I have
    reworked some of the missing debug file related code so that it can be
    shared with this feature.  E.g. gdb/python/lib/gdb/missing_files.py is
    mostly content moved from gdb/python/lib/gdb/missing_debug.py, but
    updated to be more generic.  Now gdb/python/lib/gdb/missing_debug.py
    and the new file gdb/python/lib/gdb/missing_objfile.py both call into
    the missing_files.py file.
    
    For gdb/python/lib/gdb/command/missing_files.py this is even more
    extreme, gdb/python/lib/gdb/command/missing_debug.py is completely
    gone now and gdb/python/lib/gdb/command/missing_files.py provides all
    of the new commands in a generic way.
    
    I have made one change to the existing Python API, I renamed the
    attribute Progspace.missing_debug_handlers to
    Progspace.missing_file_handlers.  I don't see this as too
    problematic.  This attribute was only used to implement the missing
    debug feature and was never documented beyond the fact that it
    existed.  There was no reason for users to be touching this attribute.
    
    Reviewed-By: Eli Zaretskii <eliz@gnu.org>

diff --git a/gdb/NEWS b/gdb/NEWS
index cfc9cb05f77..384a4c94526 100644
--- a/gdb/NEWS
+++ b/gdb/NEWS
@@ -42,6 +42,17 @@
   ** Added gdb.record.clear.  Clears the trace data of the current recording.
      This forces re-decoding of the trace for successive commands.
 
+  ** 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.
+
 * Debugger Adapter Protocol changes
 
   ** The "scopes" request will now return a scope holding global
@@ -64,6 +75,19 @@ maintenance info blocks [ADDRESS]
   are listed starting at the inner global block out to the most inner
   block.
 
+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.
+
 * Changed commands
 
 remove-symbol-file
diff --git a/gdb/data-directory/Makefile.in b/gdb/data-directory/Makefile.in
index f9472f49ee9..f3a99f63cf0 100644
--- 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/ptwrite.py \
@@ -87,7 +89,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/doc/gdb.texinfo b/gdb/doc/gdb.texinfo
index 77a4021b36a..47fe880233e 100644
--- a/gdb/doc/gdb.texinfo
+++ b/gdb/doc/gdb.texinfo
@@ -21800,6 +21800,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
diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi
index 22f0e6c6d0a..3a83791aec7 100644
--- 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
@@ -5388,10 +5389,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:
@@ -5567,6 +5569,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}.
@@ -8155,6 +8158,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/python/lib/gdb/__init__.py b/gdb/python/lib/gdb/__init__.py
index 6c3e2419a40..146a963044f 100644
--- 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
index 313b88cf8c3..463853b8cec 100644
--- 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
index 7ccc4fe496d..2c2cebacaf8 100644
--- a/gdb/python/lib/gdb/missing_debug.py
+++ b/gdb/python/lib/gdb/missing_debug.py
@@ -17,72 +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:
-    # 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 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
@@ -93,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
@@ -148,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
index 00000000000..5f2df88c728
--- /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
index 00000000000..ace0e1315b5
--- /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
index 5bc0015d728..829bb0a0e3e 100644
--- 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
index 978fc0c4cac..4f28d051c74 100644
--- 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"
@@ -130,6 +131,9 @@ static std::optional<std::string> gdbpy_colorize_disasm
 (const std::string &content, gdbarch *gdbarch);
 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.  */
 
@@ -179,7 +183,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 */
@@ -1829,6 +1834,107 @@ gdbpy_handle_missing_debuginfo (const struct extension_language_defn *extlang,
   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_flilename)
+{
+  gdb_assert (pspace != nullptr);
+  gdb_assert (build_id != nullptr);
+  gdb_assert (missing_flilename != 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 ()).release ());
+  if (pyo_buildid == nullptr)
+    {
+      gdbpy_print_stack ();
+      return {};
+    }
+
+  /* Convert MISSING_FILENAME to a Python object.  */
+  gdbpy_ref<> pyo_filename;
+  pyo_filename = host_string_to_python_string (missing_flilename);
+  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 ()))
+    {
+      bool try_again = PyObject_IsTrue (pyo_execute_ret.get ());
+      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 String");
+      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
    EXT_PRINTERS->py_type_printers.  The product of this function is used by
    gdbpy_apply_type_printers, and freed by gdbpy_free_type_printers.
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
index 00000000000..8d740b4bb71
--- /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
index 00000000000..953e1c0fb5b
--- /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
index 00000000000..51027c9ca80
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-missing-objfile.exp
@@ -0,0 +1,544 @@
+# 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
+
+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
+}
+
+set remote_python_file \
+    [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py]
+
+# Generate a core file.
+set corefile [core_find $binfile {}]
+if {$corefile == ""} {
+    unsupport "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"
+
+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
index 00000000000..0e5f91e9b32
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-missing-objfile.py
@@ -0,0 +1,158 @@
+# 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
+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)
+            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")


^ permalink raw reply	[flat|nested] 24+ messages in thread

* Re: [PATCH 4/4] gdb/python: implement Python find_exec_by_build_id hook
  2024-09-22 17:11     ` Andrew Burgess
@ 2024-09-22 17:58       ` Eli Zaretskii
  0 siblings, 0 replies; 24+ messages in thread
From: Eli Zaretskii @ 2024-09-22 17:58 UTC (permalink / raw)
  To: Andrew Burgess; +Cc: gdb-patches

> From: Andrew Burgess <aburgess@redhat.com>
> Cc: gdb-patches@sourceware.org
> Date: Sun, 22 Sep 2024 18:11:06 +0100
> 
> > Reviewed-By: Eli Zaretskii <eliz@gnu.org>
> 
> New patch is below.

Thanks, the documentation parts are okay.

Reviewed-By: Eli Zaretskii <eliz@gnu.org>

^ permalink raw reply	[flat|nested] 24+ messages in thread

* Re: [PATCH 1/4] gdb: use mapped file information to improve debuginfod text
  2024-09-10 16:56 ` [PATCH 1/4] gdb: use mapped file information to improve debuginfod text Andrew Burgess
@ 2024-10-01 17:24   ` Tom Tromey
  0 siblings, 0 replies; 24+ messages in thread
From: Tom Tromey @ 2024-10-01 17:24 UTC (permalink / raw)
  To: Andrew Burgess; +Cc: gdb-patches

>>>>> "Andrew" == Andrew Burgess <aburgess@redhat.com> writes:

Andrew> For the unlikely edge case of a core-file that has build-ids, but
Andrew> doesn't have any mapped file information then I propose that we
Andrew> synthesis a filename like: 'with build-id xxxxxx'.  We would then see
Andrew> a message like:

Andrew>   Downloading file with build-id xxxxxx...

Andrew> Where 'xxxxxx' would be replaced by the actual build-id.

Andrew> This isn't ideal, but I think is good enough, and, as I said, I think
Andrew> this case is not going to be hit very often, or maybe at all.

I actually think this is better, because it is more precise about what
gdb is doing.

Anyway this looks fine to me.
Approved-By: Tom Tromey <tom@tromey.com>

Tom

^ permalink raw reply	[flat|nested] 24+ messages in thread

* Re: [PATCH 2/4] gdb: rename ext_lang_missing_debuginfo_result
  2024-09-10 16:56 ` [PATCH 2/4] gdb: rename ext_lang_missing_debuginfo_result Andrew Burgess
@ 2024-10-01 17:25   ` Tom Tromey
  0 siblings, 0 replies; 24+ messages in thread
From: Tom Tromey @ 2024-10-01 17:25 UTC (permalink / raw)
  To: Andrew Burgess; +Cc: gdb-patches

>>>>> "Andrew" == Andrew Burgess <aburgess@redhat.com> writes:

Andrew> In preparation for later commits in this series, rename
Andrew> ext_lang_missing_debuginfo_result to ext_lang_missing_file_result.

Andrew> A later commit will add additional Python APIs to handle different
Andrew> types of missing files beyond just debuginfo.

Andrew> This is just a rename commit, there should be no functional changes
Andrew> after this commit.

Makes sense.
Approved-By: Tom Tromey <tom@tromey.com>

Tom

^ permalink raw reply	[flat|nested] 24+ messages in thread

* Re: [PATCH 3/4] gdb: add extension hook ext_lang_find_objfile_from_buildid
  2024-09-10 16:56 ` [PATCH 3/4] gdb: add extension hook ext_lang_find_objfile_from_buildid Andrew Burgess
@ 2024-10-01 17:35   ` Tom Tromey
  0 siblings, 0 replies; 24+ messages in thread
From: Tom Tromey @ 2024-10-01 17:35 UTC (permalink / raw)
  To: Andrew Burgess; +Cc: gdb-patches

>>>>> "Andrew" == Andrew Burgess <aburgess@redhat.com> writes:

Andrew>   3. Ask the extension language hook to find the file for us.  If the
Andrew>   extension language asks us to try again then we repeat step (1) only
Andrew>   and if we still don't have the file, we move to step (4).  If the
Andrew>   extension language told us where the file is then we use that file
Andrew>   and we're done.

Andrew>   4. We didn't find the file.  Carry on without it.

Andrew> Only step (3) is new in this logic, everything else was already done.

Andrew> There are no tests added here as we can't currently write an extension
Andrew> language callback.  The next commit will add the tests.

This seems fine to me, though I suspect the logic would be a little
clearer if step (1) was a new function that was then conditionally
called from (4).  This would avoid the state-tracking loop.

Approved-By: Tom Tromey <tom@tromey.com>

Tom

^ permalink raw reply	[flat|nested] 24+ messages in thread

* Re: [PATCH 4/4] gdb/python: implement Python find_exec_by_build_id hook
  2024-09-10 16:56 ` [PATCH 4/4] gdb/python: implement Python find_exec_by_build_id hook Andrew Burgess
  2024-09-10 18:13   ` Eli Zaretskii
@ 2024-10-01 18:29   ` Tom Tromey
  2024-10-11 23:07     ` Andrew Burgess
  1 sibling, 1 reply; 24+ messages in thread
From: Tom Tromey @ 2024-10-01 18:29 UTC (permalink / raw)
  To: Andrew Burgess; +Cc: gdb-patches

>>>>> "Andrew" == Andrew Burgess <aburgess@redhat.com> writes:

Andrew> Implement extension_language_ops::find_objfile_from_buildid within
Andrew> GDB's Python API.  Doing this allows users to write Python extensions
Andrew> that can help locate missing objfiles when GDB opens a core file.  A
Andrew> handler might perform some project- or site-specific actions to find a
Andrew> missing objfile.  Or might provide some project- or site-specific
Andrew> advice to the user on how they can obtain the missing objfile.

Thanks for the patch.

Andrew> +static ext_lang_missing_file_result
Andrew> +gdbpy_find_objfile_from_buildid (const struct extension_language_defn *extlang,
Andrew> +				 program_space *pspace,
Andrew> +				 const struct bfd_build_id *build_id,
Andrew> +				 const char *missing_flilename)

Typo, s/flile/file/

Andrew> +  /* Convert BUILD_ID into a Python object.  */
Andrew> +  std::string hex_form = bin2hex (build_id->data, build_id->size);
Andrew> +  gdbpy_ref<> pyo_buildid
Andrew> +    (host_string_to_python_string (hex_form.c_str ()).release ());

The .release() here is weird, since this can just be

   gdbpy_ref<> pyo_buildid = host_string_to_python_string (...);

Andrew> +  /* Convert MISSING_FILENAME to a Python object.  */
Andrew> +  gdbpy_ref<> pyo_filename;
Andrew> +  pyo_filename = host_string_to_python_string (missing_flilename);

Could combine these two lines.

Andrew> +  if (PyBool_Check (pyo_execute_ret.get ()))
Andrew> +    {
Andrew> +      bool try_again = PyObject_IsTrue (pyo_execute_ret.get ());

Technically PyObject_IsTrue can return -1 to mean an error occurred.
It's probably impossible here, but maybe comparing '== Py_True' would
avoid this possibility.

Andrew> +		       "be None, a Bool, or a String");

I think the Python types aren't capitalized and the string type is named
'str'.

Tom

^ permalink raw reply	[flat|nested] 24+ messages in thread

* Re: [PATCH 4/4] gdb/python: implement Python find_exec_by_build_id hook
  2024-10-01 18:29   ` Tom Tromey
@ 2024-10-11 23:07     ` Andrew Burgess
  0 siblings, 0 replies; 24+ messages in thread
From: Andrew Burgess @ 2024-10-11 23:07 UTC (permalink / raw)
  To: Tom Tromey; +Cc: gdb-patches

Tom Tromey <tom@tromey.com> writes:

>>>>>> "Andrew" == Andrew Burgess <aburgess@redhat.com> writes:
>
> Andrew> Implement extension_language_ops::find_objfile_from_buildid within
> Andrew> GDB's Python API.  Doing this allows users to write Python extensions
> Andrew> that can help locate missing objfiles when GDB opens a core file.  A
> Andrew> handler might perform some project- or site-specific actions to find a
> Andrew> missing objfile.  Or might provide some project- or site-specific
> Andrew> advice to the user on how they can obtain the missing objfile.
>
> Thanks for the patch.
>
> Andrew> +static ext_lang_missing_file_result
> Andrew> +gdbpy_find_objfile_from_buildid (const struct extension_language_defn *extlang,
> Andrew> +				 program_space *pspace,
> Andrew> +				 const struct bfd_build_id *build_id,
> Andrew> +				 const char *missing_flilename)
>
> Typo, s/flile/file/
>
> Andrew> +  /* Convert BUILD_ID into a Python object.  */
> Andrew> +  std::string hex_form = bin2hex (build_id->data, build_id->size);
> Andrew> +  gdbpy_ref<> pyo_buildid
> Andrew> +    (host_string_to_python_string (hex_form.c_str ()).release ());
>
> The .release() here is weird, since this can just be
>
>    gdbpy_ref<> pyo_buildid = host_string_to_python_string (...);
>
> Andrew> +  /* Convert MISSING_FILENAME to a Python object.  */
> Andrew> +  gdbpy_ref<> pyo_filename;
> Andrew> +  pyo_filename = host_string_to_python_string (missing_flilename);
>
> Could combine these two lines.
>
> Andrew> +  if (PyBool_Check (pyo_execute_ret.get ()))
> Andrew> +    {
> Andrew> +      bool try_again = PyObject_IsTrue (pyo_execute_ret.get ());
>
> Technically PyObject_IsTrue can return -1 to mean an error occurred.
> It's probably impossible here, but maybe comparing '== Py_True' would
> avoid this possibility.
>
> Andrew> +		       "be None, a Bool, or a String");
>
> I think the Python types aren't capitalized and the string type is named
> 'str'.

Thanks for the review.  I believe the version below has addressed all
your feedback.  Let me know what you think.

Thanks,
Andrew

---

commit e915b58444a150451d47ee92c12e40ac515c34ec
Author: Andrew Burgess <aburgess@redhat.com>
Date:   Wed Jul 31 15:55:57 2024 +0100

    gdb/python: implement Python find_exec_by_build_id hook
    
    Implement extension_language_ops::find_objfile_from_buildid within
    GDB's Python API.  Doing this allows users to write Python extensions
    that can help locate missing objfiles when GDB opens a core file.  A
    handler might perform some project- or site-specific actions to find a
    missing objfile.  Or might provide some project- or site-specific
    advice to the user on how they can obtain the missing objfile.
    
    The implementation is very similar to the approach taken in:
    
      commit 8f6c452b5a4e50fbb55ff1d13328b392ad1fd416
      Date:   Sun Oct 15 22:48:42 2023 +0100
    
          gdb: implement missing debug handler hook for Python
    
    The following new commands are added as commands implemented in
    Python, this is similar to how the Python missing debug and unwinder
    commands are implemented:
    
      info missing-objfile-handlers
      enable missing-objfile-handler LOCUS HANDLER
      disable missing-objfile-handler LOCUS HANDLER
    
    To make use of this extension hook a user will create missing objfile
    handler objects, and registers these handlers with GDB.  When GDB
    opens a core file and encounters a missing objfile each handler is
    called in turn until one is able to help.  Here is a minimal handler
    that does nothing useful:
    
      import gdb
      import gdb.missing_objfile
    
      class MyFirstHandler(gdb.missing_objfile.MissingObjfileHandler):
          def __init__(self):
              super().__init__("my_first_handler")
    
          def __call__(self, pspace, build_id, filename):
              # This handler does nothing useful.
              return None
    
      gdb.missing_objfile.register_handler(None, MyFirstHandler())
    
    Returning None from the __call__ method tells GDB that this handler
    was unable to find the missing objfile, and GDB should ask any other
    registered handlers.
    
    Possible return values from a handler:
    
      - None: This means the handler couldn't help.  GDB will call other
              registered handlers to see if they can help instead.
    
      - False: The handler has done all it can, but the objfile couldn't
                be found.  GDB will not call any other handlers, and will
                continue without the objfile.
    
      - True: The handler has installed the objfile into a location where
              GDB would normally expect to find it.  GDB should repeat its
              normal lookup process and the objfile should now be found.
    
      - A string: The handler can return a filename, which is the missing
                  objfile.  GDB will load this file.
    
    Handlers can be registered globally, or per program space.  GDB checks
    the handlers for the current program space first, and then all of the
    global handles.  The first handler that returns a value that is not
    None, has "handled" the missing objfile, at which point GDB continues.
    
    The implementation of this feature is mostly straight forward.  I have
    reworked some of the missing debug file related code so that it can be
    shared with this feature.  E.g. gdb/python/lib/gdb/missing_files.py is
    mostly content moved from gdb/python/lib/gdb/missing_debug.py, but
    updated to be more generic.  Now gdb/python/lib/gdb/missing_debug.py
    and the new file gdb/python/lib/gdb/missing_objfile.py both call into
    the missing_files.py file.
    
    For gdb/python/lib/gdb/command/missing_files.py this is even more
    extreme, gdb/python/lib/gdb/command/missing_debug.py is completely
    gone now and gdb/python/lib/gdb/command/missing_files.py provides all
    of the new commands in a generic way.
    
    I have made one change to the existing Python API, I renamed the
    attribute Progspace.missing_debug_handlers to
    Progspace.missing_file_handlers.  I don't see this as too
    problematic.  This attribute was only used to implement the missing
    debug feature and was never documented beyond the fact that it
    existed.  There was no reason for users to be touching this attribute.
    
    Reviewed-By: Eli Zaretskii <eliz@gnu.org>

diff --git a/gdb/NEWS b/gdb/NEWS
index 42668cbc057..44f844ae5b1 100644
--- a/gdb/NEWS
+++ b/gdb/NEWS
@@ -54,6 +54,17 @@
   ** Added gdb.record.clear.  Clears the trace data of the current recording.
      This forces re-decoding of the trace for successive commands.
 
+  ** 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.
+
 * Debugger Adapter Protocol changes
 
   ** The "scopes" request will now return a scope holding global
@@ -81,6 +92,19 @@ maintenance info blocks [ADDRESS]
   are listed starting at the inner global block out to the most inner
   block.
 
+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.
+
 * Changed commands
 
 remove-symbol-file
diff --git a/gdb/data-directory/Makefile.in b/gdb/data-directory/Makefile.in
index 1a17d23c132..287dc7ff739 100644
--- 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/ptwrite.py \
@@ -87,7 +89,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/doc/gdb.texinfo b/gdb/doc/gdb.texinfo
index 46ca62ec0c3..0ba5e56cff9 100644
--- 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
diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi
index 22f0e6c6d0a..3a83791aec7 100644
--- 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
@@ -5388,10 +5389,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:
@@ -5567,6 +5569,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}.
@@ -8155,6 +8158,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/python/lib/gdb/__init__.py b/gdb/python/lib/gdb/__init__.py
index 6c3e2419a40..146a963044f 100644
--- 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
index 313b88cf8c3..463853b8cec 100644
--- 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
index 7ccc4fe496d..2c2cebacaf8 100644
--- a/gdb/python/lib/gdb/missing_debug.py
+++ b/gdb/python/lib/gdb/missing_debug.py
@@ -17,72 +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:
-    # 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 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
@@ -93,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
@@ -148,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
index 00000000000..5f2df88c728
--- /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
index 00000000000..ace0e1315b5
--- /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
index aa1e713cc53..bb44aa42567 100644
--- 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;
 }
@@ -778,8 +778,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
index cceb7aa01c3..b0de48d4e1a 100644
--- 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"
@@ -130,6 +131,9 @@ static std::optional<std::string> gdbpy_colorize_disasm
 (const std::string &content, gdbarch *gdbarch);
 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.  */
 
@@ -179,7 +183,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 */
@@ -1829,6 +1834,107 @@ gdbpy_handle_missing_debuginfo (const struct extension_language_defn *extlang,
   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
    EXT_PRINTERS->py_type_printers.  The product of this function is used by
    gdbpy_apply_type_printers, and freed by gdbpy_free_type_printers.
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
index 00000000000..8d740b4bb71
--- /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
index 00000000000..953e1c0fb5b
--- /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
index 00000000000..51027c9ca80
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-missing-objfile.exp
@@ -0,0 +1,544 @@
+# 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
+
+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
+}
+
+set remote_python_file \
+    [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py]
+
+# Generate a core file.
+set corefile [core_find $binfile {}]
+if {$corefile == ""} {
+    unsupport "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"
+
+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
index 00000000000..0e5f91e9b32
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-missing-objfile.py
@@ -0,0 +1,158 @@
+# 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
+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)
+            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")


^ permalink raw reply	[flat|nested] 24+ messages in thread

* [PATCHv2 0/4] Python API to find missing objfiles
  2024-09-10 16:56 [PATCH 0/4] Python API to find missing objfiles Andrew Burgess
                   ` (3 preceding siblings ...)
  2024-09-10 16:56 ` [PATCH 4/4] gdb/python: implement Python find_exec_by_build_id hook Andrew Burgess
@ 2024-10-27  9:10 ` Andrew Burgess
  2024-10-27  9:10   ` [PATCHv2 1/4] gdb: use mapped file information to improve debuginfod text Andrew Burgess
                     ` (4 more replies)
  4 siblings, 5 replies; 24+ messages in thread
From: Andrew Burgess @ 2024-10-27  9:10 UTC (permalink / raw)
  To: gdb-patches; +Cc: Andrew Burgess

In v2:

  - Addressed Tom's feedback on patch #4.

  - Rebased onto current HEAD.

  - Retested.

---

This series provides a Python API which can be used to find missing
objfiles when opening a core file.  This is very similar in spirit to
the already existing code that provides a Python API for finding
missing debug information.

The first patch in this series cleans up the message that debuginfod
produces.  This is only relevant because the cleanup changes which
filenames are passed around, these same filenames are then passed into
the new Python API.

Patch #2 is a refactoring.

Patch #3 adds the new extension language API on the GDB core side.

Patch #4 adds the Python side of the same API.

For reference, see these commits for when the missing debug files
Python API was added:

  * 8f6c452b5a4 gdb: implement missing debug handler hook for Python
  * 661d98a3331 gdb: add an extension language hook for missing debug info

Thanks,
Andrew

---

Andrew Burgess (4):
  gdb: use mapped file information to improve debuginfod text
  gdb: rename ext_lang_missing_debuginfo_result
  gdb: add extension hook ext_lang_find_objfile_from_buildid
  gdb/python: implement Python find_exec_by_build_id hook

 gdb/NEWS                                      |  24 +
 gdb/build-id.c                                |  88 ++-
 gdb/build-id.h                                |   3 +-
 gdb/corelow.c                                 |  63 +-
 gdb/data-directory/Makefile.in                |   4 +-
 gdb/debuginfod-support.c                      |   4 +-
 gdb/doc/gdb.texinfo                           |   1 +
 gdb/doc/python.texi                           | 194 ++++++-
 gdb/extension-priv.h                          |  15 +-
 gdb/extension.c                               |  26 +-
 gdb/extension.h                               |  30 +-
 gdb/python/lib/gdb/__init__.py                | 110 +++-
 .../{missing_debug.py => missing_files.py}    | 135 +++--
 gdb/python/lib/gdb/missing_debug.py           | 161 +-----
 gdb/python/lib/gdb/missing_files.py           | 204 +++++++
 gdb/python/lib/gdb/missing_objfile.py         |  67 +++
 gdb/python/py-progspace.c                     |  26 +-
 gdb/python/python.c                           | 120 +++-
 gdb/solib.c                                   |   3 +-
 gdb/symfile-debug.c                           |   2 +-
 .../gdb.debuginfod/corefile-mapped-file.exp   |   2 +-
 .../gdb.debuginfod/solib-with-soname.exp      |   2 +-
 .../gdb.python/py-missing-objfile-lib.c       |  35 ++
 gdb/testsuite/gdb.python/py-missing-objfile.c |  49 ++
 .../gdb.python/py-missing-objfile.exp         | 544 ++++++++++++++++++
 .../gdb.python/py-missing-objfile.py          | 158 +++++
 26 files changed, 1787 insertions(+), 283 deletions(-)
 rename gdb/python/lib/gdb/command/{missing_debug.py => missing_files.py} (54%)
 create mode 100644 gdb/python/lib/gdb/missing_files.py
 create mode 100644 gdb/python/lib/gdb/missing_objfile.py
 create mode 100644 gdb/testsuite/gdb.python/py-missing-objfile-lib.c
 create mode 100644 gdb/testsuite/gdb.python/py-missing-objfile.c
 create mode 100644 gdb/testsuite/gdb.python/py-missing-objfile.exp
 create mode 100644 gdb/testsuite/gdb.python/py-missing-objfile.py


base-commit: a723c56efb07c4f8b3f6a3ed4b878a2f8f5572cc
-- 
2.25.4


^ permalink raw reply	[flat|nested] 24+ messages in thread

* [PATCHv2 1/4] gdb: use mapped file information to improve debuginfod text
  2024-10-27  9:10 ` [PATCHv2 0/4] Python API to find missing objfiles Andrew Burgess
@ 2024-10-27  9:10   ` Andrew Burgess
  2024-10-27  9:10   ` [PATCHv2 2/4] gdb: rename ext_lang_missing_debuginfo_result Andrew Burgess
                     ` (3 subsequent siblings)
  4 siblings, 0 replies; 24+ messages in thread
From: Andrew Burgess @ 2024-10-27  9:10 UTC (permalink / raw)
  To: gdb-patches; +Cc: Andrew Burgess, Tom Tromey

When opening a core-file GDB is able to use debuginfod to download the
executable that matches the core-file if GDB can find a build-id for
the executable in the core-file.

In this case GDB calls debuginfod_exec_query to download the
executable and GDB prints a message like:

  Downloading executable for /path/to/core-file...

which makes sense in that case.

For a long time GDB has also had the ability to download memory-mapped
files and shared libraries when opening a core-file.  However, recent
commits have made these cases more likely to trigger, which is a good
thing, but the messaging from GDB in these cases is not ideal.  When
downloading a memory-mapped file GDB prints:

  Downloading executable for /path/to/memory-mapped-file

And for a shared library:

  Downloading executable for /path/to/libfoo.so

These last two messages could, I think, be improved.

I propose making two changes.  First, I suggest instead of using
/path/to/core-file in the first case, we use the name of the
executable that GDB is fetching.  This makes the messaging consistent
in that we print the name of the file we're fetching rather than the
name of the file we're fetching something for.

I further propose that we replace 'executable for' with the more
generic word 'file'.  The messages will then become:

  Downloading file /path/to/exec-file...
  Downloading file /path/to/memory-mapped-file...
  Downloading file /path/to/libfoo.so...

I think these messages are clearer than what we used to have, and they
are consistent in that we name the thing being downloaded in all
cases.

There is one tiny problem.  The first case relies on GDB knowing the
name of the executable it wants to download.  The only place we can
currently get that from is, I think, the memory-mapped file list.

[ ASIDE: There is `bfd_core_file_failing_command` which reports the
  executable and argument list from the core file, but this
  information is not ideal for this task.  First, the executable and
  arguments are merged into a single string, and second, the string is
  a relatively short, fixed length string, so the executable name is
  often truncated.  For these reasons I don't consider fetching the
  executable name using this bfd function as a solution. ]

We do have to consider the case that the core file does not have any
mapped file information.  This shouldn't ever be the case for a Linux
target, but it's worth considering.

[ ASIDE: I mention Linux specifically because this only becomes a
  problem if we try to do a lookup via debuginfod, which requires that
  we have build-ids available.  Linux has special support for
  embedding build-ids into the core file, but I'm not sure if other
  kernels do this. ]

For the unlikely edge case of a core-file that has build-ids, but
doesn't have any mapped file information then I propose that we
synthesis a filename like: 'with build-id xxxxxx'.  We would then see
a message like:

  Downloading file with build-id xxxxxx...

Where 'xxxxxx' would be replaced by the actual build-id.

This isn't ideal, but I think is good enough, and, as I said, I think
this case is not going to be hit very often, or maybe at all.

We already had some tests that emitted two of the above messages,
which I've updated, these cover the mapped-file and shared library
case.

The message about downloading the exec for the core-file is actually
really hard to trigger now as usually the exec will also appear in the
memory-mapped file list and GDB will download the file at this stage.
Then when GDB needs the executable for loading the symbols it'll ask
debuginfod, and debuginfod will find the file in its cache, and so no
message will be printed.

If anyone has any ideas about how to trigger this case then I'm happy
to add additional tests.

Approved-By: Tom Tromey <tom@tromey.com>
---
 gdb/corelow.c                                 | 59 ++++++++++++++++++-
 gdb/debuginfod-support.c                      |  4 +-
 .../gdb.debuginfod/corefile-mapped-file.exp   |  2 +-
 .../gdb.debuginfod/solib-with-soname.exp      |  2 +-
 4 files changed, 60 insertions(+), 7 deletions(-)

diff --git a/gdb/corelow.c b/gdb/corelow.c
index 5820ffed332..87ce04d6852 100644
--- a/gdb/corelow.c
+++ b/gdb/corelow.c
@@ -255,6 +255,27 @@ class core_target final : public process_stratum_target
     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.  */
@@ -289,6 +310,11 @@ class core_target final : public process_stratum_target
   /* 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 ()
@@ -418,11 +444,23 @@ core_target::build_file_mappings ()
 	  }
       });
 
+  /* Get the build-id of the core file.  */
+  const bfd_build_id *core_build_id
+    = build_id_bfd_get (current_program_space->core_bfd ());
+
   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
@@ -832,14 +870,29 @@ 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;
 
+  /* The filename used for the find_objfile_by_build_id call.  */
+  std::string filename;
+
+  if (!target->expected_exec_filename ().empty ())
+    filename = target->expected_exec_filename ();
+  else
+    {
+      /* 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 (build_id, abfd->filename);
+    = find_objfile_by_build_id (build_id, filename.c_str ());
 
   if (execbfd != nullptr)
     {
@@ -972,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);
 
diff --git a/gdb/debuginfod-support.c b/gdb/debuginfod-support.c
index 841b6f2078c..9460ae18dd6 100644
--- 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/testsuite/gdb.debuginfod/corefile-mapped-file.exp b/gdb/testsuite/gdb.debuginfod/corefile-mapped-file.exp
index cf96b41ac9a..cad70aaa3c6 100644
--- a/gdb/testsuite/gdb.debuginfod/corefile-mapped-file.exp
+++ b/gdb/testsuite/gdb.debuginfod/corefile-mapped-file.exp
@@ -370,7 +370,7 @@ with_debuginfod_env $cache {
     gdb_load $binfile
 
     load_core_file "load corefile, download library from debuginfod" \
-	"Downloading\[^\r\n\]* executable for [string_to_regexp $library_filename]\\.\\.\\."
+	"Downloading\[^\r\n\]* file [string_to_regexp $library_filename]\\.\\.\\."
 
     set ptr_value [read_ptr_value]
     gdb_assert { $ptr_value == $ptr_expected_value } \
diff --git a/gdb/testsuite/gdb.debuginfod/solib-with-soname.exp b/gdb/testsuite/gdb.debuginfod/solib-with-soname.exp
index 9ef12041dc6..31ca7181af6 100644
--- a/gdb/testsuite/gdb.debuginfod/solib-with-soname.exp
+++ b/gdb/testsuite/gdb.debuginfod/solib-with-soname.exp
@@ -161,7 +161,7 @@ proc load_exec_and_core_file { expect_warning expect_download testname \
 		set saw_warning true
 		exp_continue
 	    }
-	    -re "^Downloading executable for \[^\r\n\]+/libfoo_1\\.so\\.\\.\\.\r\n" {
+	    -re "^Downloading file \[^\r\n\]+/libfoo_1\\.so\\.\\.\\.\r\n" {
 		set saw_download true
 		exp_continue
 	    }
-- 
2.25.4


^ permalink raw reply	[flat|nested] 24+ messages in thread

* [PATCHv2 2/4] gdb: rename ext_lang_missing_debuginfo_result
  2024-10-27  9:10 ` [PATCHv2 0/4] Python API to find missing objfiles Andrew Burgess
  2024-10-27  9:10   ` [PATCHv2 1/4] gdb: use mapped file information to improve debuginfod text Andrew Burgess
@ 2024-10-27  9:10   ` Andrew Burgess
  2024-10-27  9:10   ` [PATCHv2 3/4] gdb: add extension hook ext_lang_find_objfile_from_buildid Andrew Burgess
                     ` (2 subsequent siblings)
  4 siblings, 0 replies; 24+ messages in thread
From: Andrew Burgess @ 2024-10-27  9:10 UTC (permalink / raw)
  To: gdb-patches; +Cc: Andrew Burgess, Tom Tromey

In preparation for later commits in this series, rename
ext_lang_missing_debuginfo_result to ext_lang_missing_file_result.

A later commit will add additional Python APIs to handle different
types of missing files beyond just debuginfo.

This is just a rename commit, there should be no functional changes
after this commit.

Approved-By: Tom Tromey <tom@tromey.com>
---
 gdb/extension-priv.h |  2 +-
 gdb/extension.c      |  4 ++--
 gdb/extension.h      | 10 +++++-----
 gdb/python/python.c  | 12 ++++++------
 gdb/symfile-debug.c  |  2 +-
 5 files changed, 15 insertions(+), 15 deletions(-)

diff --git a/gdb/extension-priv.h b/gdb/extension-priv.h
index 653fd51e2f1..90810e4bbc8 100644
--- a/gdb/extension-priv.h
+++ b/gdb/extension-priv.h
@@ -292,7 +292,7 @@ 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);
 };
diff --git a/gdb/extension.c b/gdb/extension.c
index 897bf25dad4..15e1f11d619 100644
--- a/gdb/extension.c
+++ b/gdb/extension.c
@@ -1053,7 +1053,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)
@@ -1061,7 +1061,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;
diff --git a/gdb/extension.h b/gdb/extension.h
index 5b0830b6675..2a749161508 100644
--- a/gdb/extension.h
+++ b/gdb/extension.h
@@ -361,23 +361,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.  */ }
 
@@ -407,7 +407,7 @@ 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);
 
 #if GDB_SELF_TEST
diff --git a/gdb/python/python.c b/gdb/python/python.c
index cc06526ffcf..cceb7aa01c3 100644
--- a/gdb/python/python.c
+++ b/gdb/python/python.c
@@ -128,7 +128,7 @@ 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);
 
 /* The interface between gdb proper and loading of python scripts.  */
@@ -1755,10 +1755,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)
 {
@@ -1806,7 +1806,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 ()))
@@ -1826,7 +1826,7 @@ 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 ()));
 }
 
 /* Compute the list of active python type printers and store them in
diff --git a/gdb/symfile-debug.c b/gdb/symfile-debug.c
index 6bf8dc3cd82..b21a5a433f3 100644
--- a/gdb/symfile-debug.c
+++ b/gdb/symfile-debug.c
@@ -630,7 +630,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 ())
 	    {
-- 
2.25.4


^ permalink raw reply	[flat|nested] 24+ messages in thread

* [PATCHv2 3/4] gdb: add extension hook ext_lang_find_objfile_from_buildid
  2024-10-27  9:10 ` [PATCHv2 0/4] Python API to find missing objfiles Andrew Burgess
  2024-10-27  9:10   ` [PATCHv2 1/4] gdb: use mapped file information to improve debuginfod text Andrew Burgess
  2024-10-27  9:10   ` [PATCHv2 2/4] gdb: rename ext_lang_missing_debuginfo_result Andrew Burgess
@ 2024-10-27  9:10   ` Andrew Burgess
  2024-10-27  9:10   ` [PATCHv2 4/4] gdb/python: implement Python find_exec_by_build_id hook Andrew Burgess
  2024-11-05 13:15   ` [PATCHv3 0/4] Python API to find missing objfiles Andrew Burgess
  4 siblings, 0 replies; 24+ messages in thread
From: Andrew Burgess @ 2024-10-27  9:10 UTC (permalink / raw)
  To: gdb-patches; +Cc: Andrew Burgess, Tom Tromey

Add a new ext_lang_find_objfile_from_buildid function which is called
from find_objfile_by_build_id and gives extension languages a chance
to find missing objfiles.

This commit adds the ext_lang_find_objfile_from_buildid function and
the extension_language_ops::find_objfile_from_buildid() hook, but does
not implement the hook for any extension languages, that will come in
the next commit.

This commit does rewrite find_objfile_by_build_id (build-id.c) to call
the new hook though.  The basic steps of find_objfile_by_build_id are
now this:

  1. Try to find the missing objfile using the build-id by looking in
  the debug-file-directory's .build-id/ sub-directory.  If we find the
  file then we're done.

  2. Ask debuginfod to download the missing file for us.  If we
  download the file successfully then we're done.

  3. Ask the extension language hook to find the file for us.  If the
  extension language asks us to try again then we repeat step (1) only
  and if we still don't have the file, we move to step (4).  If the
  extension language told us where the file is then we use that file
  and we're done.

  4. We didn't find the file.  Carry on without it.

Only step (3) is new in this logic, everything else was already done.

There are no tests added here as we can't currently write an extension
language callback.  The next commit will add the tests.

Approved-By: Tom Tromey <tom@tromey.com>
---
 gdb/build-id.c       | 88 ++++++++++++++++++++++++++++++++------------
 gdb/build-id.h       |  3 +-
 gdb/corelow.c        |  6 ++-
 gdb/extension-priv.h | 13 +++++++
 gdb/extension.c      | 22 +++++++++++
 gdb/extension.h      | 20 ++++++++++
 gdb/solib.c          |  3 +-
 7 files changed, 128 insertions(+), 27 deletions(-)

diff --git a/gdb/build-id.c b/gdb/build-id.c
index 6266be1bd6d..9d4b005489d 100644
--- a/gdb/build-id.c
+++ b/gdb/build-id.c
@@ -28,6 +28,7 @@
 #include "cli/cli-style.h"
 #include "gdbsupport/scoped_fd.h"
 #include "debuginfod-support.h"
+#include "extension.h"
 
 /* See build-id.h.  */
 
@@ -343,32 +344,73 @@ find_separate_debug_file_by_buildid (struct objfile *objfile,
 /* See build-id.h.  */
 
 gdb_bfd_ref_ptr
-find_objfile_by_build_id (const bfd_build_id *build_id,
+find_objfile_by_build_id (program_space *pspace,
+			  const bfd_build_id *build_id,
 			  const char *expected_filename)
 {
-  /* Try to find the executable (or shared object) by looking for a
-     (sym)link on disk from the build-id to the object file.  */
-  gdb_bfd_ref_ptr abfd = build_id_to_exec_bfd (build_id->size,
-					       build_id->data);
-
-  if (abfd != nullptr)
-    return abfd;
-
-  /* Attempt to query debuginfod for the executable.  */
-  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)
+  gdb_bfd_ref_ptr abfd;
+
+  for (unsigned attempt = 0, max_attempts = 1;
+       attempt < max_attempts && abfd == nullptr;
+       ++attempt)
     {
-      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;
+      /* 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
index 1d2e7891f34..0174412c5bd 100644
--- a/gdb/build-id.h
+++ b/gdb/build-id.h
@@ -66,7 +66,8 @@ extern std::string find_separate_debug_file_by_buildid
    should be the file we were looking for but couldn't find.  */
 
 extern gdb_bfd_ref_ptr find_objfile_by_build_id
-  (const bfd_build_id *build_id, const char *expected_filename);
+  (struct program_space *pspace, const bfd_build_id *build_id,
+   const char *expected_filename);
 
 /* Return an hex-string representation of BUILD_ID.  */
 
diff --git a/gdb/corelow.c b/gdb/corelow.c
index 87ce04d6852..25de755a1dd 100644
--- a/gdb/corelow.c
+++ b/gdb/corelow.c
@@ -506,7 +506,8 @@ core_target::build_file_mappings ()
 	   || !bfd_check_format (abfd.get (), bfd_object))
 	  && file_data.build_id != nullptr)
 	{
-	  abfd = find_objfile_by_build_id (file_data.build_id,
+	  abfd = find_objfile_by_build_id (current_program_space,
+					   file_data.build_id,
 					   filename.c_str ());
 
 	  if (abfd != nullptr)
@@ -892,7 +893,8 @@ locate_exec_from_corefile_build_id (bfd *abfd, core_target *target,
     }
 
   gdb_bfd_ref_ptr execbfd
-    = find_objfile_by_build_id (build_id, filename.c_str ());
+    = find_objfile_by_build_id (current_program_space, build_id,
+				filename.c_str ());
 
   if (execbfd != nullptr)
     {
diff --git a/gdb/extension-priv.h b/gdb/extension-priv.h
index 90810e4bbc8..a31283f831c 100644
--- a/gdb/extension-priv.h
+++ b/gdb/extension-priv.h
@@ -295,6 +295,19 @@ struct extension_language_ops
   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
index 15e1f11d619..ec1aa138872 100644
--- a/gdb/extension.c
+++ b/gdb/extension.c
@@ -1070,6 +1070,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
index 2a749161508..ad3b4e6102d 100644
--- 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.
@@ -410,6 +411,25 @@ struct ext_lang_missing_file_result
 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/solib.c b/gdb/solib.c
index 5c926def715..dfcee4309bd 100644
--- a/gdb/solib.c
+++ b/gdb/solib.c
@@ -525,7 +525,8 @@ solib_map_sections (solib &so)
 	    abfd = nullptr;
 
 	  if (abfd == nullptr)
-	    abfd = find_objfile_by_build_id (mapped_file_info->build_id (),
+	    abfd = find_objfile_by_build_id (current_program_space,
+					     mapped_file_info->build_id (),
 					     so.so_name.c_str ());
 
 	  if (abfd == nullptr && mismatch)
-- 
2.25.4


^ permalink raw reply	[flat|nested] 24+ messages in thread

* [PATCHv2 4/4] gdb/python: implement Python find_exec_by_build_id hook
  2024-10-27  9:10 ` [PATCHv2 0/4] Python API to find missing objfiles Andrew Burgess
                     ` (2 preceding siblings ...)
  2024-10-27  9:10   ` [PATCHv2 3/4] gdb: add extension hook ext_lang_find_objfile_from_buildid Andrew Burgess
@ 2024-10-27  9:10   ` Andrew Burgess
  2024-11-05 13:15   ` [PATCHv3 0/4] Python API to find missing objfiles Andrew Burgess
  4 siblings, 0 replies; 24+ messages in thread
From: Andrew Burgess @ 2024-10-27  9:10 UTC (permalink / raw)
  To: gdb-patches; +Cc: Andrew Burgess, Eli Zaretskii

Implement extension_language_ops::find_objfile_from_buildid within
GDB's Python API.  Doing this allows users to write Python extensions
that can help locate missing objfiles when GDB opens a core file.  A
handler might perform some project- or site-specific actions to find a
missing objfile.  Or might provide some project- or site-specific
advice to the user on how they can obtain the missing objfile.

The implementation is very similar to the approach taken in:

  commit 8f6c452b5a4e50fbb55ff1d13328b392ad1fd416
  Date:   Sun Oct 15 22:48:42 2023 +0100

      gdb: implement missing debug handler hook for Python

The following new commands are added as commands implemented in
Python, this is similar to how the Python missing debug and unwinder
commands are implemented:

  info missing-objfile-handlers
  enable missing-objfile-handler LOCUS HANDLER
  disable missing-objfile-handler LOCUS HANDLER

To make use of this extension hook a user will create missing objfile
handler objects, and registers these handlers with GDB.  When GDB
opens a core file and encounters a missing objfile each handler is
called in turn until one is able to help.  Here is a minimal handler
that does nothing useful:

  import gdb
  import gdb.missing_objfile

  class MyFirstHandler(gdb.missing_objfile.MissingObjfileHandler):
      def __init__(self):
          super().__init__("my_first_handler")

      def __call__(self, pspace, build_id, filename):
          # This handler does nothing useful.
          return None

  gdb.missing_objfile.register_handler(None, MyFirstHandler())

Returning None from the __call__ method tells GDB that this handler
was unable to find the missing objfile, and GDB should ask any other
registered handlers.

Possible return values from a handler:

  - None: This means the handler couldn't help.  GDB will call other
          registered handlers to see if they can help instead.

  - False: The handler has done all it can, but the objfile couldn't
            be found.  GDB will not call any other handlers, and will
	    continue without the objfile.

  - True: The handler has installed the objfile into a location where
          GDB would normally expect to find it.  GDB should repeat its
	  normal lookup process and the objfile should now be found.

  - A string: The handler can return a filename, which is the missing
              objfile.  GDB will load this file.

Handlers can be registered globally, or per program space.  GDB checks
the handlers for the current program space first, and then all of the
global handles.  The first handler that returns a value that is not
None, has "handled" the missing objfile, at which point GDB continues.

The implementation of this feature is mostly straight forward.  I have
reworked some of the missing debug file related code so that it can be
shared with this feature.  E.g. gdb/python/lib/gdb/missing_files.py is
mostly content moved from gdb/python/lib/gdb/missing_debug.py, but
updated to be more generic.  Now gdb/python/lib/gdb/missing_debug.py
and the new file gdb/python/lib/gdb/missing_objfile.py both call into
the missing_files.py file.

For gdb/python/lib/gdb/command/missing_files.py this is even more
extreme, gdb/python/lib/gdb/command/missing_debug.py is completely
gone now and gdb/python/lib/gdb/command/missing_files.py provides all
of the new commands in a generic way.

I have made one change to the existing Python API, I renamed the
attribute Progspace.missing_debug_handlers to
Progspace.missing_file_handlers.  I don't see this as too
problematic.  This attribute was only used to implement the missing
debug feature and was never documented beyond the fact that it
existed.  There was no reason for users to be touching this attribute.

Reviewed-By: Eli Zaretskii <eliz@gnu.org>
---
 gdb/NEWS                                      |  24 +
 gdb/data-directory/Makefile.in                |   4 +-
 gdb/doc/gdb.texinfo                           |   1 +
 gdb/doc/python.texi                           | 194 ++++++-
 gdb/python/lib/gdb/__init__.py                | 110 +++-
 .../{missing_debug.py => missing_files.py}    | 135 +++--
 gdb/python/lib/gdb/missing_debug.py           | 161 +-----
 gdb/python/lib/gdb/missing_files.py           | 204 +++++++
 gdb/python/lib/gdb/missing_objfile.py         |  67 +++
 gdb/python/py-progspace.c                     |  26 +-
 gdb/python/python.c                           | 108 +++-
 .../gdb.python/py-missing-objfile-lib.c       |  35 ++
 gdb/testsuite/gdb.python/py-missing-objfile.c |  49 ++
 .../gdb.python/py-missing-objfile.exp         | 544 ++++++++++++++++++
 .../gdb.python/py-missing-objfile.py          | 158 +++++
 15 files changed, 1585 insertions(+), 235 deletions(-)
 rename gdb/python/lib/gdb/command/{missing_debug.py => missing_files.py} (54%)
 create mode 100644 gdb/python/lib/gdb/missing_files.py
 create mode 100644 gdb/python/lib/gdb/missing_objfile.py
 create mode 100644 gdb/testsuite/gdb.python/py-missing-objfile-lib.c
 create mode 100644 gdb/testsuite/gdb.python/py-missing-objfile.c
 create mode 100644 gdb/testsuite/gdb.python/py-missing-objfile.exp
 create mode 100644 gdb/testsuite/gdb.python/py-missing-objfile.py

diff --git a/gdb/NEWS b/gdb/NEWS
index 42b8a88fd8a..9e0e526179d 100644
--- a/gdb/NEWS
+++ b/gdb/NEWS
@@ -56,6 +56,17 @@
   ** Added gdb.record.clear.  Clears the trace data of the current recording.
      This forces re-decoding of the trace for successive commands.
 
+  ** 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.
+
 * Debugger Adapter Protocol changes
 
   ** The "scopes" request will now return a scope holding global
@@ -83,6 +94,19 @@ maintenance info blocks [ADDRESS]
   are listed starting at the inner global block out to the most inner
   block.
 
+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.
+
 * Changed commands
 
 remove-symbol-file
diff --git a/gdb/data-directory/Makefile.in b/gdb/data-directory/Makefile.in
index 1a17d23c132..287dc7ff739 100644
--- 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/ptwrite.py \
@@ -87,7 +89,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/doc/gdb.texinfo b/gdb/doc/gdb.texinfo
index eb2aff974bb..844e66046c6 100644
--- a/gdb/doc/gdb.texinfo
+++ b/gdb/doc/gdb.texinfo
@@ -21792,6 +21792,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
diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi
index 22f0e6c6d0a..3a83791aec7 100644
--- 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
@@ -5388,10 +5389,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:
@@ -5567,6 +5569,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}.
@@ -8155,6 +8158,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/python/lib/gdb/__init__.py b/gdb/python/lib/gdb/__init__.py
index 6c3e2419a40..146a963044f 100644
--- 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
index 313b88cf8c3..463853b8cec 100644
--- 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
index 7ccc4fe496d..2c2cebacaf8 100644
--- a/gdb/python/lib/gdb/missing_debug.py
+++ b/gdb/python/lib/gdb/missing_debug.py
@@ -17,72 +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:
-    # 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 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
@@ -93,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
@@ -148,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
index 00000000000..5f2df88c728
--- /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
index 00000000000..ace0e1315b5
--- /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
index aa1e713cc53..bb44aa42567 100644
--- 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;
 }
@@ -778,8 +778,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
index cceb7aa01c3..b0de48d4e1a 100644
--- 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"
@@ -130,6 +131,9 @@ static std::optional<std::string> gdbpy_colorize_disasm
 (const std::string &content, gdbarch *gdbarch);
 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.  */
 
@@ -179,7 +183,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 */
@@ -1829,6 +1834,107 @@ gdbpy_handle_missing_debuginfo (const struct extension_language_defn *extlang,
   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
    EXT_PRINTERS->py_type_printers.  The product of this function is used by
    gdbpy_apply_type_printers, and freed by gdbpy_free_type_printers.
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
index 00000000000..8d740b4bb71
--- /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
index 00000000000..953e1c0fb5b
--- /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
index 00000000000..51027c9ca80
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-missing-objfile.exp
@@ -0,0 +1,544 @@
+# 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
+
+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
+}
+
+set remote_python_file \
+    [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py]
+
+# Generate a core file.
+set corefile [core_find $binfile {}]
+if {$corefile == ""} {
+    unsupport "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"
+
+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
index 00000000000..0e5f91e9b32
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-missing-objfile.py
@@ -0,0 +1,158 @@
+# 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
+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)
+            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")
-- 
2.25.4


^ permalink raw reply	[flat|nested] 24+ messages in thread

* [PATCHv3 0/4] Python API to find missing objfiles
  2024-10-27  9:10 ` [PATCHv2 0/4] Python API to find missing objfiles Andrew Burgess
                     ` (3 preceding siblings ...)
  2024-10-27  9:10   ` [PATCHv2 4/4] gdb/python: implement Python find_exec_by_build_id hook Andrew Burgess
@ 2024-11-05 13:15   ` Andrew Burgess
  2024-11-05 13:15     ` [PATCHv3 1/4] gdb: use mapped file information to improve debuginfod text Andrew Burgess
                       ` (4 more replies)
  4 siblings, 5 replies; 24+ messages in thread
From: Andrew Burgess @ 2024-11-05 13:15 UTC (permalink / raw)
  To: gdb-patches; +Cc: Andrew Burgess

I'm planning to push this series in the near future.

There was a review of v1 which didn't show up anything serious, and
the implementation for this feature matches the implementation of the
missing objfile feature, so I don't think it would make sense to make
any significant changes.

---

In v3:

  - Minor fixes for the tests in patch #4, this resolves the failures
    when run with check-all-boards.  No issues found in GDB.

  - Rebased.

In v2:

  - Addressed Tom's feedback on patch #4.

  - Rebased onto current HEAD.

  - Retested.

---

This series provides a Python API which can be used to find missing
objfiles when opening a core file.  This is very similar in spirit to
the already existing code that provides a Python API for finding
missing debug information.

The first patch in this series cleans up the message that debuginfod
produces.  This is only relevant because the cleanup changes which
filenames are passed around, these same filenames are then passed into
the new Python API.

Patch #2 is a refactoring.

Patch #3 adds the new extension language API on the GDB core side.

Patch #4 adds the Python side of the same API.

For reference, see these commits for when the missing debug files
Python API was added:

  * 8f6c452b5a4 gdb: implement missing debug handler hook for Python
  * 661d98a3331 gdb: add an extension language hook for missing debug info

Thanks,
Andrew

---

Andrew Burgess (4):
  gdb: use mapped file information to improve debuginfod text
  gdb: rename ext_lang_missing_debuginfo_result
  gdb: add extension hook ext_lang_find_objfile_from_buildid
  gdb/python: implement Python find_exec_by_build_id hook

 gdb/NEWS                                      |  24 +
 gdb/build-id.c                                |  88 ++-
 gdb/build-id.h                                |   3 +-
 gdb/corelow.c                                 |  63 +-
 gdb/data-directory/Makefile.in                |   4 +-
 gdb/debuginfod-support.c                      |   4 +-
 gdb/doc/gdb.texinfo                           |   1 +
 gdb/doc/python.texi                           | 194 +++++-
 gdb/extension-priv.h                          |  15 +-
 gdb/extension.c                               |  26 +-
 gdb/extension.h                               |  30 +-
 gdb/python/lib/gdb/__init__.py                | 110 +++-
 .../{missing_debug.py => missing_files.py}    | 135 +++--
 gdb/python/lib/gdb/missing_debug.py           | 161 +----
 gdb/python/lib/gdb/missing_files.py           | 204 +++++++
 gdb/python/lib/gdb/missing_objfile.py         |  67 +++
 gdb/python/py-progspace.c                     |  26 +-
 gdb/python/python.c                           | 120 +++-
 gdb/solib.c                                   |   3 +-
 gdb/symfile-debug.c                           |   2 +-
 .../gdb.debuginfod/corefile-mapped-file.exp   |   2 +-
 .../gdb.debuginfod/solib-with-soname.exp      |   2 +-
 .../gdb.python/py-missing-objfile-lib.c       |  35 ++
 gdb/testsuite/gdb.python/py-missing-objfile.c |  49 ++
 .../gdb.python/py-missing-objfile.exp         | 565 ++++++++++++++++++
 .../gdb.python/py-missing-objfile.py          | 167 ++++++
 26 files changed, 1817 insertions(+), 283 deletions(-)
 rename gdb/python/lib/gdb/command/{missing_debug.py => missing_files.py} (54%)
 create mode 100644 gdb/python/lib/gdb/missing_files.py
 create mode 100644 gdb/python/lib/gdb/missing_objfile.py
 create mode 100644 gdb/testsuite/gdb.python/py-missing-objfile-lib.c
 create mode 100644 gdb/testsuite/gdb.python/py-missing-objfile.c
 create mode 100644 gdb/testsuite/gdb.python/py-missing-objfile.exp
 create mode 100644 gdb/testsuite/gdb.python/py-missing-objfile.py


base-commit: ebc73070f4b85e4adee7d2f49cb32e2bd7b16324
-- 
2.25.4


^ permalink raw reply	[flat|nested] 24+ messages in thread

* [PATCHv3 1/4] gdb: use mapped file information to improve debuginfod text
  2024-11-05 13:15   ` [PATCHv3 0/4] Python API to find missing objfiles Andrew Burgess
@ 2024-11-05 13:15     ` Andrew Burgess
  2024-11-05 13:15     ` [PATCHv3 2/4] gdb: rename ext_lang_missing_debuginfo_result Andrew Burgess
                       ` (3 subsequent siblings)
  4 siblings, 0 replies; 24+ messages in thread
From: Andrew Burgess @ 2024-11-05 13:15 UTC (permalink / raw)
  To: gdb-patches; +Cc: Andrew Burgess, Tom Tromey

When opening a core-file GDB is able to use debuginfod to download the
executable that matches the core-file if GDB can find a build-id for
the executable in the core-file.

In this case GDB calls debuginfod_exec_query to download the
executable and GDB prints a message like:

  Downloading executable for /path/to/core-file...

which makes sense in that case.

For a long time GDB has also had the ability to download memory-mapped
files and shared libraries when opening a core-file.  However, recent
commits have made these cases more likely to trigger, which is a good
thing, but the messaging from GDB in these cases is not ideal.  When
downloading a memory-mapped file GDB prints:

  Downloading executable for /path/to/memory-mapped-file

And for a shared library:

  Downloading executable for /path/to/libfoo.so

These last two messages could, I think, be improved.

I propose making two changes.  First, I suggest instead of using
/path/to/core-file in the first case, we use the name of the
executable that GDB is fetching.  This makes the messaging consistent
in that we print the name of the file we're fetching rather than the
name of the file we're fetching something for.

I further propose that we replace 'executable for' with the more
generic word 'file'.  The messages will then become:

  Downloading file /path/to/exec-file...
  Downloading file /path/to/memory-mapped-file...
  Downloading file /path/to/libfoo.so...

I think these messages are clearer than what we used to have, and they
are consistent in that we name the thing being downloaded in all
cases.

There is one tiny problem.  The first case relies on GDB knowing the
name of the executable it wants to download.  The only place we can
currently get that from is, I think, the memory-mapped file list.

[ ASIDE: There is `bfd_core_file_failing_command` which reports the
  executable and argument list from the core file, but this
  information is not ideal for this task.  First, the executable and
  arguments are merged into a single string, and second, the string is
  a relatively short, fixed length string, so the executable name is
  often truncated.  For these reasons I don't consider fetching the
  executable name using this bfd function as a solution. ]

We do have to consider the case that the core file does not have any
mapped file information.  This shouldn't ever be the case for a Linux
target, but it's worth considering.

[ ASIDE: I mention Linux specifically because this only becomes a
  problem if we try to do a lookup via debuginfod, which requires that
  we have build-ids available.  Linux has special support for
  embedding build-ids into the core file, but I'm not sure if other
  kernels do this. ]

For the unlikely edge case of a core-file that has build-ids, but
doesn't have any mapped file information then I propose that we
synthesis a filename like: 'with build-id xxxxxx'.  We would then see
a message like:

  Downloading file with build-id xxxxxx...

Where 'xxxxxx' would be replaced by the actual build-id.

This isn't ideal, but I think is good enough, and, as I said, I think
this case is not going to be hit very often, or maybe at all.

We already had some tests that emitted two of the above messages,
which I've updated, these cover the mapped-file and shared library
case.

The message about downloading the exec for the core-file is actually
really hard to trigger now as usually the exec will also appear in the
memory-mapped file list and GDB will download the file at this stage.
Then when GDB needs the executable for loading the symbols it'll ask
debuginfod, and debuginfod will find the file in its cache, and so no
message will be printed.

If anyone has any ideas about how to trigger this case then I'm happy
to add additional tests.

Approved-By: Tom Tromey <tom@tromey.com>
---
 gdb/corelow.c                                 | 59 ++++++++++++++++++-
 gdb/debuginfod-support.c                      |  4 +-
 .../gdb.debuginfod/corefile-mapped-file.exp   |  2 +-
 .../gdb.debuginfod/solib-with-soname.exp      |  2 +-
 4 files changed, 60 insertions(+), 7 deletions(-)

diff --git a/gdb/corelow.c b/gdb/corelow.c
index 5820ffed332..87ce04d6852 100644
--- a/gdb/corelow.c
+++ b/gdb/corelow.c
@@ -255,6 +255,27 @@ class core_target final : public process_stratum_target
     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.  */
@@ -289,6 +310,11 @@ class core_target final : public process_stratum_target
   /* 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 ()
@@ -418,11 +444,23 @@ core_target::build_file_mappings ()
 	  }
       });
 
+  /* Get the build-id of the core file.  */
+  const bfd_build_id *core_build_id
+    = build_id_bfd_get (current_program_space->core_bfd ());
+
   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
@@ -832,14 +870,29 @@ 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;
 
+  /* The filename used for the find_objfile_by_build_id call.  */
+  std::string filename;
+
+  if (!target->expected_exec_filename ().empty ())
+    filename = target->expected_exec_filename ();
+  else
+    {
+      /* 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 (build_id, abfd->filename);
+    = find_objfile_by_build_id (build_id, filename.c_str ());
 
   if (execbfd != nullptr)
     {
@@ -972,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);
 
diff --git a/gdb/debuginfod-support.c b/gdb/debuginfod-support.c
index 841b6f2078c..9460ae18dd6 100644
--- 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/testsuite/gdb.debuginfod/corefile-mapped-file.exp b/gdb/testsuite/gdb.debuginfod/corefile-mapped-file.exp
index cf96b41ac9a..cad70aaa3c6 100644
--- a/gdb/testsuite/gdb.debuginfod/corefile-mapped-file.exp
+++ b/gdb/testsuite/gdb.debuginfod/corefile-mapped-file.exp
@@ -370,7 +370,7 @@ with_debuginfod_env $cache {
     gdb_load $binfile
 
     load_core_file "load corefile, download library from debuginfod" \
-	"Downloading\[^\r\n\]* executable for [string_to_regexp $library_filename]\\.\\.\\."
+	"Downloading\[^\r\n\]* file [string_to_regexp $library_filename]\\.\\.\\."
 
     set ptr_value [read_ptr_value]
     gdb_assert { $ptr_value == $ptr_expected_value } \
diff --git a/gdb/testsuite/gdb.debuginfod/solib-with-soname.exp b/gdb/testsuite/gdb.debuginfod/solib-with-soname.exp
index 9ef12041dc6..31ca7181af6 100644
--- a/gdb/testsuite/gdb.debuginfod/solib-with-soname.exp
+++ b/gdb/testsuite/gdb.debuginfod/solib-with-soname.exp
@@ -161,7 +161,7 @@ proc load_exec_and_core_file { expect_warning expect_download testname \
 		set saw_warning true
 		exp_continue
 	    }
-	    -re "^Downloading executable for \[^\r\n\]+/libfoo_1\\.so\\.\\.\\.\r\n" {
+	    -re "^Downloading file \[^\r\n\]+/libfoo_1\\.so\\.\\.\\.\r\n" {
 		set saw_download true
 		exp_continue
 	    }
-- 
2.25.4


^ permalink raw reply	[flat|nested] 24+ messages in thread

* [PATCHv3 2/4] gdb: rename ext_lang_missing_debuginfo_result
  2024-11-05 13:15   ` [PATCHv3 0/4] Python API to find missing objfiles Andrew Burgess
  2024-11-05 13:15     ` [PATCHv3 1/4] gdb: use mapped file information to improve debuginfod text Andrew Burgess
@ 2024-11-05 13:15     ` Andrew Burgess
  2024-11-05 13:15     ` [PATCHv3 3/4] gdb: add extension hook ext_lang_find_objfile_from_buildid Andrew Burgess
                       ` (2 subsequent siblings)
  4 siblings, 0 replies; 24+ messages in thread
From: Andrew Burgess @ 2024-11-05 13:15 UTC (permalink / raw)
  To: gdb-patches; +Cc: Andrew Burgess, Tom Tromey

In preparation for later commits in this series, rename
ext_lang_missing_debuginfo_result to ext_lang_missing_file_result.

A later commit will add additional Python APIs to handle different
types of missing files beyond just debuginfo.

This is just a rename commit, there should be no functional changes
after this commit.

Approved-By: Tom Tromey <tom@tromey.com>
---
 gdb/extension-priv.h |  2 +-
 gdb/extension.c      |  4 ++--
 gdb/extension.h      | 10 +++++-----
 gdb/python/python.c  | 12 ++++++------
 gdb/symfile-debug.c  |  2 +-
 5 files changed, 15 insertions(+), 15 deletions(-)

diff --git a/gdb/extension-priv.h b/gdb/extension-priv.h
index 653fd51e2f1..90810e4bbc8 100644
--- a/gdb/extension-priv.h
+++ b/gdb/extension-priv.h
@@ -292,7 +292,7 @@ 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);
 };
diff --git a/gdb/extension.c b/gdb/extension.c
index 897bf25dad4..15e1f11d619 100644
--- a/gdb/extension.c
+++ b/gdb/extension.c
@@ -1053,7 +1053,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)
@@ -1061,7 +1061,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;
diff --git a/gdb/extension.h b/gdb/extension.h
index 5b0830b6675..2a749161508 100644
--- a/gdb/extension.h
+++ b/gdb/extension.h
@@ -361,23 +361,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.  */ }
 
@@ -407,7 +407,7 @@ 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);
 
 #if GDB_SELF_TEST
diff --git a/gdb/python/python.c b/gdb/python/python.c
index cc06526ffcf..cceb7aa01c3 100644
--- a/gdb/python/python.c
+++ b/gdb/python/python.c
@@ -128,7 +128,7 @@ 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);
 
 /* The interface between gdb proper and loading of python scripts.  */
@@ -1755,10 +1755,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)
 {
@@ -1806,7 +1806,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 ()))
@@ -1826,7 +1826,7 @@ 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 ()));
 }
 
 /* Compute the list of active python type printers and store them in
diff --git a/gdb/symfile-debug.c b/gdb/symfile-debug.c
index 6bf8dc3cd82..b21a5a433f3 100644
--- a/gdb/symfile-debug.c
+++ b/gdb/symfile-debug.c
@@ -630,7 +630,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 ())
 	    {
-- 
2.25.4


^ permalink raw reply	[flat|nested] 24+ messages in thread

* [PATCHv3 3/4] gdb: add extension hook ext_lang_find_objfile_from_buildid
  2024-11-05 13:15   ` [PATCHv3 0/4] Python API to find missing objfiles Andrew Burgess
  2024-11-05 13:15     ` [PATCHv3 1/4] gdb: use mapped file information to improve debuginfod text Andrew Burgess
  2024-11-05 13:15     ` [PATCHv3 2/4] gdb: rename ext_lang_missing_debuginfo_result Andrew Burgess
@ 2024-11-05 13:15     ` Andrew Burgess
  2024-11-05 13:15     ` [PATCHv3 4/4] gdb/python: implement Python find_exec_by_build_id hook Andrew Burgess
  2024-11-10 10:21     ` [PATCHv3 0/4] Python API to find missing objfiles Andrew Burgess
  4 siblings, 0 replies; 24+ messages in thread
From: Andrew Burgess @ 2024-11-05 13:15 UTC (permalink / raw)
  To: gdb-patches; +Cc: Andrew Burgess, Tom Tromey

Add a new ext_lang_find_objfile_from_buildid function which is called
from find_objfile_by_build_id and gives extension languages a chance
to find missing objfiles.

This commit adds the ext_lang_find_objfile_from_buildid function and
the extension_language_ops::find_objfile_from_buildid() hook, but does
not implement the hook for any extension languages, that will come in
the next commit.

This commit does rewrite find_objfile_by_build_id (build-id.c) to call
the new hook though.  The basic steps of find_objfile_by_build_id are
now this:

  1. Try to find the missing objfile using the build-id by looking in
  the debug-file-directory's .build-id/ sub-directory.  If we find the
  file then we're done.

  2. Ask debuginfod to download the missing file for us.  If we
  download the file successfully then we're done.

  3. Ask the extension language hook to find the file for us.  If the
  extension language asks us to try again then we repeat step (1) only
  and if we still don't have the file, we move to step (4).  If the
  extension language told us where the file is then we use that file
  and we're done.

  4. We didn't find the file.  Carry on without it.

Only step (3) is new in this logic, everything else was already done.

There are no tests added here as we can't currently write an extension
language callback.  The next commit will add the tests.

Approved-By: Tom Tromey <tom@tromey.com>
---
 gdb/build-id.c       | 88 ++++++++++++++++++++++++++++++++------------
 gdb/build-id.h       |  3 +-
 gdb/corelow.c        |  6 ++-
 gdb/extension-priv.h | 13 +++++++
 gdb/extension.c      | 22 +++++++++++
 gdb/extension.h      | 20 ++++++++++
 gdb/solib.c          |  3 +-
 7 files changed, 128 insertions(+), 27 deletions(-)

diff --git a/gdb/build-id.c b/gdb/build-id.c
index 6266be1bd6d..9d4b005489d 100644
--- a/gdb/build-id.c
+++ b/gdb/build-id.c
@@ -28,6 +28,7 @@
 #include "cli/cli-style.h"
 #include "gdbsupport/scoped_fd.h"
 #include "debuginfod-support.h"
+#include "extension.h"
 
 /* See build-id.h.  */
 
@@ -343,32 +344,73 @@ find_separate_debug_file_by_buildid (struct objfile *objfile,
 /* See build-id.h.  */
 
 gdb_bfd_ref_ptr
-find_objfile_by_build_id (const bfd_build_id *build_id,
+find_objfile_by_build_id (program_space *pspace,
+			  const bfd_build_id *build_id,
 			  const char *expected_filename)
 {
-  /* Try to find the executable (or shared object) by looking for a
-     (sym)link on disk from the build-id to the object file.  */
-  gdb_bfd_ref_ptr abfd = build_id_to_exec_bfd (build_id->size,
-					       build_id->data);
-
-  if (abfd != nullptr)
-    return abfd;
-
-  /* Attempt to query debuginfod for the executable.  */
-  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)
+  gdb_bfd_ref_ptr abfd;
+
+  for (unsigned attempt = 0, max_attempts = 1;
+       attempt < max_attempts && abfd == nullptr;
+       ++attempt)
     {
-      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;
+      /* 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
index 1d2e7891f34..0174412c5bd 100644
--- a/gdb/build-id.h
+++ b/gdb/build-id.h
@@ -66,7 +66,8 @@ extern std::string find_separate_debug_file_by_buildid
    should be the file we were looking for but couldn't find.  */
 
 extern gdb_bfd_ref_ptr find_objfile_by_build_id
-  (const bfd_build_id *build_id, const char *expected_filename);
+  (struct program_space *pspace, const bfd_build_id *build_id,
+   const char *expected_filename);
 
 /* Return an hex-string representation of BUILD_ID.  */
 
diff --git a/gdb/corelow.c b/gdb/corelow.c
index 87ce04d6852..25de755a1dd 100644
--- a/gdb/corelow.c
+++ b/gdb/corelow.c
@@ -506,7 +506,8 @@ core_target::build_file_mappings ()
 	   || !bfd_check_format (abfd.get (), bfd_object))
 	  && file_data.build_id != nullptr)
 	{
-	  abfd = find_objfile_by_build_id (file_data.build_id,
+	  abfd = find_objfile_by_build_id (current_program_space,
+					   file_data.build_id,
 					   filename.c_str ());
 
 	  if (abfd != nullptr)
@@ -892,7 +893,8 @@ locate_exec_from_corefile_build_id (bfd *abfd, core_target *target,
     }
 
   gdb_bfd_ref_ptr execbfd
-    = find_objfile_by_build_id (build_id, filename.c_str ());
+    = find_objfile_by_build_id (current_program_space, build_id,
+				filename.c_str ());
 
   if (execbfd != nullptr)
     {
diff --git a/gdb/extension-priv.h b/gdb/extension-priv.h
index 90810e4bbc8..a31283f831c 100644
--- a/gdb/extension-priv.h
+++ b/gdb/extension-priv.h
@@ -295,6 +295,19 @@ struct extension_language_ops
   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
index 15e1f11d619..ec1aa138872 100644
--- a/gdb/extension.c
+++ b/gdb/extension.c
@@ -1070,6 +1070,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
index 2a749161508..ad3b4e6102d 100644
--- 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.
@@ -410,6 +411,25 @@ struct ext_lang_missing_file_result
 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/solib.c b/gdb/solib.c
index 5c926def715..dfcee4309bd 100644
--- a/gdb/solib.c
+++ b/gdb/solib.c
@@ -525,7 +525,8 @@ solib_map_sections (solib &so)
 	    abfd = nullptr;
 
 	  if (abfd == nullptr)
-	    abfd = find_objfile_by_build_id (mapped_file_info->build_id (),
+	    abfd = find_objfile_by_build_id (current_program_space,
+					     mapped_file_info->build_id (),
 					     so.so_name.c_str ());
 
 	  if (abfd == nullptr && mismatch)
-- 
2.25.4


^ permalink raw reply	[flat|nested] 24+ messages in thread

* [PATCHv3 4/4] gdb/python: implement Python find_exec_by_build_id hook
  2024-11-05 13:15   ` [PATCHv3 0/4] Python API to find missing objfiles Andrew Burgess
                       ` (2 preceding siblings ...)
  2024-11-05 13:15     ` [PATCHv3 3/4] gdb: add extension hook ext_lang_find_objfile_from_buildid Andrew Burgess
@ 2024-11-05 13:15     ` Andrew Burgess
  2024-11-10 10:21     ` [PATCHv3 0/4] Python API to find missing objfiles Andrew Burgess
  4 siblings, 0 replies; 24+ messages in thread
From: Andrew Burgess @ 2024-11-05 13:15 UTC (permalink / raw)
  To: gdb-patches; +Cc: Andrew Burgess, Eli Zaretskii

Implement extension_language_ops::find_objfile_from_buildid within
GDB's Python API.  Doing this allows users to write Python extensions
that can help locate missing objfiles when GDB opens a core file.  A
handler might perform some project- or site-specific actions to find a
missing objfile.  Or might provide some project- or site-specific
advice to the user on how they can obtain the missing objfile.

The implementation is very similar to the approach taken in:

  commit 8f6c452b5a4e50fbb55ff1d13328b392ad1fd416
  Date:   Sun Oct 15 22:48:42 2023 +0100

      gdb: implement missing debug handler hook for Python

The following new commands are added as commands implemented in
Python, this is similar to how the Python missing debug and unwinder
commands are implemented:

  info missing-objfile-handlers
  enable missing-objfile-handler LOCUS HANDLER
  disable missing-objfile-handler LOCUS HANDLER

To make use of this extension hook a user will create missing objfile
handler objects, and registers these handlers with GDB.  When GDB
opens a core file and encounters a missing objfile each handler is
called in turn until one is able to help.  Here is a minimal handler
that does nothing useful:

  import gdb
  import gdb.missing_objfile

  class MyFirstHandler(gdb.missing_objfile.MissingObjfileHandler):
      def __init__(self):
          super().__init__("my_first_handler")

      def __call__(self, pspace, build_id, filename):
          # This handler does nothing useful.
          return None

  gdb.missing_objfile.register_handler(None, MyFirstHandler())

Returning None from the __call__ method tells GDB that this handler
was unable to find the missing objfile, and GDB should ask any other
registered handlers.

Possible return values from a handler:

  - None: This means the handler couldn't help.  GDB will call other
          registered handlers to see if they can help instead.

  - False: The handler has done all it can, but the objfile couldn't
            be found.  GDB will not call any other handlers, and will
	    continue without the objfile.

  - True: The handler has installed the objfile into a location where
          GDB would normally expect to find it.  GDB should repeat its
	  normal lookup process and the objfile should now be found.

  - A string: The handler can return a filename, which is the missing
              objfile.  GDB will load this file.

Handlers can be registered globally, or per program space.  GDB checks
the handlers for the current program space first, and then all of the
global handles.  The first handler that returns a value that is not
None, has "handled" the missing objfile, at which point GDB continues.

The implementation of this feature is mostly straight forward.  I have
reworked some of the missing debug file related code so that it can be
shared with this feature.  E.g. gdb/python/lib/gdb/missing_files.py is
mostly content moved from gdb/python/lib/gdb/missing_debug.py, but
updated to be more generic.  Now gdb/python/lib/gdb/missing_debug.py
and the new file gdb/python/lib/gdb/missing_objfile.py both call into
the missing_files.py file.

For gdb/python/lib/gdb/command/missing_files.py this is even more
extreme, gdb/python/lib/gdb/command/missing_debug.py is completely
gone now and gdb/python/lib/gdb/command/missing_files.py provides all
of the new commands in a generic way.

I have made one change to the existing Python API, I renamed the
attribute Progspace.missing_debug_handlers to
Progspace.missing_file_handlers.  I don't see this as too
problematic.  This attribute was only used to implement the missing
debug feature and was never documented beyond the fact that it
existed.  There was no reason for users to be touching this attribute.

Reviewed-By: Eli Zaretskii <eliz@gnu.org>
---
 gdb/NEWS                                      |  24 +
 gdb/data-directory/Makefile.in                |   4 +-
 gdb/doc/gdb.texinfo                           |   1 +
 gdb/doc/python.texi                           | 194 +++++-
 gdb/python/lib/gdb/__init__.py                | 110 +++-
 .../{missing_debug.py => missing_files.py}    | 135 +++--
 gdb/python/lib/gdb/missing_debug.py           | 161 +----
 gdb/python/lib/gdb/missing_files.py           | 204 +++++++
 gdb/python/lib/gdb/missing_objfile.py         |  67 +++
 gdb/python/py-progspace.c                     |  26 +-
 gdb/python/python.c                           | 108 +++-
 .../gdb.python/py-missing-objfile-lib.c       |  35 ++
 gdb/testsuite/gdb.python/py-missing-objfile.c |  49 ++
 .../gdb.python/py-missing-objfile.exp         | 565 ++++++++++++++++++
 .../gdb.python/py-missing-objfile.py          | 167 ++++++
 15 files changed, 1615 insertions(+), 235 deletions(-)
 rename gdb/python/lib/gdb/command/{missing_debug.py => missing_files.py} (54%)
 create mode 100644 gdb/python/lib/gdb/missing_files.py
 create mode 100644 gdb/python/lib/gdb/missing_objfile.py
 create mode 100644 gdb/testsuite/gdb.python/py-missing-objfile-lib.c
 create mode 100644 gdb/testsuite/gdb.python/py-missing-objfile.c
 create mode 100644 gdb/testsuite/gdb.python/py-missing-objfile.exp
 create mode 100644 gdb/testsuite/gdb.python/py-missing-objfile.py

diff --git a/gdb/NEWS b/gdb/NEWS
index 678d031b02f..74ba5b4ee59 100644
--- a/gdb/NEWS
+++ b/gdb/NEWS
@@ -58,6 +58,17 @@
 
   ** Added the new event source gdb.tui_enabled.
 
+  ** 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.
+
 * Debugger Adapter Protocol changes
 
   ** The "scopes" request will now return a scope holding global
@@ -85,6 +96,19 @@ maintenance info blocks [ADDRESS]
   are listed starting at the inner global block out to the most inner
   block.
 
+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.
+
 * Changed commands
 
 remove-symbol-file
diff --git a/gdb/data-directory/Makefile.in b/gdb/data-directory/Makefile.in
index 1a17d23c132..287dc7ff739 100644
--- 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/ptwrite.py \
@@ -87,7 +89,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/doc/gdb.texinfo b/gdb/doc/gdb.texinfo
index eb2aff974bb..844e66046c6 100644
--- a/gdb/doc/gdb.texinfo
+++ b/gdb/doc/gdb.texinfo
@@ -21792,6 +21792,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
diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi
index b68fe0440ab..fed9b5c1893 100644
--- 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
@@ -5398,10 +5399,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:
@@ -5577,6 +5579,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}.
@@ -8165,6 +8168,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/python/lib/gdb/__init__.py b/gdb/python/lib/gdb/__init__.py
index 6c3e2419a40..146a963044f 100644
--- 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
index 313b88cf8c3..463853b8cec 100644
--- 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
index 7ccc4fe496d..2c2cebacaf8 100644
--- a/gdb/python/lib/gdb/missing_debug.py
+++ b/gdb/python/lib/gdb/missing_debug.py
@@ -17,72 +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:
-    # 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 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
@@ -93,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
@@ -148,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
index 00000000000..5f2df88c728
--- /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
index 00000000000..ace0e1315b5
--- /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
index aa1e713cc53..bb44aa42567 100644
--- 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;
 }
@@ -778,8 +778,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
index cceb7aa01c3..b0de48d4e1a 100644
--- 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"
@@ -130,6 +131,9 @@ static std::optional<std::string> gdbpy_colorize_disasm
 (const std::string &content, gdbarch *gdbarch);
 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.  */
 
@@ -179,7 +183,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 */
@@ -1829,6 +1834,107 @@ gdbpy_handle_missing_debuginfo (const struct extension_language_defn *extlang,
   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
    EXT_PRINTERS->py_type_printers.  The product of this function is used by
    gdbpy_apply_type_printers, and freed by gdbpy_free_type_printers.
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
index 00000000000..8d740b4bb71
--- /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
index 00000000000..953e1c0fb5b
--- /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
index 00000000000..15a69521eac
--- /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 == ""} {
+    unsupport "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
index 00000000000..9b158965411
--- /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")
-- 
2.25.4


^ permalink raw reply	[flat|nested] 24+ messages in thread

* Re: [PATCHv3 0/4] Python API to find missing objfiles
  2024-11-05 13:15   ` [PATCHv3 0/4] Python API to find missing objfiles Andrew Burgess
                       ` (3 preceding siblings ...)
  2024-11-05 13:15     ` [PATCHv3 4/4] gdb/python: implement Python find_exec_by_build_id hook Andrew Burgess
@ 2024-11-10 10:21     ` Andrew Burgess
  4 siblings, 0 replies; 24+ messages in thread
From: Andrew Burgess @ 2024-11-10 10:21 UTC (permalink / raw)
  To: gdb-patches

Andrew Burgess <aburgess@redhat.com> writes:

> I'm planning to push this series in the near future.
>
> There was a review of v1 which didn't show up anything serious, and
> the implementation for this feature matches the implementation of the
> missing objfile feature, so I don't think it would make sense to make
> any significant changes.
>
> ---
>
> In v3:
>
>   - Minor fixes for the tests in patch #4, this resolves the failures
>     when run with check-all-boards.  No issues found in GDB.
>
>   - Rebased.
>
> In v2:
>
>   - Addressed Tom's feedback on patch #4.
>
>   - Rebased onto current HEAD.
>
>   - Retested.
>
> ---
>
> This series provides a Python API which can be used to find missing
> objfiles when opening a core file.  This is very similar in spirit to
> the already existing code that provides a Python API for finding
> missing debug information.
>
> The first patch in this series cleans up the message that debuginfod
> produces.  This is only relevant because the cleanup changes which
> filenames are passed around, these same filenames are then passed into
> the new Python API.
>
> Patch #2 is a refactoring.
>
> Patch #3 adds the new extension language API on the GDB core side.
>
> Patch #4 adds the Python side of the same API.
>
> For reference, see these commits for when the missing debug files
> Python API was added:
>
>   * 8f6c452b5a4 gdb: implement missing debug handler hook for Python
>   * 661d98a3331 gdb: add an extension language hook for missing debug info

I've checked this in now.  Let me know if there are any problems.

Thanks,
Andrew


^ permalink raw reply	[flat|nested] 24+ messages in thread

end of thread, other threads:[~2024-11-10 10:21 UTC | newest]

Thread overview: 24+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2024-09-10 16:56 [PATCH 0/4] Python API to find missing objfiles Andrew Burgess
2024-09-10 16:56 ` [PATCH 1/4] gdb: use mapped file information to improve debuginfod text Andrew Burgess
2024-10-01 17:24   ` Tom Tromey
2024-09-10 16:56 ` [PATCH 2/4] gdb: rename ext_lang_missing_debuginfo_result Andrew Burgess
2024-10-01 17:25   ` Tom Tromey
2024-09-10 16:56 ` [PATCH 3/4] gdb: add extension hook ext_lang_find_objfile_from_buildid Andrew Burgess
2024-10-01 17:35   ` Tom Tromey
2024-09-10 16:56 ` [PATCH 4/4] gdb/python: implement Python find_exec_by_build_id hook Andrew Burgess
2024-09-10 18:13   ` Eli Zaretskii
2024-09-22 17:11     ` Andrew Burgess
2024-09-22 17:58       ` Eli Zaretskii
2024-10-01 18:29   ` Tom Tromey
2024-10-11 23:07     ` Andrew Burgess
2024-10-27  9:10 ` [PATCHv2 0/4] Python API to find missing objfiles Andrew Burgess
2024-10-27  9:10   ` [PATCHv2 1/4] gdb: use mapped file information to improve debuginfod text Andrew Burgess
2024-10-27  9:10   ` [PATCHv2 2/4] gdb: rename ext_lang_missing_debuginfo_result Andrew Burgess
2024-10-27  9:10   ` [PATCHv2 3/4] gdb: add extension hook ext_lang_find_objfile_from_buildid Andrew Burgess
2024-10-27  9:10   ` [PATCHv2 4/4] gdb/python: implement Python find_exec_by_build_id hook Andrew Burgess
2024-11-05 13:15   ` [PATCHv3 0/4] Python API to find missing objfiles Andrew Burgess
2024-11-05 13:15     ` [PATCHv3 1/4] gdb: use mapped file information to improve debuginfod text Andrew Burgess
2024-11-05 13:15     ` [PATCHv3 2/4] gdb: rename ext_lang_missing_debuginfo_result Andrew Burgess
2024-11-05 13:15     ` [PATCHv3 3/4] gdb: add extension hook ext_lang_find_objfile_from_buildid Andrew Burgess
2024-11-05 13:15     ` [PATCHv3 4/4] gdb/python: implement Python find_exec_by_build_id hook Andrew Burgess
2024-11-10 10:21     ` [PATCHv3 0/4] Python API to find missing objfiles Andrew Burgess

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).