[PATCHv2,4/4] gdb/python: implement Python find_exec_by_build_id hook

Message ID 065c0444e7c2a5adaa687e5969030b0ad7793e62.1730020155.git.aburgess@redhat.com
State New
Headers
Series Python API to find missing objfiles |

Checks

Context Check Description
linaro-tcwg-bot/tcwg_gdb_build--master-aarch64 success Build passed
linaro-tcwg-bot/tcwg_gdb_build--master-arm success Build passed
linaro-tcwg-bot/tcwg_gdb_check--master-arm fail Test failed
linaro-tcwg-bot/tcwg_gdb_check--master-aarch64 success Test passed

Commit Message

Andrew Burgess Oct. 27, 2024, 9:10 a.m. UTC
  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
  

Patch

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")