diff options
-rw-r--r-- | gdb/NEWS | 26 | ||||
-rw-r--r-- | gdb/data-directory/Makefile.in | 2 | ||||
-rw-r--r-- | gdb/doc/python.texi | 140 | ||||
-rw-r--r-- | gdb/python/lib/gdb/__init__.py | 41 | ||||
-rw-r--r-- | gdb/python/lib/gdb/command/missing_debug.py | 226 | ||||
-rw-r--r-- | gdb/python/lib/gdb/missing_debug.py | 169 | ||||
-rw-r--r-- | gdb/python/py-progspace.c | 51 | ||||
-rw-r--r-- | gdb/python/python.c | 83 | ||||
-rw-r--r-- | gdb/testsuite/gdb.python/py-missing-debug.c | 22 | ||||
-rw-r--r-- | gdb/testsuite/gdb.python/py-missing-debug.exp | 473 | ||||
-rw-r--r-- | gdb/testsuite/gdb.python/py-missing-debug.py | 120 |
11 files changed, 1352 insertions, 1 deletions
@@ -16,6 +16,21 @@ disassemble command will now give an error. Previously the 'b' flag would always override the 'r' flag. +* New Commands + +info missing-debug-handler + List all the registered missing debug handlers. + +enable missing-debug-handler LOCUS HANDLER +disable missing-debug-handler LOCUS HANDLER + Enable or disable a missing debug handler with a name matching the + regular expression HANDLER, in LOCUS. + + LOCUS can be 'global' to operate on global missing debug 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. + * Python API ** New function gdb.notify_mi(NAME, DATA), that emits custom @@ -24,6 +39,17 @@ disassemble ** New read/write attribute gdb.Value.bytes that contains a bytes object holding the contents of this value. + ** New module gdb.missing_debug that facilitates dealing with + objfiles that are missing any debug information. + + ** New function gdb.missing_debug.register_handler that can register + an instance of a sub-class of gdb.missing_debug.MissingDebugInfo + as a handler for objfiles that are missing debug information. + + ** New class gdb.missing_debug.MissingDebugInfo which can be + sub-classed to create handlers for objfiles with missing debug + information. + * New commands maintenance info linux-lwps diff --git a/gdb/data-directory/Makefile.in b/gdb/data-directory/Makefile.in index 04a8c8e..7af6baf 100644 --- a/gdb/data-directory/Makefile.in +++ b/gdb/data-directory/Makefile.in @@ -73,6 +73,7 @@ PYTHON_FILE_LIST = \ gdb/FrameDecorator.py \ gdb/FrameIterator.py \ gdb/frames.py \ + gdb/missing_debug.py \ gdb/printing.py \ gdb/prompt.py \ gdb/styling.py \ @@ -82,6 +83,7 @@ PYTHON_FILE_LIST = \ gdb/command/__init__.py \ gdb/command/explore.py \ gdb/command/frame_filters.py \ + gdb/command/missing_debug.py \ gdb/command/pretty_printers.py \ gdb/command/prompt.py \ gdb/command/type_printers.py \ diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi index 8cc3f92..c26949e 100644 --- a/gdb/doc/python.texi +++ b/gdb/doc/python.texi @@ -230,6 +230,7 @@ optional arguments while skipping others. Example: * Connections In Python:: Python representation of connections. * 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. @end menu @node Basic Python @@ -5252,6 +5253,12 @@ 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. +@end defvar + A program space has the following methods: @defun Progspace.block_for_pc (pc) @@ -7819,6 +7826,139 @@ class NibbleSwapDisassembler(gdb.disassembler.Disassembler): gdb.disassembler.register_disassembler(NibbleSwapDisassembler()) @end smallexample +@node Missing Debug Info In Python +@subsubsection Missing Debug Info In Python +@cindex python, handle missing debug information + +When @value{GDBN} encounters a new objfile (@pxref{Objfiles In +Python}), e.g.@: the primary executable, or any shared libraries used +by the inferior, @value{GDBN} will attempt to load the corresponding +debug information for that objfile. The debug information might be +found within the objfile itself, or within a separate objfile which +@value{GDBN} will automatically locate and load. + +Sometimes though, @value{GDBN} might not find any debug information +for an objfile, in this case the debugging experience will be +restricted. + +If @value{GDBN} fails to locate any debug information for a particular +objfile, there is an opportunity for a Python extension to step in. A +Python extension can potentially locate the missing debug information +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 debug information. + +A missing debug information 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 +an objfile for which it is unable to find any debug information, it +invokes the @code{__call__} method. Full details of how handlers are +written can be found below. + +@subheading The @code{gdb.missing_debug} Module + +@value{GDBN} comes with a @code{gdb.missing_debug} module which +contains the following class and global function: + +@deftp{class} gdb.missing_debug.MissingDebugHandler + +@code{MissingDebugHandler} 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 MissingDebugHandler.__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 MissingDebugHandler.__call__ (objfile) +Sub-classes must override the @code{__call__} method. The +@var{objfile} argument will be a @code{gdb.Objfile}, this is the +objfile for which @value{GDBN} was unable to find any debug +information. + +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 help with @var{objfile}, +@value{GDBN} should call any other registered handlers. + +@item @code{True} + +This indicates that this handler has installed the debug information +into a location where @value{GDBN} would normally expect to find it +when looking for separate debug information files (@pxref{Separate +Debug Files}). @value{GDBN} will repeat the normal lookup process, +which should now find the separate debug file. + +If @value{GDBN} still doesn't find the separate debug information file +after this second attempt, then the Python missing debug information +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 any debug information for @var{objfile}. + +@item @code{False} + +This indicates that this handler has done everything that it intends +to do with @var{objfile}, but no separate debug information can be +found. @value{GDBN} will not call any other registered handlers for +@var{objfile}. @value{GDBN} will continue without debugging +information for @var{objfile}. + +@item A string + +The returned string should contain a filename. @value{GDBN} will not +call any further registered handlers, and will instead load the debug +information from the file identified by the returned filename. +@end itemize + +Invoking the @code{__call__} method from this base class will raise a +@code{NotImplementedError} exception. +@end defun + +@defvar MissingDebugHandler.name +A read-only attribute which is a string, the name of this handler +passed to the @code{__init__} method. +@end defvar + +@defvar MissingDebugHandler.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_debug.register_handler (locus, handler, replace=@code{False}) +Register a new missing debug handler with @value{GDBN}. + +@var{handler} is an instance of a sub-class of +@code{MissingDebugHandler}, or at least an instance of an object that +has the same attributes and methods as @code{MissingDebugHandler}. + +@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 +for this objfile. +@end defun + @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 b312436..93ed50e 100644 --- a/gdb/python/lib/gdb/__init__.py +++ b/gdb/python/lib/gdb/__init__.py @@ -84,6 +84,8 @@ xmethods = [] frame_filters = {} # Initial frame unwinders. frame_unwinders = [] +# Initial missing debug handlers. +missing_debug_handlers = [] def _execute_unwinders(pending_frame): @@ -291,3 +293,42 @@ class Thread(threading.Thread): # threads. with blocked_signals(): super().start() + + +def _handle_missing_debuginfo(objfile): + """Internal function called from GDB to execute missing debug + handlers. + + Run each of the currently registered, and enabled missing debug + handler objects for the current program space and then from the + global list. Stop after the first handler that returns a result + other than None. + + Arguments: + objfile: A gdb.Objfile for which GDB could not find any debug + information. + + Returns: + None: No debug information could be found for objfile. + False: A handler has done all it can with objfile, but no + debug information could be found. + True: Debug information might have been installed by a + handler, GDB should check again. + 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 + + for handler in missing_debug_handlers: + if handler.enabled: + result = handler(objfile) + if result is not None: + return result + + return None diff --git a/gdb/python/lib/gdb/command/missing_debug.py b/gdb/python/lib/gdb/command/missing_debug.py new file mode 100644 index 0000000..6fe1cdb --- /dev/null +++ b/gdb/python/lib/gdb/command/missing_debug.py @@ -0,0 +1,226 @@ +# Missing debug related commands. +# +# Copyright 2023 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 gdb +import re + + +def validate_regexp(exp, idstring): + """Compile exp into a compiler regular expression object. + + Arguments: + exp: The string to compile into a re.Pattern object. + idstring: A string, what exp is a regexp for. + + Returns: + A re.Pattern object representing exp. + + 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. + + Arguments: + arg: The arguments to the command. The format is: + [locus-regexp [name-regexp]] + + Returns: + A 2-tuple of compiled regular expressions. + + Raises: + SyntaxError: an error processing ARG + """ + argv = gdb.string_to_argv(arg) + argc = len(argv) + if argc > 2: + raise SyntaxError("Too many arguments.") + locus_regexp = "" + name_regexp = "" + if argc >= 1: + locus_regexp = argv[0] + if argc >= 2: + name_regexp = argv[1] + return ( + validate_regexp(locus_regexp, "locus"), + validate_regexp(name_regexp, "handler"), + ) + + +class InfoMissingDebugHanders(gdb.Command): + """GDB command to list missing debug handlers. + + Usage: info missing-debug-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 + loci are listed. A locus can be 'global', 'progspace' to list + the handlers from the current progspace, or a regular expression + matching filenames of progspaces. + + NAME-REGEXP is a regular expression to filter missing debug + 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 list_handlers(self, title, handlers, name_re): + """Lists the missing debug handlers whose name matches regexp. + + Arguments: + title: The line to print before the list. + handlers: The list of the missing debug handlers. + name_re: handler name filter. + """ + if not handlers: + return + print(title) + for handler in handlers: + 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) + + 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 + ) + + for progspace in gdb.progspaces(): + filename = progspace.filename or "" + if locus_re.match(filename): + if filename == "": + if progspace == gdb.current_progspace(): + msg = "Current Progspace:" + else: + msg = "Progspace <no-file>:" + else: + msg = "Progspace %s:" % filename + self.list_handlers( + msg, + progspace.missing_debug_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) + + +def do_enable_handler1(handlers, name_re, flag): + """Enable/disable missing debug handlers whose names match given regex. + + Arguments: + handlers: The list of missing debug handlers. + name_re: Handler name filter. + flag: A boolean indicating if we should enable or disable. + + Returns: + The number of handlers affected. + """ + total = 0 + for handler in handlers: + 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) + total = 0 + if locus_re.match("global"): + total += do_enable_handler1(gdb.missing_debug_handlers, name_re, flag) + if locus_re.match("progspace") and locus_re.pattern != "": + total += do_enable_handler1( + gdb.current_progspace().missing_debug_handlers, name_re, flag + ) + 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) + print( + "%d missing debug handler%s %s" + % (total, "" if total == 1 else "s", "enabled" if flag else "disabled") + ) + + +class EnableMissingDebugHandler(gdb.Command): + """GDB command to enable missing debug handlers. + + Usage: enable missing-debug-handler [LOCUS-REGEXP [NAME-REGEXP]] + + LOCUS-REGEXP is a regular expression specifying the handlers to + enable. It can be 'global', 'progspace' for the current + progspace, or the filename for a file associated with a progspace. + + NAME_REGEXP is a regular expression to filter handler names. If + this omitted for a specified locus, then all registered handlers + in the locus are affected. + """ + + def __init__(self): + super().__init__("enable missing-debug-handler", gdb.COMMAND_FILES) + + def invoke(self, arg, from_tty): + """GDB calls this to perform the command.""" + do_enable_handler(arg, True) + + +class DisableMissingDebugHandler(gdb.Command): + """GDB command to disable missing debug handlers. + + Usage: disable missing-debug-handler [LOCUS-REGEXP [NAME-REGEXP]] + + LOCUS-REGEXP is a regular expression specifying the handlers to + enable. It can be 'global', 'progspace' for the current + progspace, or the filename for a file associated with a progspace. + + NAME_REGEXP is a regular expression to filter handler names. If + this omitted for a specified locus, then all registered handlers + in the locus are affected. + """ + + def __init__(self): + super().__init__("disable missing-debug-handler", gdb.COMMAND_FILES) + + def invoke(self, arg, from_tty): + """GDB calls this to perform the command.""" + do_enable_handler(arg, False) + + +def register_missing_debug_handler_commands(): + """Installs the missing debug handler commands.""" + InfoMissingDebugHanders() + EnableMissingDebugHandler() + DisableMissingDebugHandler() + + +register_missing_debug_handler_commands() diff --git a/gdb/python/lib/gdb/missing_debug.py b/gdb/python/lib/gdb/missing_debug.py new file mode 100644 index 0000000..42d6985 --- /dev/null +++ b/gdb/python/lib/gdb/missing_debug.py @@ -0,0 +1,169 @@ +# Copyright (C) 2023 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/>. + +""" +MissingDebugHandler base class, and register_handler function. +""" + +import gdb + + +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 ch.isascii() or not (ch.isalnum() or ch in "_-"): + raise ValueError("invalid character '%s' in handler name: %s" % (ch, name)) + + +class MissingDebugHandler(object): + """Base class for missing debug handlers written in Python. + + A missing debug 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 __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. + + Arguments: + objfile: A gdb.Objfile for which GDB could not find any + debug information. + + Returns: + True: GDB should try again to locate the debug information + for objfile, the handler may have installed the + missing information. + False: GDB should move on without the debug information + for objfile. + A string: GDB should load the file at the given path; it + contains the debug information for objfile. + None: This handler can't help with objfile. GDB should + try any other registered handlers. + """ + raise NotImplementedError("MissingDebugHandler.__call__()") + + +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) diff --git a/gdb/python/py-progspace.c b/gdb/python/py-progspace.c index f636ffd..0797ef1 100644 --- a/gdb/python/py-progspace.c +++ b/gdb/python/py-progspace.c @@ -55,6 +55,9 @@ struct pspace_object /* The debug method list. */ PyObject *xmethods; + + /* The missing debug handler list. */ + PyObject *missing_debug_handlers; }; extern PyTypeObject pspace_object_type @@ -164,6 +167,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_TYPE (self)->tp_free (self); } @@ -199,6 +203,10 @@ pspy_initialize (pspace_object *self) if (self->xmethods == NULL) return 0; + self->missing_debug_handlers = PyList_New (0); + if (self->missing_debug_handlers == nullptr) + return 0; + return 1; } @@ -353,6 +361,47 @@ pspy_get_xmethods (PyObject *o, void *ignore) return self->xmethods; } +/* Return the list of missing debug handlers for this program space. */ + +static PyObject * +pspy_get_missing_debug_handlers (PyObject *o, void *ignore) +{ + pspace_object *self = (pspace_object *) o; + + Py_INCREF (self->missing_debug_handlers); + return self->missing_debug_handlers; +} + +/* Set this program space's list of missing debug handlers to HANDLERS. */ + +static int +pspy_set_missing_debug_handlers (PyObject *o, PyObject *handlers, + void *ignore) +{ + pspace_object *self = (pspace_object *) o; + + if (handlers == nullptr) + { + PyErr_SetString (PyExc_TypeError, + "cannot delete the missing debug handlers list"); + return -1; + } + + if (!PyList_Check (handlers)) + { + PyErr_SetString (PyExc_TypeError, + "the missing debug handlers attribute must be a list"); + return -1; + } + + /* Take care in case the LHS and RHS are related somehow. */ + gdbpy_ref<> tmp (self->missing_debug_handlers); + Py_INCREF (handlers); + self->missing_debug_handlers = handlers; + + return 0; +} + /* Set the 'type_printers' attribute. */ static int @@ -745,6 +794,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 }, { NULL } }; diff --git a/gdb/python/python.c b/gdb/python/python.c index d569fb5..4523e30 100644 --- a/gdb/python/python.c +++ b/gdb/python/python.c @@ -127,7 +127,9 @@ static enum ext_lang_rc gdbpy_before_prompt_hook static gdb::optional<std::string> gdbpy_colorize (const std::string &filename, const std::string &contents); static gdb::optional<std::string> gdbpy_colorize_disasm - (const std::string &content, gdbarch *gdbarch); +(const std::string &content, gdbarch *gdbarch); +static ext_lang_missing_debuginfo_result gdbpy_handle_missing_debuginfo + (const struct extension_language_defn *extlang, struct objfile *objfile); /* The interface between gdb proper and loading of python scripts. */ @@ -173,6 +175,8 @@ static const struct extension_language_ops python_extension_ops = gdbpy_colorize_disasm, gdbpy_print_insn, + + gdbpy_handle_missing_debuginfo }; #endif /* HAVE_PYTHON */ @@ -1688,6 +1692,83 @@ gdbpy_get_current_objfile (PyObject *unused1, PyObject *unused2) return objfile_to_objfile_object (gdbpy_current_objfile).release (); } +/* Implement the 'handle_missing_debuginfo' hook for Python. GDB has + failed to find any debug information for OBJFILE. The extension has a + chance to record this, or even install the required debug information. + See the description of ext_lang_missing_debuginfo_result in + extension-priv.h for details of the return value. */ + +static ext_lang_missing_debuginfo_result +gdbpy_handle_missing_debuginfo (const struct extension_language_defn *extlang, + struct objfile *objfile) +{ + /* Early exit if Python is not initialised. */ + if (!gdb_python_initialized) + return {}; + + struct gdbarch *gdbarch = objfile->arch (); + + gdbpy_enter enter_py (gdbarch); + + /* Convert OBJFILE into the corresponding Python object. */ + gdbpy_ref<> pyo_objfile = objfile_to_objfile_object (objfile); + if (pyo_objfile == nullptr) + { + gdbpy_print_stack (); + return {}; + } + + /* Lookup the helper function within the GDB module. */ + gdbpy_ref<> pyo_handler + (PyObject_GetAttrString (gdb_python_module, "_handle_missing_debuginfo")); + 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_objfile.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_debuginfo_result (try_again); + } + + if (!gdbpy_is_string (pyo_execute_ret.get ())) + { + PyErr_SetString (PyExc_ValueError, + "return value from _handle_missing_debuginfo 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_debuginfo_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-debug.c b/gdb/testsuite/gdb.python/py-missing-debug.c new file mode 100644 index 0000000..8cbea3b --- /dev/null +++ b/gdb/testsuite/gdb.python/py-missing-debug.c @@ -0,0 +1,22 @@ +/* This test program is part of GDB, the GNU debugger. + + Copyright 2023 Free Software Foundation, Inc. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. */ + +int +main () +{ + return 0; +} diff --git a/gdb/testsuite/gdb.python/py-missing-debug.exp b/gdb/testsuite/gdb.python/py-missing-debug.exp new file mode 100644 index 0000000..3e0d70b --- /dev/null +++ b/gdb/testsuite/gdb.python/py-missing-debug.exp @@ -0,0 +1,473 @@ +# Copyright (C) 2023 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 + +if {[build_executable "failed to prepare" ${testfile} ${srcfile}]} { + return -1 +} + +# Remove debug information from BINFILE and place it into +# BINFILE.debug. +if {[gdb_gnu_strip_debug $binfile]} { + unsupported "cannot produce separate debug info files" + return -1 +} + +set remote_python_file \ + [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py] + +set debug_filename ${binfile}.debug +set hidden_filename ${binfile}.hidden + +# Start GDB. +clean_restart + +# Some initial sanity checks; initially, we can find the debug information +# (this will use the .gnu_debuglink), then after we move the debug +# information, reload the executable, now the debug can't be found. +with_test_prefix "initial checks" { + # Load BINFILE, we should find the separate debug information. + gdb_file_cmd $binfile + gdb_assert {$gdb_file_cmd_debug_info == "debug"} \ + "debug info is found" + + # Rename the debug information file, re-load BINFILE, GDB should fail + # to find the debug information + remote_exec build "mv $debug_filename $hidden_filename" + gdb_file_cmd $binfile + gdb_assert {$gdb_file_cmd_debug_info == "nodebug"} \ + "debug info no longer found" +} + +# Load the Python script into GDB. +gdb_test "source $remote_python_file" "^Success" \ + "source python script" + +# Setup the separate debug info directory. This isn't actually needed until +# some of the later tests, but might as well get this done now. +set debug_directory [standard_output_file "debug-dir"] +remote_exec build "mkdir -p $debug_directory" +gdb_test_no_output "set debug-file-directory $debug_directory" \ + "set debug-file-directory" + +# Initially the missing debug handler we install is in a mode where it +# returns None, indicating that it can't help locate the debug information. +# Check this works as expected. +with_test_prefix "handler returning None" { + gdb_test_no_output \ + "python gdb.missing_debug.register_handler(None, handler_obj)" \ + "register the initial handler" + + gdb_file_cmd $binfile + gdb_assert {$gdb_file_cmd_debug_info == "nodebug"} \ + "debug info not found" + + # Check the handler was only called once. + gdb_test "python print(handler_obj.call_count)" "^1" \ + "check handler was only called once" +} + +# Now configure the handler to move the debug file back to the +# .gnu_debuglink location and then return True, this will cause GDB to +# recheck, at which point it should find the debug info. +with_test_prefix "handler in gnu_debuglink mode" { + gdb_test_no_output "python handler_obj.set_mode(Mode.RETURN_TRUE, \ + \"$hidden_filename\", \ + \"$debug_filename\")" \ + "confirgure handler" + gdb_file_cmd $binfile + gdb_assert {$gdb_file_cmd_debug_info == "debug"} "debug info found" + + # Check the handler was only called once. + gdb_test "python print(handler_obj.call_count)" "^1" \ + "check handler was only called once" +} + +# Setup a directory structure based on the build-id of BINFILE, but don't +# move the debug information into place just yet. +# +# Instead, configure the handler to move the debug info into the build-id +# directory. +# +# Reload BINFILE, at which point the handler will move the debug info into +# the build-id directory and return True, GDB will then recheck for the +# debug information, and should find it. +with_test_prefix "handler in build-id mode" { + # Move the debug file out of the way once more. + remote_exec build "mv $debug_filename $hidden_filename" + + # Create the build-id based directory in which the debug information + # will be placed. + set build_id_filename \ + $debug_directory/[build_id_debug_filename_get $binfile] + remote_exec build "mkdir -p [file dirname $build_id_filename]" + + # Configure the handler to move the debug info into the build-id dir. + gdb_test_no_output "python handler_obj.set_mode(Mode.RETURN_TRUE, \ + \"$hidden_filename\", \ + \"$build_id_filename\")" \ + "confirgure handler" + + # Reload the binary and check the debug information is found. + gdb_file_cmd $binfile + gdb_assert {$gdb_file_cmd_debug_info == "debug"} "debug info found" + + # Check the handler was only called once. + gdb_test "python print(handler_obj.call_count)" "^1" \ + "check handler was only called once" +} + +# Move the debug information back to a hidden location and configure the +# handler to return the filename of the hidden debug info location. GDB +# should immediately use this file as the debug information. +with_test_prefix "handler returning a string" { + remote_exec build "mv $build_id_filename $hidden_filename" + + # Configure the handler return a filename string. + gdb_test_no_output "python handler_obj.set_mode(Mode.RETURN_STRING, \ + \"$hidden_filename\")" \ + "confirgure handler" + + # Reload the binary and check the debug information is found. + gdb_file_cmd $binfile + gdb_assert {$gdb_file_cmd_debug_info == "debug"} "debug info found" + + # Check the handler was only called once. + gdb_test "python print(handler_obj.call_count)" "^1" \ + "check handler was only called once" +} + +# Register another global handler, this one raises an exception. Reload the +# debug information, 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_debug.register_handler(None, rhandler)" + + foreach_with_prefix exception_type {gdb.GdbError TypeError} { + gdb_test_no_output \ + "python rhandler.exception_type = $exception_type" + + gdb_file_cmd $binfile + gdb_assert {$gdb_file_cmd_debug_info == "nodebug"} \ + "debug info not found" + + set re [string_to_regexp \ + "Python Exception <class '$exception_type'>: message"] + gdb_assert {[regexp $re $gdb_file_cmd_msg]} \ + "check for exception in file command output" + + # 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" + } +} + +gdb_test "info missing-debug-handlers" \ + [multi_line \ + "Global:" \ + " exception_handler" \ + " handler"] \ + "check both handlers are visible" + +# Re-start GDB. +clean_restart + +# Load the Python script into GDB. +gdb_test "source $remote_python_file" "^Success" \ + "source python script for bad handler name checks" + +# 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 while executing Python code\\."] + + 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-debug-handlers" \ + "check no handlers are registered" + +# Check we can use the enable/disable commands where there are no handlers +# registered. +gdb_test "enable missing-debug-handler foo" \ + "^0 missing debug handlers enabled" +gdb_test "disable missing-debug-handler foo" \ + "^0 missing debug handlers disabled" + +# 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-debug-handlers" \ + [multi_line \ + "Current Progspace:" \ + " abc-def" \ + " baz-" \ + "Global:" \ + " -bar" \ + " Foo"] + + gdb_file_cmd $binfile + gdb_test "python print(handler_call_log)" \ + [string_to_regexp {['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-debug-handler progspace baz-" \ + "^1 missing debug handler disabled" + + gdb_test "info missing-debug-handlers" \ + [multi_line \ + "Progspace \[^\r\n\]+:" \ + " abc-def" \ + " baz- \\\[disabled\\\]" \ + "Global:" \ + " -bar" \ + " Foo"] + + gdb_file_cmd $binfile + gdb_test "python print(handler_call_log)" \ + [string_to_regexp {['abc-def', '-bar', 'Foo']}] + gdb_test_no_output "python handler_call_log = \[\]" \ + "reset call log" +} + +with_test_prefix "disable 'Foo'" { + gdb_test "disable missing-debug-handler .* Foo" \ + "^1 missing debug handler disabled" + + gdb_test "info missing-debug-handlers" \ + [multi_line \ + "Progspace \[^\r\n\]+:" \ + " abc-def" \ + " baz- \\\[disabled\\\]" \ + "Global:" \ + " -bar" \ + " Foo \\\[disabled\\\]"] + + gdb_file_cmd $binfile + gdb_test "python print(handler_call_log)" \ + [string_to_regexp {['abc-def', '-bar']}] + gdb_test_no_output "python handler_call_log = \[\]" \ + "reset call log" +} + +with_test_prefix "disable everything" { + gdb_test "disable missing-debug-handler .* .*" \ + "^2 missing debug handlers disabled" + + gdb_test "info missing-debug-handlers" \ + [multi_line \ + "Progspace \[^\r\n\]+:" \ + " abc-def \\\[disabled\\\]" \ + " baz- \\\[disabled\\\]" \ + "Global:" \ + " -bar \\\[disabled\\\]" \ + " Foo \\\[disabled\\\]"] + + gdb_file_cmd $binfile + 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 $binfile] + + gdb_test "enable missing-debug-handler \"$re\" abc-def" \ + "^1 missing debug handler enabled" + + gdb_test "info missing-debug-handlers" \ + [multi_line \ + "Progspace \[^\r\n\]+:" \ + " abc-def" \ + " baz- \\\[disabled\\\]" \ + "Global:" \ + " -bar \\\[disabled\\\]" \ + " Foo \\\[disabled\\\]"] + + gdb_file_cmd $binfile + gdb_test "python print(handler_call_log)" \ + [string_to_regexp {['abc-def']}] + gdb_test_no_output "python handler_call_log = \[\]" \ + "reset call log" +} + +with_test_prefix "enable global handlers" { + set re [string_to_regexp $binfile] + + gdb_test "enable missing-debug-handler global" \ + "^2 missing debug handlers enabled" + + gdb_test "info missing-debug-handlers" \ + [multi_line \ + "Progspace \[^\r\n\]+:" \ + " abc-def" \ + " baz- \\\[disabled\\\]" \ + "Global:" \ + " -bar" \ + " Foo"] + + gdb_file_cmd $binfile + gdb_test "python print(handler_call_log)" \ + [string_to_regexp {['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 progspace list" { + gdb_test "enable missing-debug-handler progspace" \ + "^1 missing debug handler enabled" + + gdb_test_no_output \ + "python gdb.missing_debug.register_handler(None, handler_obj)" \ + "register the initial handler" + + gdb_test "info missing-debug-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" + + gdb_file_cmd $binfile + gdb_test "python print(handler_call_log)" \ + [string_to_regexp {['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 global list" { + gdb_test_no_output \ + "python gdb.missing_debug.register_handler(pspace, handler_obj)" \ + "register the initial handler" + + gdb_test "info missing-debug-handlers" \ + [multi_line \ + "Progspace \[^\r\n\]+:" \ + " handler" \ + " abc-def" \ + " baz-" \ + "Global:" \ + " handler" \ + " -bar" \ + " Foo"] + + gdb_file_cmd $binfile + gdb_test "python print(handler_call_log)" \ + [string_to_regexp {['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-debug-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_debug.register_handler(pspace, log_handler(\"Foo\"))" \ + [multi_line \ + "RuntimeError: Handler Foo already exists\\." \ + "Error while executing Python code\\."] + + gdb_test "python gdb.missing_debug.register_handler(handler=log_handler(\"Foo\"), locus=pspace)" \ + [multi_line \ + "RuntimeError: Handler Foo already exists\\." \ + "Error while executing Python code\\."] + + # 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_debug.register_handler(pspace, log_handler(\"Foo\"), replace=True)" + + gdb_test_no_output \ + "python gdb.missing_debug.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-debug-handler progspace Foo" \ + "^1 missing debug handler disabled" + + gdb_test "python gdb.missing_debug.register_handler(pspace, log_handler(\"Foo\"))" \ + [multi_line \ + "RuntimeError: Handler Foo already exists\\." \ + "Error while executing Python code\\."] \ + "still get an error when handler is disabled" + + gdb_test_no_output \ + "python gdb.missing_debug.register_handler(pspace, log_handler(\"Foo\"), replace=True)" \ + "can replace a disabled handler" +} diff --git a/gdb/testsuite/gdb.python/py-missing-debug.py b/gdb/testsuite/gdb.python/py-missing-debug.py new file mode 100644 index 0000000..720648e --- /dev/null +++ b/gdb/testsuite/gdb.python/py-missing-debug.py @@ -0,0 +1,120 @@ +# Copyright (C) 2023 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 gdb +from gdb.missing_debug import MissingDebugHandler +from enum import Enum +import os + +# A global log that is filled in by instances of the LOG_HANDLER class +# when they are called. +handler_call_log = [] + + +class Mode(Enum): + RETURN_NONE = 0 + RETURN_TRUE = 1 + RETURN_FALSE = 2 + RETURN_STRING = 3 + + +class handler(MissingDebugHandler): + def __init__(self): + super().__init__("handler") + self._call_count = 0 + self._mode = Mode.RETURN_NONE + + def __call__(self, objfile): + global handler_call_log + handler_call_log.append(self.name) + self._call_count += 1 + if self._mode == Mode.RETURN_NONE: + return None + + if self._mode == Mode.RETURN_TRUE: + os.rename(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 + + +class exception_handler(MissingDebugHandler): + def __init__(self): + super().__init__("exception_handler") + self.exception_type = None + + def __call__(self, objfile): + global handler_call_log + handler_call_log.append(self.name) + assert self.exception_type is not None + raise self.exception_type("message") + + +class log_handler(MissingDebugHandler): + def __call__(self, objfile): + global handler_call_log + 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_debug.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") |