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