diff options
-rw-r--r-- | gdb/NEWS | 24 | ||||
-rw-r--r-- | gdb/data-directory/Makefile.in | 4 | ||||
-rw-r--r-- | gdb/doc/gdb.texinfo | 1 | ||||
-rw-r--r-- | gdb/doc/python.texi | 194 | ||||
-rw-r--r-- | gdb/python/lib/gdb/__init__.py | 110 | ||||
-rw-r--r-- | gdb/python/lib/gdb/command/missing_files.py (renamed from gdb/python/lib/gdb/command/missing_debug.py) | 135 | ||||
-rw-r--r-- | gdb/python/lib/gdb/missing_debug.py | 161 | ||||
-rw-r--r-- | gdb/python/lib/gdb/missing_files.py | 204 | ||||
-rw-r--r-- | gdb/python/lib/gdb/missing_objfile.py | 67 | ||||
-rw-r--r-- | gdb/python/py-progspace.c | 26 | ||||
-rw-r--r-- | gdb/python/python.c | 108 | ||||
-rw-r--r-- | gdb/testsuite/gdb.python/py-missing-objfile-lib.c | 35 | ||||
-rw-r--r-- | gdb/testsuite/gdb.python/py-missing-objfile.c | 49 | ||||
-rw-r--r-- | gdb/testsuite/gdb.python/py-missing-objfile.exp | 565 | ||||
-rw-r--r-- | gdb/testsuite/gdb.python/py-missing-objfile.py | 167 |
15 files changed, 1615 insertions, 235 deletions
@@ -58,6 +58,17 @@ ** Added the new event source gdb.tui_enabled. + ** New module gdb.missing_objfile that facilitates dealing with + missing objfiles when opening a core-file. + + ** New function gdb.missing_objfile.register_handler that can + register an instance of a sub-class of + gdb.missing_debug.MissingObjfileHandler as a handler for missing + objfiles. + + ** New class gdb.missing_objfile.MissingObjfileHandler which can be + sub-classed to create handlers for missing objfiles. + * Debugger Adapter Protocol changes ** The "scopes" request will now return a scope holding global @@ -85,6 +96,19 @@ maintenance info blocks [ADDRESS] are listed starting at the inner global block out to the most inner block. +info missing-objfile-handlers + List all the registered missing-objfile handlers. + +enable missing-objfile-handler LOCUS HANDLER +disable missing-objfile-handler LOCUS HANDLER + Enable or disable a missing-objfile handler with a name matching the + regular expression HANDLER, in LOCUS. + + LOCUS can be 'global' to operate on global missing-objfile handler, + 'progspace' to operate on handlers within the current program space, + or can be a regular expression which is matched against the filename + of the primary executable in each program space. + * Changed commands remove-symbol-file diff --git a/gdb/data-directory/Makefile.in b/gdb/data-directory/Makefile.in index 1a17d23..287dc7f 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 eb2aff9..844e660 100644 --- a/gdb/doc/gdb.texinfo +++ b/gdb/doc/gdb.texinfo @@ -21792,6 +21792,7 @@ contained in @var{filename} by using the @samp{-readnever} option. @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 b68fe04..fed9b5c 100644 --- a/gdb/doc/python.texi +++ b/gdb/doc/python.texi @@ -231,6 +231,7 @@ optional arguments while skipping others. Example: * TUI Windows In Python:: Implementing new TUI windows. * Disassembly In Python:: Instruction Disassembly In Python * Missing Debug Info In Python:: Handle missing debug info from Python. +* Missing Objfiles In Python:: Handle objfiles from Python. @end menu @node Basic Python @@ -5398,10 +5399,11 @@ The @code{frame_filters} attribute is a dictionary of frame filter objects. @xref{Frame Filter API}, for more information. @end defvar -@defvar Progspace.missing_debug_handlers -The @code{missing_debug_handlers} attribute is a list of the missing -debug handler objects for this program space. @xref{Missing Debug -Info In Python}, for more information. +@defvar Progspace.missing_file_handlers +The @code{missing_file_handlers} attribute is a list of tuples. Each +tuple holds a missing-file handler object for this program space. For +more information, @pxref{Missing Debug Info In Python}, and +@ref{Missing Objfiles In Python}. @end defvar A program space has the following methods: @@ -5577,6 +5579,7 @@ Separate debug info objfiles are added with the @code{gdb.Objfile.add_separate_debug_file} method, described below. @end defvar +@anchor{Objfile.build_id} @defvar Objfile.build_id The build ID of the objfile as a string. If the objfile does not have a build ID then the value is @code{None}. @@ -8165,6 +8168,189 @@ returns a value other than @code{None}, no further handlers are called 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 6c3e241..146a963 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 index 313b88c..463853b 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 7ccc4fe..2c2ceba 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 0000000..5f2df88c --- /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 0000000..ace0e13 --- /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 aa1e713..bb44aa4 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 cceb7aa..b0de48d 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 0000000..8d740b4 --- /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 0000000..953e1c0 --- /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 0000000..15a6952 --- /dev/null +++ b/gdb/testsuite/gdb.python/py-missing-objfile.exp @@ -0,0 +1,565 @@ +# Copyright (C) 2024 Free Software Foundation, Inc. + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +load_lib gdb-python.exp + +require allow_python_tests +require {!is_remote host} + +standard_testfile .c -lib.c + +# Build the library. +set libname ${testfile}-lib +set libfile [standard_output_file $libname] +if { [build_executable "build shlib" $libfile $srcfile2 \ + {debug shlib build-id}] == -1} { + return +} + +# Build the executable. +set opts [list debug build-id shlib=${libfile}] +if { [build_executable "build exec" $binfile $srcfile $opts] == -1} { + return +} + +# The cc-with-gnu-debuglink board will split the debug out into the +# .debug directory. This test script relies on having GDB lookup the +# objfile and debug via the build-id, which this test sets up. Trying +# to do that, while also supporting the cc-with-gnu-debuglink board is +# just too complicated. +if {[file isdirectory [standard_output_file ".debug"]]} { + unsupported "split debug testing not supported" + return +} + +set remote_python_file \ + [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py] + +# Generate a core file. +set corefile [core_find $binfile {}] +if {$corefile == ""} { + unsupport "core file not generated" + return 0 +} + +# Create a directory named DIRNAME for use as the +# debug-file-directory. Populate the directory with links (based on +# the build-ids) to each file in the list FILES. +# +# Return the full filename of DIRNAME on the host. +proc setup_debugdir { dirname files } { + set debugdir [host_standard_output_file $dirname] + + # Create basic empty directory structure (in case FILES is empty). + remote_exec host "mkdir -p $debugdir/.build-id/" + + foreach file $files { + set build_id_filename [build_id_debug_filename_get $file ""] + + remote_exec host "mkdir -p $debugdir/[file dirname $build_id_filename]" + remote_exec host "ln -s $file $debugdir/$build_id_filename" + } + + return $debugdir +} + +# Query some symbols in the inferior to see if GDB managed to find the +# executable (when EXEC_LOADED is true) and/or the library (when LIB_LOADED +# is true). +proc check_loaded_debug { exec_loaded lib_loaded } { + if { $exec_loaded } { + gdb_test "whatis global_exec_var" "^type = volatile struct exec_type" + + if { $lib_loaded } { + gdb_test "whatis global_lib_var" "^type = volatile struct lib_type" + } else { + gdb_test "whatis global_lib_var" \ + "^No symbol \"global_lib_var\" in current context\\." + } + } else { + gdb_test "whatis global_exec_var" \ + "^No symbol table is loaded\\. Use the \"file\" command\\." + gdb_test "whatis global_lib_var" \ + "^No symbol table is loaded\\. Use the \"file\" command\\." + } +} + +# Load the global corefile. The EXTRA_RE is checked for prior to GDB +# announcing that the core-file has been loaded. +proc load_core_file { {extra_re ".*"} } { + gdb_test "core-file $::corefile" \ + [multi_line \ + "$extra_re" \ + "Core was generated by \[^\r\n\]+" \ + "Program terminated with signal SIGABRT, Aborted\\." \ + "\[^\r\n\]+(?:\r\n\[^\r\n\]+)?"] \ + "loaded the core file" +} + +# Set the debug-file-directory to DIRNAME. +proc set_debug_file_dir { dirname } { + gdb_test_no_output "set debug-file-directory $dirname" \ + "set debug-file-directory" +} + +# Restart GDB and load the support Python script. +proc clean_restart_load_python {} { + clean_restart + gdb_test "source $::remote_python_file" "^Success" \ + "load python script" +} + +# For sanity, lets check that we can load the specify the executable +# and then load the core-file the easy way. +with_test_prefix "initial sanity check" { + clean_restart $binfile + load_core_file + check_loaded_debug true true +} + +# Move the executable and library into a location that the core-file +# can't possibly know about. After this the only way GDB can track +# down these files will be by looking in the debug-file-directory. +set hidden_dir [host_standard_output_file "hidden"] +set hidden_binfile "$hidden_dir/$testfile" +set hidden_libfile "$hidden_dir/$libname" +remote_exec host "mkdir -p $hidden_dir" +remote_exec host "mv $libfile $hidden_libfile" +remote_exec host "mv $binfile $hidden_binfile" + +# If using the fission-dwp board then we'll have .dwp files that also +# need to be moved. +if {[remote_file host exists ${libfile}.dwp]} { + remote_exec host "mv ${libfile}.dwp ${hidden_libfile}.dwp" +} + +if {[remote_file host exists ${binfile}.dwp]} { + remote_exec host "mv ${binfile}.dwp ${hidden_binfile}.dwp" +} + +with_test_prefix "no objfiles, no debug-file-directory" { + clean_restart + load_core_file + check_loaded_debug false false +} + +# Setup some debug-file-directories. +set debugdir_no_lib \ + [setup_debugdir "debugdir.no-lib" [list "$hidden_binfile"]] +set debugdir_empty \ + [setup_debugdir "debugdir.empty" {}] +set debugdir_all \ + [setup_debugdir "debugdir.all" [list "$hidden_libfile" \ + "$hidden_binfile"]] + +with_test_prefix "no objfiles available" { + # Another sanity check that GDB can find the files via the + # debug-file-directory. + clean_restart + set_debug_file_dir $debugdir_empty + load_core_file + check_loaded_debug false false +} + +with_test_prefix "all objfiles available" { + # Another sanity check that GDB can find the files via the + # debug-file-directory. + set_debug_file_dir $debugdir_all + load_core_file + check_loaded_debug true true +} + +with_test_prefix "lib objfile missing" { + # Another sanity check that GDB can find the files via the + # debug-file-directory. + set_debug_file_dir $debugdir_no_lib + load_core_file + check_loaded_debug true false +} + +with_test_prefix "all objfiles missing, handler returns None" { + clean_restart_load_python + gdb_test_no_output \ + "python gdb.missing_objfile.register_handler(None, handler_obj)" \ + "register initial handler" + load_core_file + + check_loaded_debug false false + + # The handler should be called three times, once for the + # mapped-file, once for the core-file's exec, and once for the + # shared library. + gdb_test "python print(handler_obj.call_count)" "^3" \ + "check handler was called three times" +} + +with_test_prefix "lib objfile missing, handler returns None" { + # Reset handler_obj. + gdb_test_no_output "python handler_obj.set_mode(Mode.RETURN_NONE)" + + set_debug_file_dir $debugdir_no_lib + load_core_file + check_loaded_debug true false + + # The handler will be called twice, once when GDB tries to + # load the shared library during the memory-mapped file phase, + # then again for the shared library loading. + gdb_test "python print(handler_obj.call_count)" "^2" \ + "check handler was called three times" +} + +with_test_prefix "handler installs lib objfile" { + set build_id_filename [build_id_debug_filename_get \ + $hidden_libfile ""] + remote_exec host \ + "mkdir -p $debugdir_no_lib/[file dirname $build_id_filename]" + gdb_test_no_output "python handler_obj.set_mode(Mode.RETURN_TRUE, \ + \"$hidden_libfile\", \"$debugdir_no_lib/$build_id_filename\")" \ + "configure handler" + + load_core_file + check_loaded_debug true true + + # Cleanup so the test can be reproduced again later if needed. + remote_exec host "rm $debugdir_no_lib/$build_id_filename" +} + +with_test_prefix "handler points to lib objfile" { + set build_id_filename [build_id_debug_filename_get \ + $hidden_libfile ""] + remote_exec host \ + "mkdir -p $debugdir_no_lib/[file dirname $build_id_filename]" + gdb_test_no_output "python handler_obj.set_mode(Mode.RETURN_STRING, \ + \"$hidden_libfile\")" \ + "configure handler" + + load_core_file + check_loaded_debug true true + + # Cleanup so the test can be reproduced again later if needed. + remote_exec host "rm $debugdir_no_lib/$build_id_filename" + + # The handler will only have been called once when loading the + # memory-mapped file. GDB is smart enough to reuse the previously + # discovered BFD object as the shared library. + gdb_test "python print(handler_obj.call_count)" "^1" \ + "check good handler hasn't been called again" + + # Validate the filename and build-id arguments passed to the handler. + set expected_buildid [get_build_id $hidden_libfile] + gdb_test "python print(handler_last_buildid)" "^$expected_buildid" + gdb_test "python print(handler_last_filename)" \ + "^[string_to_regexp $libfile]" +} + +# Register another global handler, this one raises an exception. Reload the +# core-file, the bad handler should be invoked first, which raises an +# excetption, at which point GDB should skip further Python handlers. +with_test_prefix "handler raises an exception" { + gdb_test_no_output \ + "python gdb.missing_objfile.register_handler(None, rhandler)" + + foreach_with_prefix exception_type {gdb.GdbError TypeError} { + gdb_test_no_output \ + "python rhandler.exception_type = $exception_type" + + # Load the core file. We expect the exception message to appear at + # least once in the output. + set re [string_to_regexp \ + "Python Exception <class '$exception_type'>: message"] + load_core_file "${re}.*" + + # Our original handler is still registered, but should not have been + # called again (as the exception occurs first). + gdb_test "python print(handler_obj.call_count)" "^1" \ + "check good handler hasn't been called again" + } +} + +# Re-start GDB. +clean_restart_load_python + +# Attempt to register a missing-debug-handler with NAME. The expectation is +# that this should fail as NAME contains some invalid characters. +proc check_bad_name {name} { + set name_re [string_to_regexp $name] + set re \ + [multi_line \ + "ValueError.*: invalid character '.' in handler name: $name_re" \ + "Error occurred in Python.*"] + + gdb_test "python register(\"$name\")" $re \ + "check that '$name' is not accepted" +} + +# We don't attempt to be exhaustive here, just check a few random examples +# of invalid names. +check_bad_name "!! Bad Name" +check_bad_name "Bad Name" +check_bad_name "(Bad Name)" +check_bad_name "Bad \[Name\]" +check_bad_name "Bad,Name" +check_bad_name "Bad;Name" + +# Check that there are no handlers registered. +gdb_test_no_output "info missing-objfile-handlers" \ + "check no handlers are registered" + +# Grab the current program space object, used for registering handler later. +gdb_test_no_output "python pspace = gdb.selected_inferior().progspace" + +# Now register some handlers. +foreach hspec {{\"Foo\" None} + {\"-bar\" None} + {\"baz-\" pspace} + {\"abc-def\" pspace}} { + lassign $hspec name locus + gdb_test "python register($name, $locus)" +} + +with_test_prefix "all handlers enabled" { + gdb_test "info missing-objfile-handlers" \ + [multi_line \ + "Current Progspace:" \ + " abc-def" \ + " baz-" \ + "Global:" \ + " -bar" \ + " Foo"] + + set_debug_file_dir $debugdir_no_lib + load_core_file + + # As we perform two look ups, first for the mapped-file then for the + # shared library, each handler will be called twice. + gdb_test "python print(handler_call_log)" \ + [string_to_regexp {['abc-def', 'baz-', '-bar', 'Foo', 'abc-def', 'baz-', '-bar', 'Foo']}] + gdb_test_no_output "python handler_call_log = \[\]" \ + "reset call log" +} + +with_test_prefix "disable 'baz-'" { + gdb_test "disable missing-objfile-handler progspace baz-" \ + "^1 missing objfile handler disabled" + + gdb_test "info missing-objfile-handlers" \ + [multi_line \ + "Progspace \[^\r\n\]+:" \ + " abc-def" \ + " baz- \\\[disabled\\\]" \ + "Global:" \ + " -bar" \ + " Foo"] + + load_core_file + gdb_test "python print(handler_call_log)" \ + [string_to_regexp {['abc-def', '-bar', 'Foo', 'abc-def', '-bar', 'Foo']}] + gdb_test_no_output "python handler_call_log = \[\]" \ + "reset call log" +} + +with_test_prefix "disable 'Foo'" { + gdb_test "disable missing-objfile-handler .* Foo" \ + "^1 missing objfile handler disabled" + + gdb_test "info missing-objfile-handlers" \ + [multi_line \ + "Progspace \[^\r\n\]+:" \ + " abc-def" \ + " baz- \\\[disabled\\\]" \ + "Global:" \ + " -bar" \ + " Foo \\\[disabled\\\]"] + + load_core_file + gdb_test "python print(handler_call_log)" \ + [string_to_regexp {['abc-def', '-bar', 'abc-def', '-bar']}] + gdb_test_no_output "python handler_call_log = \[\]" \ + "reset call log" +} + +with_test_prefix "disable everything" { + gdb_test "disable missing-objfile-handler .* .*" \ + "^2 missing objfile handlers disabled" + + gdb_test "info missing-objfile-handlers" \ + [multi_line \ + "Progspace \[^\r\n\]+:" \ + " abc-def \\\[disabled\\\]" \ + " baz- \\\[disabled\\\]" \ + "Global:" \ + " -bar \\\[disabled\\\]" \ + " Foo \\\[disabled\\\]"] + + load_core_file + gdb_test "python print(handler_call_log)" \ + [string_to_regexp {[]}] + gdb_test_no_output "python handler_call_log = \[\]" \ + "reset call log" +} + +with_test_prefix "enable 'abc-def'" { + set re [string_to_regexp $hidden_binfile] + + gdb_test "enable missing-objfile-handler \"$re\" abc-def" \ + "^1 missing objfile handler enabled" \ + "enable missing-objfile-handler" + + gdb_test "info missing-objfile-handlers" \ + [multi_line \ + "Progspace \[^\r\n\]+:" \ + " abc-def" \ + " baz- \\\[disabled\\\]" \ + "Global:" \ + " -bar \\\[disabled\\\]" \ + " Foo \\\[disabled\\\]"] + + load_core_file + gdb_test "python print(handler_call_log)" \ + [string_to_regexp {['abc-def', 'abc-def']}] + gdb_test_no_output "python handler_call_log = \[\]" \ + "reset call log" +} + +with_test_prefix "enable global handlers" { + gdb_test "enable missing-objfile-handler global" \ + "^2 missing objfile handlers enabled" + + gdb_test "info missing-objfile-handlers" \ + [multi_line \ + "Progspace \[^\r\n\]+:" \ + " abc-def" \ + " baz- \\\[disabled\\\]" \ + "Global:" \ + " -bar" \ + " Foo"] + + load_core_file + gdb_test "python print(handler_call_log)" \ + [string_to_regexp {['abc-def', '-bar', 'Foo', 'abc-def', '-bar', 'Foo']}] + gdb_test_no_output "python handler_call_log = \[\]" \ + "reset call log" +} + +# Add handler_obj to the global handler list, and configure it to +# return False. We should call all of the program space specific +# handlers (which return None), and then call handler_obj from the +# global list, which returns False, at which point we shouldn't call +# anyone else. +with_test_prefix "return False handler in global list" { + gdb_test "enable missing-objfile-handler progspace" \ + "^1 missing objfile handler enabled" + + gdb_test_no_output \ + "python gdb.missing_objfile.register_handler(None, handler_obj)" \ + "register handler_obj in global list" + + gdb_test "info missing-objfile-handlers" \ + [multi_line \ + "Progspace \[^\r\n\]+:" \ + " abc-def" \ + " baz-" \ + "Global:" \ + " handler" \ + " -bar" \ + " Foo"] + + gdb_test_no_output "python handler_obj.set_mode(Mode.RETURN_FALSE)" \ + "confirgure handler" + + load_core_file + gdb_test "python print(handler_call_log)" \ + [string_to_regexp {['abc-def', 'baz-', 'handler', 'abc-def', 'baz-', 'handler']}] + gdb_test_no_output "python handler_call_log = \[\]" \ + "reset call log" +} + +# Now add handler_obj to the current program space's handler list. We +# use the same handler object here, that's fine. We should only see a +# call to the first handler object in the call log. +with_test_prefix "return False handler in progspace list" { + gdb_test_no_output \ + "python gdb.missing_objfile.register_handler(pspace, handler_obj)" \ + "register handler_obj in progspace list" + + gdb_test "info missing-objfile-handlers" \ + [multi_line \ + "Progspace \[^\r\n\]+:" \ + " handler" \ + " abc-def" \ + " baz-" \ + "Global:" \ + " handler" \ + " -bar" \ + " Foo"] + + load_core_file + gdb_test "python print(handler_call_log)" \ + [string_to_regexp {['handler', 'handler']}] + gdb_test_no_output "python handler_call_log = \[\]" \ + "reset call log" +} + +with_test_prefix "check handler replacement" { + # First, check we can have the same name appear in both program + # space and global lists without giving an error. + gdb_test_no_output "python register(\"Foo\", pspace)" + + gdb_test "info missing-objfile-handlers" \ + [multi_line \ + "Progspace \[^\r\n\]+:" \ + " Foo" \ + " handler" \ + " abc-def" \ + " baz-" \ + "Global:" \ + " handler" \ + " -bar" \ + " Foo"] + + # Now check that we get an error if we try to add a handler with + # the same name. + gdb_test "python gdb.missing_objfile.register_handler(pspace, log_handler(\"Foo\"))" \ + [multi_line \ + "RuntimeError.*: Handler Foo already exists\\." \ + "Error occurred in Python.*"] + + gdb_test "python gdb.missing_objfile.register_handler(handler=log_handler(\"Foo\"), locus=pspace)" \ + [multi_line \ + "RuntimeError.*: Handler Foo already exists\\." \ + "Error occurred in Python.*"] + + # And now try again, but this time with 'replace=True', we + # shouldn't get an error in this case. + gdb_test_no_output \ + "python gdb.missing_objfile.register_handler(pspace, log_handler(\"Foo\"), replace=True)" + + gdb_test_no_output \ + "python gdb.missing_objfile.register_handler(handler=log_handler(\"Foo\"), locus=None, replace=True)" + + # Now disable a handler and check we still need to use 'replace=True'. + gdb_test "disable missing-objfile-handler progspace Foo" \ + "^1 missing objfile handler disabled" + + gdb_test "python gdb.missing_objfile.register_handler(pspace, log_handler(\"Foo\"))" \ + [multi_line \ + "RuntimeError.*: Handler Foo already exists\\." \ + "Error occurred in Python.*"] \ + "still get an error when handler is disabled" + + gdb_test_no_output \ + "python gdb.missing_objfile.register_handler(pspace, log_handler(\"Foo\"), replace=True)" \ + "can replace a disabled handler" +} diff --git a/gdb/testsuite/gdb.python/py-missing-objfile.py b/gdb/testsuite/gdb.python/py-missing-objfile.py new file mode 100644 index 0000000..9b15896 --- /dev/null +++ b/gdb/testsuite/gdb.python/py-missing-objfile.py @@ -0,0 +1,167 @@ +# Copyright (C) 2024 Free Software Foundation, Inc. + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import shutil +import os +from enum import Enum + +import gdb +from gdb.missing_objfile import MissingObjfileHandler + +# A global log that is filled in by instances of the LOG_HANDLER class +# when they are called. +handler_call_log = [] + +# A global holding a string, the build-id of the last missing objfile +# which triggered the 'handler' class below. This is set in the +# __call__ method of the 'handler' class and then checked from the +# expect script. +handler_last_buildid = None + + +# A global holding a string, the filename of the last missing objfile +# which triggered the 'handler' class below. This is set in the +# __call__ method of the 'handler' class and then checked from the +# expect script. +handler_last_filename = None + + +# A helper function that makes some assertions about the arguments +# passed to a MissingObjfileHandler.__call__() method. +def check_args(pspace, buildid, filename): + assert type(filename) == str + assert filename != "" + assert type(pspace) == gdb.Progspace + assert type(buildid) == str + assert buildid != "" + + +# Enum used to configure the 'handler' class from the test script. +class Mode(Enum): + RETURN_NONE = 0 + RETURN_TRUE = 1 + RETURN_FALSE = 2 + RETURN_STRING = 3 + + +# A missing objfile handler which can be configured to return each of +# the different possible return types. +class handler(MissingObjfileHandler): + def __init__(self): + super().__init__("handler") + self._call_count = 0 + self._mode = Mode.RETURN_NONE + + def __call__(self, pspace, buildid, filename): + global handler_call_log, handler_last_buildid, handler_last_filename + check_args(pspace, buildid, filename) + handler_call_log.append(self.name) + handler_last_buildid = buildid + handler_last_filename = filename + self._call_count += 1 + if self._mode == Mode.RETURN_NONE: + return None + + if self._mode == Mode.RETURN_TRUE: + shutil.copy(self._src, self._dest) + + # If we're using the fission-dwp board then there will + # also be a .dwp file that needs to be copied. + dwp_src = self._src + ".dwp" + if os.path.exists(dwp_src): + dwp_dest = self._dest + ".dwp" + shutil.copy(dwp_src, dwp_dest) + + return True + + if self._mode == Mode.RETURN_FALSE: + return False + + if self._mode == Mode.RETURN_STRING: + return self._dest + + assert False + + @property + def call_count(self): + """Return a count, the number of calls to __call__ since the last + call to set_mode. + """ + return self._call_count + + def set_mode(self, mode, *args): + self._call_count = 0 + self._mode = mode + + if mode == Mode.RETURN_NONE: + assert len(args) == 0 + return + + if mode == Mode.RETURN_TRUE: + assert len(args) == 2 + self._src = args[0] + self._dest = args[1] + return + + if mode == Mode.RETURN_FALSE: + assert len(args) == 0 + return + + if mode == Mode.RETURN_STRING: + assert len(args) == 1 + self._dest = args[0] + return + + assert False + + +# A missing objfile handler which raises an exception. The type of +# exception to be raised is configured from the test script. +class exception_handler(MissingObjfileHandler): + def __init__(self): + super().__init__("exception_handler") + self.exception_type = None + + def __call__(self, pspace, buildid, filename): + global handler_call_log + check_args(pspace, buildid, filename) + handler_call_log.append(self.name) + assert self.exception_type is not None + raise self.exception_type("message") + + +# A very simple logging missing objfile handler. Always returns None +# so that GDB will try any other registered handlers, but first logs +# the name of this handler into the global HANDLER_CALL_LOG, which can +# then be checked from the test script. +class log_handler(MissingObjfileHandler): + def __call__(self, pspace, buildid, filename): + global handler_call_log + check_args(pspace, buildid, filename) + handler_call_log.append(self.name) + return None + + +# A basic helper function, this keeps lines shorter in the TCL script. +def register(name, locus=None): + gdb.missing_objfile.register_handler(locus, log_handler(name)) + + +# Create instances of the handlers, but don't install any. We install +# these as needed from the TCL script. +rhandler = exception_handler() +handler_obj = handler() + +print("Success") |