From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from us-smtp-delivery-124.mimecast.com (us-smtp-delivery-124.mimecast.com [170.10.129.124]) by sourceware.org (Postfix) with ESMTPS id DAAFF388883C for ; Wed, 23 Mar 2022 22:41:58 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.1 sourceware.org DAAFF388883C Received: from mail-wr1-f70.google.com (mail-wr1-f70.google.com [209.85.221.70]) by relay.mimecast.com with ESMTP with STARTTLS (version=TLSv1.2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id us-mta-13-Q3z_s9KcNDO2Ou6IJor2fg-1; Wed, 23 Mar 2022 18:41:54 -0400 X-MC-Unique: Q3z_s9KcNDO2Ou6IJor2fg-1 Received: by mail-wr1-f70.google.com with SMTP id z16-20020adff1d0000000b001ef7dc78b23so982356wro.12 for ; Wed, 23 Mar 2022 15:41:54 -0700 (PDT) X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; h=x-gm-message-state:from:to:cc:subject:date:message-id:in-reply-to :references:mime-version:content-transfer-encoding; bh=ScPqGZF/A1VAtLRQ/vLp0tEbUJfspelCMP72ixfuDzk=; b=VJA0cWxTzbs/Aa0u7XYF9rqkaTn8FX1DZqR6ITV2sm3dzFoim119KbpA0Ys8SNut6r L3fgTfgIfUJZ0adACS6e8Et6zyCNEa01rLtO46o+ZP6Zln2HIIbIuhddEorxLVb45GAv Lv+CiUw2sA9P3LgMx+3pIJdHsV8ZIbmVGL0w5EeYtdnGG8/DIf4CEHUaqNPaPyTGmsgu /XmCFmWAnLPQjcztW8OiKTyfI2uswgbP2VQlWnep1QxaMTWSvNrNocpIHvzIYim6q+eG B23QP1kSF3qAYuNP5PEOgbi+VONAQdQo/yvkXKgIWsB7nTlYxXBZfZPW/TfuvThPFG+K vQTQ== X-Gm-Message-State: AOAM530i15CPvpJokK90Dvrh3G7ILatumyqr3CNqU0acmW5SnCZCN/lg jMlKB9PIJ3UG5GZptPrDkdBFAMn6sMVruZ801G2roiEs4UXQPv6VIQ2X6x3TQAJLgb4ktCV/f2s eqUbHdRTphMCmz5V3CBHEPiR1zb+2SoszlpYsRxu+aU6AtMbZh9BvqKBwo9+T9UBhiHlDn5gaEw == X-Received: by 2002:a05:600c:4fd5:b0:38c:ca19:1be2 with SMTP id o21-20020a05600c4fd500b0038cca191be2mr2055471wmq.167.1648075311356; Wed, 23 Mar 2022 15:41:51 -0700 (PDT) X-Google-Smtp-Source: ABdhPJybvSOnT8vapqV0lEH1Dn0PdWkP9p08y7aIBz4gUztV9xlGblQzpsc4UowWffhkoyPj5akaXg== X-Received: by 2002:a05:600c:4fd5:b0:38c:ca19:1be2 with SMTP id o21-20020a05600c4fd500b0038cca191be2mr2055373wmq.167.1648075309464; Wed, 23 Mar 2022 15:41:49 -0700 (PDT) Received: from localhost (host109-158-45-15.range109-158.btcentralplus.com. [109.158.45.15]) by smtp.gmail.com with ESMTPSA id l13-20020adfbd8d000000b002040daf5dffsm1320225wrh.18.2022.03.23.15.41.48 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 23 Mar 2022 15:41:48 -0700 (PDT) From: Andrew Burgess To: gdb-patches@sourceware.org Cc: Andrew Burgess Subject: [PATCHv2 3/3] gdb/python: implement the print_insn extension language hook Date: Wed, 23 Mar 2022 22:41:41 +0000 Message-Id: X-Mailer: git-send-email 2.25.4 In-Reply-To: References: MIME-Version: 1.0 X-Mimecast-Spam-Score: 0 X-Mimecast-Originator: redhat.com Content-Transfer-Encoding: 8bit Content-Type: text/plain; charset="US-ASCII"; x-default=true X-Spam-Status: No, score=-12.4 required=5.0 tests=BAYES_00, DKIMWL_WL_HIGH, DKIM_SIGNED, DKIM_VALID, DKIM_VALID_AU, DKIM_VALID_EF, GIT_PATCH_0, KAM_SHORT, RCVD_IN_DNSWL_NONE, RCVD_IN_MSPIKE_H4, RCVD_IN_MSPIKE_WL, SPF_HELO_NONE, SPF_NONE, TXREP, T_SCC_BODY_TEXT_LINE autolearn=ham autolearn_force=no version=3.4.4 X-Spam-Checker-Version: SpamAssassin 3.4.4 (2020-01-24) on server2.sourceware.org X-BeenThere: gdb-patches@sourceware.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: Gdb-patches mailing list List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Wed, 23 Mar 2022 22:42:04 -0000 From: Andrew Burgess This commit extends the Python API to include disassembler support. The motivation for this commit was to provide an API by which the user could write Python scripts that would augment the output of the disassembler. To achieve this I have followed the model of the existing libopcodes disassembler, that is, instructions are disassembled one by one. This does restrict the type of things that it is possible to do from a Python script, i.e. all additional output has to fit on a single line, but this was all I needed, and creating something more complex would, I think, require greater changes to how GDB's internal disassembler operates. The disassembler API is contained in the new gdb.disassembler module, which defines the following classes: DisassembleInfo Similar to libopcodes disassemble_info structure, has read-only properties: address, architecture, and progspace. And has methods: read_memory, and is_valid. Each time GDB wants an instruction disassembled, an instance of this class is passed to a user written disassembler function, by reading the properties, and calling the methods (and other support methods in the gdb.disassembler module) the user can perform and return the disassembly. Disassembler This is a base-class which user written disassemblers should inherit from. This base class just provides base implementations of __init__ and __call__ which the user written disassembler should override. DisassemblerResult This class can be used to hold the result of a call to the disassembler, it's really just a wrapper around a string (the text of the disassembled instruction) and a length (in bytes). The user can return an instance of this class from Disassembler.__call__ to represent the newly disassembled instruction. The gdb.disassembler module also provides the following functions: register_disassembler This function registers an instance of a Disassembler sub-class as a disassembler, either for one specific architecture, or, as a global disassembler for all architectures. builtin_disassemble This provides access to GDB's builtin disassembler. A common use case that I see is augmenting the existing disassembler output. The user code can call this function to have GDB disassemble the instruction in the normal way. The user gets back a DisassemblerResult object, which they can then read in order to augment the disassembler output in any way they wish. This function also provides a mechanism to intercept the disassemblers reads of memory, thus the user can adjust what GDB sees when it is disassembling. The included documentation provides a more detailed description of the API. --- gdb/Makefile.in | 1 + gdb/NEWS | 34 + gdb/data-directory/Makefile.in | 1 + gdb/doc/python.texi | 239 ++++++ gdb/python/lib/gdb/disassembler.py | 109 +++ gdb/python/py-disasm.c | 970 +++++++++++++++++++++++++ gdb/python/python-internal.h | 16 + gdb/python/python.c | 3 +- gdb/testsuite/gdb.python/py-disasm.c | 25 + gdb/testsuite/gdb.python/py-disasm.exp | 150 ++++ gdb/testsuite/gdb.python/py-disasm.py | 456 ++++++++++++ 11 files changed, 2003 insertions(+), 1 deletion(-) create mode 100644 gdb/python/lib/gdb/disassembler.py create mode 100644 gdb/python/py-disasm.c create mode 100644 gdb/testsuite/gdb.python/py-disasm.c create mode 100644 gdb/testsuite/gdb.python/py-disasm.exp create mode 100644 gdb/testsuite/gdb.python/py-disasm.py diff --git a/gdb/Makefile.in b/gdb/Makefile.in index aecab41eeb8..bbcf8c467dc 100644 --- a/gdb/Makefile.in +++ b/gdb/Makefile.in @@ -393,6 +393,7 @@ SUBDIR_PYTHON_SRCS = \ python/py-cmd.c \ python/py-connection.c \ python/py-continueevent.c \ + python/py-disasm.c \ python/py-event.c \ python/py-evtregistry.c \ python/py-evts.c \ diff --git a/gdb/NEWS b/gdb/NEWS index e10062752d0..723ca6d5fee 100644 --- a/gdb/NEWS +++ b/gdb/NEWS @@ -16,6 +16,40 @@ This is the same format that GDB uses when printing address, symbol, and offset information from the disassembler. + ** New Python API for wrapping GDB's disassembler: + + - gdb.disassembler.register_disassembler(DISASSEMBLER, ARCH). + DISASSEMBLER is a sub-class of gdb.disassembler.Disassembler. + ARCH is either None or a string containing a bfd architecture + name. DISASSEMBLER is registered as a disassembler for + architecture ARCH, or for all architectures if ARCH is None. + The previous disassembler registered for ARCH is returned, this + can be None if no previous disassembler was registered. + + - gdb.disassembler.Disassembler is the class from which all + disassemblers should inherit. Its constructor takes a string, + a name for the disassembler, which is currently only used is + some debug output. Sub-classes should override the __call__ + method to perform disassembly, invoking __call__ on this base + class will raise an exception. + + - gdb.disassembler.DisassembleInfo is the class used to describe + a single disassembly request from GDB. An instance of this + class is passed to the __call__ method of + gdb.disassembler.Disassembler and has the following read-only + attributes: 'address', and 'architecture', as well as the + following method: 'read_memory'. + + - gdb.disassembler.builtin_disassemble(INFO, MEMORY_SOURCE), + calls GDB's builtin disassembler on INFO, which is a + gdb.disassembler.DisassembleInfo object. MEMORY_SOURCE is + optional, its default value is None. If MEMORY_SOURCE is not + None then it must be an object that has a 'read_memory' method. + + - gdb.disassembler.DisassemblerResult is a class that can be used + to wrap the result of a call to a Disassembler. It has + read-only attributes 'length' and 'string'. + *** Changes in GDB 12 * DBX mode is deprecated, and will be removed in GDB 13 diff --git a/gdb/data-directory/Makefile.in b/gdb/data-directory/Makefile.in index b606fc654b5..cf5226f3961 100644 --- a/gdb/data-directory/Makefile.in +++ b/gdb/data-directory/Makefile.in @@ -69,6 +69,7 @@ PYTHON_DIR = python PYTHON_INSTALL_DIR = $(DESTDIR)$(GDB_DATADIR)/$(PYTHON_DIR) PYTHON_FILE_LIST = \ gdb/__init__.py \ + gdb/disassembler.py \ gdb/FrameDecorator.py \ gdb/FrameIterator.py \ gdb/frames.py \ diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi index 7c414b01d70..8eb112dd99a 100644 --- a/gdb/doc/python.texi +++ b/gdb/doc/python.texi @@ -222,6 +222,7 @@ * Registers In Python:: Python representation of registers. * Connections In Python:: Python representation of connections. * TUI Windows In Python:: Implementing new TUI windows. +* Disassembly In Python:: Instruction Disassembly In Python @end menu @node Basic Python @@ -598,6 +599,7 @@ related prompts are prohibited from being changed. @end defun +@anchor{gdb_architecture_names} @defun gdb.architecture_names () Return a list containing all of the architecture names that the current build of @value{GDBN} supports. Each architecture name is a @@ -3278,6 +3280,7 @@ particular frame (@pxref{Frames In Python}). @end defun +@anchor{gdbpy_inferior_read_memory} @findex Inferior.read_memory @defun Inferior.read_memory (address, length) Read @var{length} addressable memory units from the inferior, starting at @@ -6558,6 +6561,242 @@ values can be 1 (left), 2 (middle), or 3 (right). @end defun +@node Disassembly In Python +@cindex python instruction disassembly +@subsubsection Instruction Disassembly In Python + +@value{GDBN}'s builtin disassembler can be extended, or even replaced, +using the Python API. The disassembler related features are contained +within the @code{gdb.disassembler} module: + +@deftp {class} gdb.disassembler.DisassembleInfo +Disassembly is driven by instances of this class. Each time +@value{GDBN} needs to disassemble an instruction, an instance of this +class is created and passed to a registered disassembler. The +disassembler is then responsible for disassembling an instruction and +returning a result. This class has the following attributes: + +@defivar DisassembleInfo address +An integer containing the address at which @value{GDBN} wishes to +disassemble a single instruction. +@end defivar + +@defivar DisassembleInfo architecture +The @code{gdb.Architecture} (@pxref{Architectures In Python}) for +which @value{GDBN} is currently disassembling. +@end defivar + +@defivar DisassembleInfo progspace +The @code{gdb.Progspace} (@pxref{Progspaces In Python,,Program Spaces +In Python}) for which @value{GDBN} is currently disassembling. +@end defivar + +@defmethod DisassembleInfo read_memory (length, offset) +This method allows the disassembler to read the bytes of the +instruction to be disassembled. The method reads @var{length} bytes, +starting at @var{offset} from +@code{DisassembleInfo.address}. + +It is important that the disassembler read the instruction bytes using +this method, rather than reading inferior memory directly, as in some +cases @value{GDBN} disassembles from an internal buffer rather than +directly from inferior memory. + +Returns a buffer object, which behaves much like an array or a string, +just as @code{Inferior.read_memory} does +(@pxref{gdbpy_inferior_read_memory,,Inferior.read_memory}). +@end defmethod + +@defmethod DisassembleInfo is_valid () +Returns @code{True} if the @code{DisassembleInfo} object is valid, +@code{False} if not. A @code{DisassembleInfo} object will become +invalid once the disassembly call for which the @code{DisassembleInfo} +was created, has returned. Calling other @code{DisassembleInfo} +methods, or accessing @code{DisassembleInfo} properties, will raise a +@code{RuntimeError} exception if it is invalid. +@end defmethod +@end deftp + +@deftp {class} Disassembler +This is a base class from which all user implemented disassemblers +must inherit. + +@defmethod Disassembler __init__ (name) +The constructor takes @var{name}, a string, which should be a short +name for this disassembler. Currently, this name is only used in some +debug output. +@end defmethod + +@defmethod Disassembler __call__ (info) +The @code{__call__} method must be overridden by sub-classes to +perform disassembly. Calling @code{__call__} on this base class will +raise a @code{NotImplementedError} exception. + +The @var{info} argument is an instance of @code{DisassembleInfo}, and +describes the instruction that @value{GDBN} wants disassembling. + +If this function returns @code{None} then this indicates to +@value{GDBN} that this sub-class doesn't wish to disassemble the +requested instruction, @value{GDBN} will then use its builtin +disassembler to perform the disassembly. + +Or, this function can return an object that represents the +disassembled instruction. The object must have the following two +attributes: + +@defvar length +The length of the disassembled instruction in bytes, which must be +greater than zero. +@end defvar + +@defvar string +A non-empty string representing the disassembled instruction. +@end defvar + +The @code{DisassemblerResult} type is defined as a possible class to +represent disassembled instructions, but it is not required to use +this type, so long as the required attributes are present. + +The @code{__call__} method can raise a @code{gdb.MemoryError} +exception (@pxref{Exception Handling}) to indicate to @value{GDBN} +that there was a problem accessing the required memory, this will then +be displayed by @value{GDBN} within the disassembler output. + +Any other exception type raised by the @code{__call__} method is an +error, @value{GDBN} will display the error and then use its builtin +disassembler to disassemble the instruction instead. +@end defmethod +@end deftp + +@deftp {class} DisassemblerResult +This class is provided as a means to hold the result of calling +@code{Disassembler.__call__}. It is not required to use this type, +any type with the required attributes will do. + +The required attributes, which this class provides are: + +@defvar length +The length of the disassembled instruction in bytes, which must be +greater than zero. +@end defvar + +@defvar string +A non-empty string representing the disassembled instruction. +@end defvar + +This class also provides a constructor: + +@defun DisassemblerResult.__init__ (@var{length}, @var{string}) +Initialise an instance of this class, @var{length} is the length of +the disassembled instruction in bytes, which must be greater than +zero, and @var{string} is a non-empty string that represents the +disassembled instruction. +@end defun +@end deftp + +@defun register_disassembler (disassembler, architecture) +The @var{disassembler} must be a sub-class of @code{Disassembler}. + +The optional @var{architecture} is either a string, or the value +@code{None}. If it is a string, then it should be the name of an +architecture known to @value{GDBN}, as returned either from +@code{gdb.Architecture.name} +(@pxref{gdbpy_architecture_name,,gdb.Architecture.name}), or from +@code{gdb.architecture_names} +(@pxref{gdb_architecture_names,,gdb.architecture_names}). + +The @var{disassembler} will be installed for the architecture named by +@var{architecture}, or if @var{architecture} is @code{None}, then +@var{disassembler} will be installed as a global disassembler for use +by all architectures. + +@value{GDBN} only records a single disassembler for each architecture, +and a single global disassembler. Calling +@code{register_disassembler} for an architecture, or for the global +disassembler, will replace any existing disassembler registered for +that @var{architecture} value. The previous disassembler is returned. + +When @value{GDBN} is looking for a disassembler to use, @value{GDBN} +first looks for an architecture specific disassembler. If none has +been registered then @value{GDBN} looks for a global disassembler (one +registered with @var{architecture} set to @code{None}). Only one +disassembler is called to perform disassembly, so, if there is both an +architecture specific disassembler, and a global disassembler +registered, it is the architecture specific disassembler that will be +used. + +@value{GDBN} tracks the architecture specific, and global +disassemblers separately, so it doesn't matter in which order +disassemblers are created or registed, an architecture specific +disassembler, if present, will always be used before a global +disassembler. +@end defun + +@defun builtin_disassemble (info, memory_source) +This function calls back into @value{GDBN}'s builtin disassembler to +disassemble the instruction identified by @var{info}, an instance of +@code{DisassembleInfo}. + +If the requested instruction disassembled successfully then an instance +of @code{DisassemblerResult} is returned. + +If the builtin disassembler fails then this function will raise a +@code{gdb.MemoryError} exception. + +The optional @var{memory_source} argument has the default value of +@code{None}, in which case, the builtin disassembler will read the +instruction from memory in the normal way. + +If @var{memory_source} is not @code{None}, then it should be an +instance of a class that implements the following method: + +@defmethod memory_source read_memory (length, offset) +This method will be called by the builtin disassembler to fetch bytes +of the instruction being disassembled. @var{length} is the number of +bytes to fetch, and @var{offset} is the offset from the address of the +instruction being disassembled, this address is obtained from +@code{DisassembleInfo.address}. + +This function should return a Python object that supports the buffer +protocol, i.e.@: a string, an array, or the object returned from +@code{DisassembleInfo.read_memory}. + +The length of the returned buffer @emph{must} be @var{length} +otherwise a @code{ValueError} exception will be raised. + +Alternatively, this function can raise a @code{gdb.MemoryError} +exception to indicate that the read failed, raising any other +exception type is an error. + +It is important to understand that, even when this function raises a +@code{gdb.MemoryError}, it is the internal disassembler itself that +reports the memory error to @value{GDBN}. The reason for this is that +the disassembler might probe memory to see if a byte is readable or +not, if the byte can't be read then the disassembler may choose not to +report an error, but to instead disassemble the bytes that it does +have available. +@end defmethod +@end defun + +Here is an example that registers a global disassembler. The new +disassembler invokes the builtin disassembler, and then adds a +comment, @code{## Comment}, to each line of disassembly output: + +@smallexample +class ExampleDisassembler(gdb.disassembler.Disassembler): + def __init__(self): + super(ExampleDisassembler, self).__init__("ExampleDisassembler") + + def __call__(self, info): + result = gdb.disassembler.builtin_disassemble(info) + if result.string is not None: + length = result.length + text = result.string + "\t## Comment" + return gdb.disassembler.DisassemblerResult(length, text) + +gdb.disassembler.register_disassembler(ExampleDisassembler()) +@end smallexample + @node Python Auto-loading @subsection Python Auto-loading @cindex Python auto-loading diff --git a/gdb/python/lib/gdb/disassembler.py b/gdb/python/lib/gdb/disassembler.py new file mode 100644 index 00000000000..19ec0ecf82f --- /dev/null +++ b/gdb/python/lib/gdb/disassembler.py @@ -0,0 +1,109 @@ +# Copyright (C) 2021-2022 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 . + +"""Disassembler related module.""" + +import gdb +import _gdb.disassembler + +from _gdb.disassembler import * + +# Module global dictionary of gdb.disassembler.Disassembler objects. +# The keys of this dictionary are bfd architecture names, or the +# special value None. +# +# When a request to disassemble comes in we first lookup the bfd +# architecture name from the gdbarch, if that name exists in this +# dictionary then we use that Disassembler object. +# +# If there's no architecture specific disassembler then we look for +# the key None in this dictionary, and if that key exists, we use that +# disassembler. +# +# If none of the above checks found a suitable disassembler, then no +# disassembly is performed in Python. +_disassemblers_dict = {} + + +class Disassembler(object): + """A base class from which all user implemented disassemblers must + inherit.""" + + def __init__(self, name): + """Constructor. Takes a name, which should be a string, which can be + used to identify this disassembler in diagnostic messages.""" + self.name = name + + def __call__(self, info): + """A default implementation of __call__. All sub-classes must + override this method. Calling this default implementation will throw + a NotImplementedError exception.""" + raise NotImplementedError("Disassembler.__call__") + + +def register_disassembler(disassembler, architecture=None): + """Register a disassembler. DISASSEMBLER is a sub-class of + gdb.disassembler.Disassembler. ARCHITECTURE is either None or a + string, the name of an architecture known to GDB. + + DISASSEMBLER is registered as a disassmbler for ARCHITECTURE, or + all architectures when ARCHITECTURE is None. + + Returns the previous disassembler registered with this + ARCHITECTURE value. + """ + + if not isinstance(disassembler, Disassembler) and disassembler is not None: + raise TypeError("disassembler should sub-class gdb.disassembler.Disassembler") + + old = None + if architecture in _disassemblers_dict: + old = _disassemblers_dict[architecture] + del _disassemblers_dict[architecture] + if disassembler is not None: + _disassemblers_dict[architecture] = disassembler + + # Call the private _set_enabled function within the + # _gdb.disassembler module. This function sets a global flag + # within GDB's C++ code that enables or dissables the Python + # disassembler functionality, this improves performance of the + # disassembler by avoiding unneeded calls into Python when we know + # that no disassemblers are registered. + _gdb.disassembler._set_enabled(len(_disassemblers_dict) > 0) + return old + + +def _print_insn(info): + """This function is called by GDB when it wants to disassemble an + instruction. INFO describes the instruction to be + disassembled.""" + + def lookup_disassembler(arch): + try: + name = arch.name() + if name is None: + return None + if name in _disassemblers_dict: + return _disassemblers_dict[name] + if None in _disassemblers_dict: + return _disassemblers_dict[None] + return None + except: + return None + + disassembler = lookup_disassembler(info.architecture) + if disassembler is None: + return None + return disassembler(info) diff --git a/gdb/python/py-disasm.c b/gdb/python/py-disasm.c new file mode 100644 index 00000000000..9aa1b156023 --- /dev/null +++ b/gdb/python/py-disasm.c @@ -0,0 +1,970 @@ +/* Python interface to instruction disassembly. + + Copyright (C) 2021-2022 Free Software Foundation, Inc. + + This file is part of GDB. + + 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 . */ + +#include "defs.h" +#include "python-internal.h" +#include "dis-asm.h" +#include "arch-utils.h" +#include "charset.h" +#include "disasm.h" +#include "progspace.h" + +/* Implement gdb.disassembler.DisassembleInfo type. An object of this type + represents a single disassembler request from GDB. */ + +struct disasm_info_object { + PyObject_HEAD + + /* The architecture in which we are disassembling. */ + struct gdbarch *gdbarch; + + /* The program_space in which we are disassembling. */ + struct program_space *program_space; + + /* Address of the instruction to disassemble. */ + bfd_vma address; + + /* The disassemble_info passed from core GDB, this contains the + callbacks necessary to read the instruction from core GDB, and to + print the disassembled instruction. */ + disassemble_info *gdb_info; +}; + +extern PyTypeObject disasm_info_object_type + CPYCHECKER_TYPE_OBJECT_FOR_TYPEDEF ("disasm_info_object"); + +/* Implement gdb.disassembler.DisassemblerResult type, an object that holds + the result of calling the disassembler. This is mostly the length of + the disassembled instruction (in bytes), and the string representing the + disassembled instruction. */ + +struct disasm_result_object { + PyObject_HEAD + + /* The length of the disassembled instruction in bytes. */ + int length; + + /* A buffer which, when allocated, holds the disassembled content of an + instruction. */ + string_file *content; +}; + +extern PyTypeObject disasm_result_object_type + CPYCHECKER_TYPE_OBJECT_FOR_TYPEDEF ("disasm_result_object"); + +/* When this is false we fast path out of gdbpy_print_insn, which should + keep the performance impact of the Python disassembler down. This is + set to true from Python by calling gdb.disassembler._set_enabled() when + the user registers a disassembler. */ + +static bool python_print_insn_enabled = false; + +/* A sub-class of gdb_disassembler that holds a pointer to a Python + DisassembleInfo object. A pointer to an instance of this class is + placed in the application_data field of the disassemble_info that is + used when we call gdbarch_print_insn. */ + +struct gdbpy_disassembler : public gdb_disassemble_info +{ + /* Constructor. */ + gdbpy_disassembler (disasm_info_object *obj, PyObject *memory_source); + + /* Get the DisassembleInfo object pointer. */ + disasm_info_object * + py_disasm_info () const + { + return m_disasm_info_object; + } + + /* Callbacks used by disassemble_info. */ + static void memory_error_func (int status, bfd_vma memaddr, + struct disassemble_info *info); + static void print_address_func (bfd_vma addr, + struct disassemble_info *info); + static int read_memory_func (bfd_vma memaddr, gdb_byte *buff, + unsigned int len, + struct disassemble_info *info); + + /* Return a reference to an optional that contains the address at which a + memory error occurred. The optional will only have a value if a + memory error actually occurred. */ + const gdb::optional &memory_error_address () const + { return m_memory_error_address; } + + /* Return the content of the disassembler as a string. The contents are + moved out of the disassembler, so after this call the disassembler + contents have been reset back to empty. */ + std::string release () + { + return m_string_file.release (); + } + +private: + + /* Where the disassembler result is written. */ + string_file m_string_file; + + /* The DisassembleInfo object we are disassembling for. */ + disasm_info_object *m_disasm_info_object; + + /* When the user indicates that a memory error has occurred then the + address of the memory error is stored in here. */ + gdb::optional m_memory_error_address; + + /* When the user calls the builtin_disassemble function, if they pass a + memory source object then a pointer to the object is placed in here, + otherwise, this field is nullptr. */ + PyObject *m_memory_source; +}; + +/* Return true if OBJ is still valid, otherwise, return false. A valid OBJ + will have a non-nullptr gdb_info field. */ + +static bool +disasm_info_object_is_valid (disasm_info_object *obj) +{ + return obj->gdb_info != nullptr; +} + +/* Implement DisassembleInfo.is_valid(), really just a wrapper around the + disasm_info_object_is_valid function above. */ + +static PyObject * +disasmpy_info_is_valid (PyObject *self, PyObject *args) +{ + disasm_info_object *disasm_obj = (disasm_info_object *) self; + + if (disasm_info_object_is_valid (disasm_obj)) + Py_RETURN_TRUE; + + Py_RETURN_FALSE; +} + +/* Set the Python exception to be a gdb.MemoryError object, with ADDRESS + as its payload. */ + +static void +disasmpy_set_memory_error_for_address (CORE_ADDR address) +{ + PyObject *address_obj = gdb_py_object_from_longest (address).release (); + PyErr_SetObject (gdbpy_gdb_memory_error, address_obj); +} + + +/* Ensure that a gdb.disassembler.DisassembleInfo is valid. */ +#define DISASMPY_DISASM_INFO_REQUIRE_VALID(Info) \ + do { \ + if (!disasm_info_object_is_valid (Info)) \ + { \ + PyErr_SetString (PyExc_RuntimeError, \ + _("DisassembleInfo is no longer valid.")); \ + return nullptr; \ + } \ + } while (0) + +/* Implement gdb.disassembler.builtin_disassemble(). Calls back into GDB's + builtin disassembler. The first argument is a DisassembleInfo object + describing what to disassemble. The second argument is optional and + provides a mechanism to modify the memory contents that the builtin + disassembler will actually disassemble. + + Returns an instance of gdb.disassembler.DisassemblerResult, an object + that wraps a disassembled instruction, or it raises a + gdb.MemoryError. */ + +static PyObject * +disasmpy_builtin_disassemble (PyObject *self, PyObject *args, PyObject *kw) +{ + PyObject *info_obj, *memory_source_obj = nullptr; + static const char *keywords[] = { "info", "memory_source", nullptr }; + if (!gdb_PyArg_ParseTupleAndKeywords (args, kw, "O!|O", keywords, + &disasm_info_object_type, &info_obj, + &memory_source_obj)) + return nullptr; + + disasm_info_object *disasm_info = (disasm_info_object *) info_obj; + if (!disasm_info_object_is_valid (disasm_info)) + { + PyErr_SetString (PyExc_RuntimeError, + _("DisassembleInfo is no longer valid.")); + return nullptr; + } + + /* A memory source is any object that provides the 'read_memory' + callback. At this point we only check for the existence of a + 'read_memory' attribute, if this isn't callable then we'll throw an + exception from within gdbpy_disassembler::read_memory_func. */ + if (memory_source_obj != nullptr) + { + if (!PyObject_HasAttrString (memory_source_obj, "read_memory")) + { + PyErr_SetString (PyExc_TypeError, + _("memory_source doesn't have a read_memory method")); + return nullptr; + } + } + + /* Where the result will be written. */ + gdbpy_disassembler disassembler (disasm_info, memory_source_obj); + + /* Now actually perform the disassembly. */ + int length + = gdbarch_print_insn (disasm_info->gdbarch, disasm_info->address, + disassembler.disasm_info ()); + + if (length == -1) + { + + /* In an ideal world, every disassembler should always call the + memory error function before returning a status of -1 as the only + error a disassembler should encounter is a failure to read + memory. Unfortunately, there are some disassemblers who don't + follow this rule, and will return -1 without calling the memory + error function. + + To make the Python API simpler, we just classify everything as a + memory error, but the message has to be modified for the case + where the disassembler didn't call the memory error function. */ + if (disassembler.memory_error_address ().has_value ()) + { + CORE_ADDR addr = *disassembler.memory_error_address (); + disasmpy_set_memory_error_for_address (addr); + } + else + PyErr_Format (gdbpy_gdb_memory_error, "unknown disassembly error"); + return nullptr; + } + + /* Instructions are either non-zero in length, or we got an error, + indicated by a length of -1, which we handled above. */ + gdb_assert (length > 0); + + /* We should not have seen a memory error in this case. */ + gdb_assert (!disassembler.memory_error_address ().has_value ()); + + /* Create an object to represent the result of the disassembler. */ + gdbpy_ref res + (PyObject_New (disasm_result_object, &disasm_result_object_type)); + res->length = length; + res->content = new string_file; + *(res->content) = disassembler.release (); + + return reinterpret_cast (res.release ()); +} + +/* Implement gdb.set_enabled function. Takes a boolean parameter, and + sets whether GDB should enter the Python disassembler code or not. + + This is called from within the Python code when a new disassembler is + registered. When no disassemblers are registered the global C++ flag + is set to false, and GDB never even enters the Python environment to + check for a disassembler. + + When the user registers a new Python disassembler, the global C++ flag + is set to true, and now GDB will enter the Python environment to check + if there's a disassembler registered for the current architecture. */ + +static PyObject * +disasmpy_set_enabled (PyObject *self, PyObject *args, PyObject *kw) +{ + PyObject *newstate; + static const char *keywords[] = { "state", nullptr }; + if (!gdb_PyArg_ParseTupleAndKeywords (args, kw, "O", keywords, + &newstate)) + return nullptr; + + if (!PyBool_Check (newstate)) + { + PyErr_SetString (PyExc_TypeError, + _("The value passed to `_set_enabled' must be a boolean.")); + return nullptr; + } + + python_print_insn_enabled = PyObject_IsTrue (newstate); + Py_RETURN_NONE; +} + +/* Implement DisassembleInfo.read_memory(LENGTH, OFFSET). Read LENGTH + bytes at OFFSET from the start of the instruction currently being + disassembled, and return a memory buffer containing the bytes. + + OFFSET defaults to zero if it is not provided. LENGTH is required. If + the read fails then this will raise a gdb.MemoryError exception. */ + +static PyObject * +disasmpy_info_read_memory (PyObject *self, PyObject *args, PyObject *kw) +{ + disasm_info_object *obj = (disasm_info_object *) self; + DISASMPY_DISASM_INFO_REQUIRE_VALID (obj); + + LONGEST length, offset = 0; + gdb::unique_xmalloc_ptr buffer; + static const char *keywords[] = { "length", "offset", nullptr }; + + if (!gdb_PyArg_ParseTupleAndKeywords (args, kw, "L|L", keywords, + &length, &offset)) + return nullptr; + + /* The apparent address from which we are reading memory. Note that in + some cases GDB actually disassembles instructions from a buffer, so + we might not actually be reading this information directly from the + inferior memory. This is all hidden behind the read_memory_func API + within the disassemble_info structure. */ + CORE_ADDR address = obj->address + offset; + + /* Setup a buffer to hold the result. */ + buffer.reset ((gdb_byte *) xmalloc (length)); + + /* Read content into BUFFER. If the read fails then raise a memory + error, otherwise, convert BUFFER to a Python memory buffer, and return + it to the user. */ + disassemble_info *info = obj->gdb_info; + if (info->read_memory_func ((bfd_vma) address, buffer.get (), + (unsigned int) length, info) != 0) + { + disasmpy_set_memory_error_for_address (address); + return nullptr; + } + return gdbpy_buffer_to_membuf (std::move (buffer), address, length); +} + +/* Implement DisassembleInfo.address attribute, return the address at which + GDB would like an instruction disassembled. */ + +static PyObject * +disasmpy_info_address (PyObject *self, void *closure) +{ + disasm_info_object *obj = (disasm_info_object *) self; + DISASMPY_DISASM_INFO_REQUIRE_VALID (obj); + return gdb_py_object_from_longest (obj->address).release (); +} + +/* Implement DisassembleInfo.architecture attribute. Return the + gdb.Architecture in which we are disassembling. */ + +static PyObject * +disasmpy_info_architecture (PyObject *self, void *closure) +{ + disasm_info_object *obj = (disasm_info_object *) self; + DISASMPY_DISASM_INFO_REQUIRE_VALID (obj); + return gdbarch_to_arch_object (obj->gdbarch); +} + +/* Implement DisassembleInfo.progspace attribute. Return the + gdb.Progspace in which we are disassembling. */ + +static PyObject * +disasmpy_info_progspace (PyObject *self, void *closure) +{ + disasm_info_object *obj = (disasm_info_object *) self; + DISASMPY_DISASM_INFO_REQUIRE_VALID (obj); + return pspace_to_pspace_object (obj->program_space).release (); +} + +/* This implements the disassemble_info read_memory_func callback. This + will either call the standard read memory function, or, if the user has + supplied a memory source (see disasmpy_builtin_disassemble) then this + will call back into Python to obtain the memory contents. + + Read LEN bytes from MEMADDR and place them into BUFF. Return 0 on + success (in which case BUFF has been filled), or -1 on error, in which + case the contents of BUFF are undefined. */ + +int +gdbpy_disassembler::read_memory_func (bfd_vma memaddr, gdb_byte *buff, + unsigned int len, + struct disassemble_info *info) +{ + gdbpy_disassembler *dis + = static_cast (info->application_data); + disasm_info_object *obj = dis->py_disasm_info (); + PyObject *memory_source = dis->m_memory_source; + + /* The simple case, the user didn't pass a separate memory source, so we + just delegate to the standard disassemble_info read_memory_func, + passing in the original disassemble_info object, which core GDB might + require in order to read the instruction bytes (when reading the + instruction from a buffer). */ + if (memory_source == nullptr) + return obj->gdb_info->read_memory_func (memaddr, buff, len, obj->gdb_info); + + /* The user provided a separate memory source, we need to call the + read_memory method on the memory source and use the buffer it returns + as the bytes of memory. */ + LONGEST offset = (LONGEST) memaddr - (LONGEST) obj->address; + gdbpy_ref<> result_obj (PyObject_CallMethod (memory_source, "read_memory", + "KL", len, offset)); + if (result_obj == nullptr) + { + /* If we got a gdb.MemoryError then we ignore this and just report + that the read failed to the caller. The caller is then + responsible for calling the memory_error_func if it wants to. + Remember, the disassembler might just be probing to see if these + bytes can be read, if we automatically call the memory error + function, we can end up registering an error prematurely. */ + if (PyErr_ExceptionMatches (gdbpy_gdb_memory_error)) + PyErr_Clear (); + else + gdbpy_print_stack (); + return -1; + } + + /* Convert the result to a buffer. */ + Py_buffer py_buff; + if (!PyObject_CheckBuffer (result_obj.get ()) + || PyObject_GetBuffer (result_obj.get(), &py_buff, PyBUF_CONTIG_RO) < 0) + { + PyErr_Format (PyExc_TypeError, + _("Result from read_memory is not a buffer")); + gdbpy_print_stack (); + return -1; + } + + /* Wrap PY_BUFF so that it is cleaned up correctly at the end of this + scope. */ + Py_buffer_up buffer_up (&py_buff); + + /* Validate that the buffer is the correct length. */ + if (py_buff.len != len) + { + PyErr_Format (PyExc_ValueError, + _("Result from read_memory is incorrectly sized buffer")); + gdbpy_print_stack (); + return -1; + } + + /* Copy the data out of the Python buffer and return succsess.*/ + const gdb_byte *buffer = (const gdb_byte *) py_buff.buf; + memcpy (buff, buffer, len); + return 0; +} + +/* Implement DisassemblerResult.length attribute, return the length of the + disassembled instruction. */ + +static PyObject * +disasmpy_result_length (PyObject *self, void *closure) +{ + disasm_result_object *obj = (disasm_result_object *) self; + return gdb_py_object_from_longest (obj->length).release (); +} + +/* Implement DisassemblerResult.string attribute, return the content string + of the disassembled instruction. */ + +static PyObject * +disasmpy_result_string (PyObject *self, void *closure) +{ + disasm_result_object *obj = (disasm_result_object *) self; + + gdb_assert (obj->content != nullptr); + gdb_assert (strlen (obj->content->c_str ()) > 0); + gdb_assert (obj->length > 0); + return PyUnicode_Decode (obj->content->c_str (), + obj->content->size (), + host_charset (), nullptr); +} + +/* Implement DisassemblerResult.__init__. Takes two arguments, an + integer, the length in bytes of the disassembled instruction, and a + string, the disassembled content of the instruction. */ + +static int +disasmpy_result_init (PyObject *self, PyObject *args, PyObject *kwargs) +{ + static const char *keywords[] = { "length", "string", NULL }; + int length; + const char *string; + if (!gdb_PyArg_ParseTupleAndKeywords (args, kwargs, "is", keywords, + &length, &string)) + return -1; + + if (length <= 0) + { + PyErr_SetString (PyExc_ValueError, + _("Length must be greater than 0.")); + return -1; + } + + if (strlen (string) == 0) + { + PyErr_SetString (PyExc_ValueError, + _("String must not be empty.")); + return -1; + } + + disasm_result_object *obj = (disasm_result_object *) self; + if (obj->content == nullptr) + obj->content = new string_file; + obj->length = length; + obj->content->write (string, strlen (string)); + return 0; +} + +/* Implement memory_error_func callback for disassemble_info. Extract the + underlying DisassembleInfo Python object, and set a memory error on + it. */ + +void +gdbpy_disassembler::memory_error_func (int status, bfd_vma memaddr, + struct disassemble_info *info) +{ + gdbpy_disassembler *dis + = static_cast (info->application_data); + dis->m_memory_error_address.emplace (memaddr); +} + +/* Wrapper of print_address. */ + +void +gdbpy_disassembler::print_address_func (bfd_vma addr, + struct disassemble_info *info) +{ + gdbpy_disassembler *dis + = static_cast (info->application_data); + print_address (dis->arch (), addr, (struct ui_file *) info->stream); +} + +/* constructor. */ + +gdbpy_disassembler::gdbpy_disassembler (disasm_info_object *obj, + PyObject *memory_source) + : gdb_disassemble_info (obj->gdbarch, &m_string_file, read_memory_func, + memory_error_func, print_address_func, + fprintf_func), + m_disasm_info_object (obj), + m_memory_source (memory_source) +{ /* Nothing. */ } + +/* A wrapper around a reference to a Python DisassembleInfo object, which + ensures that the object is marked as invalid when we leave the enclosing + scope. + + Each DisassembleInfo is created in gdbpy_print_insn, and is done with by + the time that function returns. However, there's nothing to stop a user + caching a reference to the DisassembleInfo, and thus keeping the object + around. + + We therefore have the notion of a DisassembleInfo becoming invalid, this + happens when gdbpy_print_insn returns. This class is responsible for + marking the DisassembleInfo as invalid in its destructor. */ + +struct scoped_disasm_info_object +{ + /* Constructor. */ + scoped_disasm_info_object (struct gdbarch *gdbarch, CORE_ADDR memaddr, + disassemble_info *info) + : m_disasm_info (allocate_disasm_info_object ()) + { + m_disasm_info->address = memaddr; + m_disasm_info->gdb_info = info; + m_disasm_info->gdbarch = gdbarch; + m_disasm_info->program_space = current_program_space; + } + + /* Upon destruction mark m_diasm_info as invalid. */ + ~scoped_disasm_info_object () + { + m_disasm_info->gdb_info = nullptr; + } + + /* Return a pointer to the underlying disasm_info_object instance. */ + disasm_info_object * + get () const + { + return m_disasm_info.get (); + } + +private: + + /* Wrapper around the call to PyObject_New, this wrapper function can be + called from the constructor initialization list, while PyObject_New, a + macro, can't. */ + static disasm_info_object * + allocate_disasm_info_object () + { + return (disasm_info_object *) PyObject_New (disasm_info_object, + &disasm_info_object_type); + } + + /* A reference to a gdb.disassembler.DisassembleInfo object. When this + containing instance goes out of scope this reference is released, + however, the user might be holding other references to the + DisassembleInfo object in Python code, so the underlying object might + not be deleted. */ + gdbpy_ref m_disasm_info; +}; + +/* See python-internal.h. */ + +gdb::optional +gdbpy_print_insn (struct gdbarch *gdbarch, CORE_ADDR memaddr, + disassemble_info *info) +{ + /* Early exit case. This must be done as early as possible, and + definitely before we enter Python environment. The + python_print_insn_enabled flag is set (from Python) only when the user + has installed one (or more) Python disassemblers. So in the common + case (no custom disassembler installed) this flag will be false, + allowing for a quick return. */ + if (!gdb_python_initialized || !python_print_insn_enabled) + return {}; + + gdbpy_enter enter_py (get_current_arch (), current_language); + + /* The attribute we are going to lookup that provides the print_insn + functionality. */ + static const char *callback_name = "_print_insn"; + + /* Grab a reference to the gdb.disassembler module, and check it has the + attribute that we need. */ + gdbpy_ref<> gdb_python_disassembler_module + (PyImport_ImportModule ("gdb.disassembler")); + if (gdb_python_disassembler_module == nullptr + || !PyObject_HasAttrString (gdb_python_disassembler_module.get (), + callback_name)) + return {}; + + /* Now grab the callback attribute from the module. */ + gdbpy_ref<> hook + (PyObject_GetAttrString (gdb_python_disassembler_module.get (), + callback_name)); + if (hook == nullptr) + { + gdbpy_print_stack (); + return {}; + } + + /* Create the new DisassembleInfo object we will pass into Python. This + object will be marked as invalid when we leave this scope. */ + scoped_disasm_info_object scoped_disasm_info (gdbarch, memaddr, info); + disasm_info_object *disasm_info = scoped_disasm_info.get (); + + /* Call into the registered disassembler to (possibly) perform the + disassembly. */ + PyObject *insn_disas_obj = (PyObject *) disasm_info; + gdbpy_ref<> result (PyObject_CallFunctionObjArgs (hook.get (), + insn_disas_obj, + nullptr)); + + if (result == nullptr) + { + /* The call into Python code resulted in an exception. If this was a + gdb.MemoryError, then we can figure out an address and call the + disassemble_info::memory_error_func to report the error back to + core GDB. Any other exception type we assume means a bug in the + user's code, and print stack. */ + + if (PyErr_ExceptionMatches (gdbpy_gdb_memory_error)) + { + /* A gdb.MemoryError might have an address attribute which + contains the address at which the memory error occurred. If + this is the case then use this address, otherwise, fallback to + just using the address of the instruction we were asked to + disassemble. */ + PyObject *error_type, *error_value, *error_traceback; + CORE_ADDR addr; + + PyErr_Fetch (&error_type, &error_value, &error_traceback); + + if (error_value != nullptr + && PyObject_HasAttrString (error_value, "address")) + { + PyObject *addr_obj = PyObject_GetAttrString (error_value, + "address"); + if (get_addr_from_python (addr_obj, &addr) < 0) + addr = disasm_info->address; + } + else + addr = disasm_info->address; + + PyErr_Clear (); + info->memory_error_func (-1, addr, info); + return gdb::optional (-1); + } + else + { + /* Anything that is not gdb.MemoryError. */ + gdbpy_print_stack (); + return {}; + } + } + else if (result == Py_None) + { + /* A return value of None indicates that the Python code could not, + or doesn't want to, disassemble this instruction. Just return an + empty result and core GDB will try to disassemble this for us. */ + return {}; + } + + /* The call into Python neither raised an exception, or returned None. + Check to see if the result looks valid. */ + gdbpy_ref<> length_obj (PyObject_GetAttrString (result.get (), "length")); + if (length_obj == nullptr) + { + gdbpy_print_stack (); + return {}; + } + + gdbpy_ref<> string_obj (PyObject_GetAttrString (result.get (), "string")); + if (string_obj == nullptr) + { + gdbpy_print_stack (); + return {}; + } + if (!gdbpy_is_string (string_obj.get ())) + { + PyErr_SetString (PyExc_TypeError, _("string attribute is not a string.")); + gdbpy_print_stack (); + return {}; + } + + gdb::unique_xmalloc_ptr string + = gdbpy_obj_to_string (string_obj.get ()); + if (string == nullptr) + { + gdbpy_print_stack (); + return {}; + } + + long length; + if (!gdb_py_int_as_long (length_obj.get (), &length)) + { + gdbpy_print_stack (); + return {}; + } + + long max_insn_length = (gdbarch_max_insn_length_p (gdbarch) ? + gdbarch_max_insn_length (gdbarch) : INT_MAX); + if (length <= 0 || length > max_insn_length) + { + PyErr_SetString (PyExc_ValueError, _("Invalid length attribute.")); + gdbpy_print_stack (); + return {}; + } + + if (strlen (string.get ()) == 0) + { + PyErr_SetString (PyExc_ValueError, _("string attribute must not be empty.")); + gdbpy_print_stack (); + return {}; + } + + /* Print the disassembled instruction back to core GDB, and return the + length of the disassembled instruction. */ + info->fprintf_func (info->stream, "%s", string.get ()); + return gdb::optional (length); +} + +/* The tp_dealloc callback for the DisassemblerResult type. Takes care of + deallocating the content buffer. */ + +static void +disasmpy_dealloc_result (PyObject *self) +{ + disasm_result_object *obj = (disasm_result_object *) self; + delete obj->content; + Py_TYPE (self)->tp_free (self); +} + +/* The get/set attributes of the gdb.disassembler.DisassembleInfo type. */ + +static gdb_PyGetSetDef disasm_info_object_getset[] = { + { "address", disasmpy_info_address, nullptr, + "Start address of the instruction to disassemble.", nullptr }, + { "architecture", disasmpy_info_architecture, nullptr, + "Architecture to disassemble in", nullptr }, + { "progspace", disasmpy_info_progspace, nullptr, + "Program space to disassemble in", nullptr }, + { nullptr } /* Sentinel */ +}; + +/* The methods of the gdb.disassembler.DisassembleInfo type. */ + +static PyMethodDef disasm_info_object_methods[] = { + { "read_memory", (PyCFunction) disasmpy_info_read_memory, + METH_VARARGS | METH_KEYWORDS, + "read_memory (LEN, OFFSET = 0) -> Octets[]\n\ +Read LEN octets for the instruction to disassemble." }, + { "is_valid", disasmpy_info_is_valid, METH_NOARGS, + "is_valid () -> Boolean.\n\ +Return true if this DisassembleInfo is valid, false if not." }, + {nullptr} /* Sentinel */ +}; + +/* The get/set attributes of the gdb.disassembler.DisassemblerResult type. */ + +static gdb_PyGetSetDef disasm_result_object_getset[] = { + { "length", disasmpy_result_length, nullptr, + "Length of the disassembled instruction.", nullptr }, + { "string", disasmpy_result_string, nullptr, + "String representing the disassembled instruction.", nullptr }, + { nullptr } /* Sentinel */ +}; + +/* These are the methods we add into the _gdb.disassembler module, which + are then imported into the gdb.disassembler module. These are global + functions that support performing disassembly. */ + +PyMethodDef python_disassembler_methods[] = +{ + { "builtin_disassemble", (PyCFunction) disasmpy_builtin_disassemble, + METH_VARARGS | METH_KEYWORDS, + "builtin_disassemble (INFO, MEMORY_SOURCE = None) -> None\n\ +Disassemble using GDB's builtin disassembler. INFO is an instance of\n\ +gdb.disassembler.DisassembleInfo. The MEMORY_SOURCE, if not None, should\n\ +be an object with the read_memory method." }, + { "_set_enabled", (PyCFunction) disasmpy_set_enabled, + METH_VARARGS | METH_KEYWORDS, + "_set_enabled (STATE) -> None\n\ +Set whether GDB should call into the Python _print_insn code or not." }, + {nullptr, nullptr, 0, nullptr} +}; + +/* Structure to define the _gdb.disassembler module. */ + +static struct PyModuleDef python_disassembler_module_def = +{ + PyModuleDef_HEAD_INIT, + "_gdb.disassembler", + nullptr, + -1, + python_disassembler_methods, + nullptr, + nullptr, + nullptr, + nullptr +}; + +/* Called to initialize the Python structures in this file. */ + +int +gdbpy_initialize_disasm +(void) +{ + /* Create the _gdb.disassembler module, and add it to the _gdb module. */ + + PyObject *gdb_disassembler_module; + gdb_disassembler_module = PyModule_Create (&python_disassembler_module_def); + if (gdb_disassembler_module == nullptr) + return -1; + PyModule_AddObject(gdb_module, "disassembler", gdb_disassembler_module); + + /* This is needed so that 'import _gdb.disassembler' will work. */ + PyObject *dict = PyImport_GetModuleDict (); + PyDict_SetItemString (dict, "_gdb.disassembler", gdb_disassembler_module); + + /* Having the tp_new field as nullptr means that this class can't be + created from user code. The only way they can be created is from + within GDB, and then they are passed into user code. */ + gdb_assert (disasm_info_object_type.tp_new == nullptr); + if (PyType_Ready (&disasm_info_object_type) < 0) + return -1; + + if (gdb_pymodule_addobject (gdb_disassembler_module, "DisassembleInfo", + (PyObject *) &disasm_info_object_type) < 0) + return -1; + + disasm_result_object_type.tp_new = PyType_GenericNew; + if (PyType_Ready (&disasm_result_object_type) < 0) + return -1; + + if (gdb_pymodule_addobject (gdb_disassembler_module, "DisassemblerResult", + (PyObject *) &disasm_result_object_type) < 0) + return -1; + + return 0; +} + +/* Describe the gdb.disassembler.DisassembleInfo type. */ + +PyTypeObject disasm_info_object_type = { + PyVarObject_HEAD_INIT (nullptr, 0) + "gdb.disassembler.DisassembleInfo", /*tp_name*/ + sizeof (disasm_info_object), /*tp_basicsize*/ + 0, /*tp_itemsize*/ + 0, /*tp_dealloc*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + 0, /*tp_repr*/ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ + 0, /*tp_hash */ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT, /*tp_flags*/ + "GDB instruction disassembler object", /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + disasm_info_object_methods, /* tp_methods */ + 0, /* tp_members */ + disasm_info_object_getset /* tp_getset */ +}; + +/* Describe the gdb.disassembler.DisassemblerResult type. */ + +PyTypeObject disasm_result_object_type = { + PyVarObject_HEAD_INIT (nullptr, 0) + "gdb.disassembler.DisassemblerResult", /*tp_name*/ + sizeof (disasm_result_object), /*tp_basicsize*/ + 0, /*tp_itemsize*/ + disasmpy_dealloc_result, /*tp_dealloc*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + 0, /*tp_repr*/ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ + 0, /*tp_hash */ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/ + "GDB object, representing a disassembler result", /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + 0, /* tp_methods */ + 0, /* tp_members */ + disasm_result_object_getset, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + disasmpy_result_init, /* tp_init */ + 0, /* tp_alloc */ +}; diff --git a/gdb/python/python-internal.h b/gdb/python/python-internal.h index d947b96033b..ed5894c1c3d 100644 --- a/gdb/python/python-internal.h +++ b/gdb/python/python-internal.h @@ -540,6 +540,8 @@ int gdbpy_initialize_connection () int gdbpy_initialize_micommands (void) CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION; void gdbpy_finalize_micommands (); +int gdbpy_initialize_disasm () + CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION; /* A wrapper for PyErr_Fetch that handles reference counting for the caller. */ @@ -822,4 +824,18 @@ extern bool gdbpy_is_architecture (PyObject *obj); extern bool gdbpy_is_progspace (PyObject *obj); +/* Implement the 'print_insn' hook for Python. Disassemble an instruction + whose address is ADDRESS for architecture GDBARCH. The bytes of the + instruction should be read with INFO->read_memory_func as the + instruction being disassembled might actually be in a buffer. + + Used INFO->fprintf_func to print the results of the disassembly, and + return the length of the instruction in octets. + + If no instruction can be disassembled then return an empty value. */ + +extern gdb::optional gdbpy_print_insn (struct gdbarch *gdbarch, + CORE_ADDR address, + disassemble_info *info); + #endif /* PYTHON_PYTHON_INTERNAL_H */ diff --git a/gdb/python/python.c b/gdb/python/python.c index df794dcd63a..84e4d64473b 100644 --- a/gdb/python/python.c +++ b/gdb/python/python.c @@ -167,7 +167,7 @@ static const struct extension_language_ops python_extension_ops = gdbpy_colorize_disasm, - NULL, /* gdbpy_print_insn, */ + gdbpy_print_insn, }; #endif /* HAVE_PYTHON */ @@ -2045,6 +2045,7 @@ do_start_initialization () if (gdbpy_initialize_auto_load () < 0 || gdbpy_initialize_values () < 0 + || gdbpy_initialize_disasm () < 0 || gdbpy_initialize_frames () < 0 || gdbpy_initialize_commands () < 0 || gdbpy_initialize_instruction () < 0 diff --git a/gdb/testsuite/gdb.python/py-disasm.c b/gdb/testsuite/gdb.python/py-disasm.c new file mode 100644 index 00000000000..ee0bb157f4d --- /dev/null +++ b/gdb/testsuite/gdb.python/py-disasm.c @@ -0,0 +1,25 @@ +/* This test program is part of GDB, the GNU debugger. + + Copyright 2021-2022 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 . */ + +int +main () +{ + asm ("nop"); + asm ("nop"); /* Break here. */ + asm ("nop"); + return 0; +} diff --git a/gdb/testsuite/gdb.python/py-disasm.exp b/gdb/testsuite/gdb.python/py-disasm.exp new file mode 100644 index 00000000000..ea7847fc6df --- /dev/null +++ b/gdb/testsuite/gdb.python/py-disasm.exp @@ -0,0 +1,150 @@ +# Copyright (C) 2021-2022 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 . + +# This file is part of the GDB testsuite. It validates the Python +# disassembler API. + +load_lib gdb-python.exp + +standard_testfile + +if { [prepare_for_testing "failed to prepare" ${testfile} ${srcfile} "debug"] } { + return -1 +} + +# Skip all tests if Python scripting is not enabled. +if { [skip_python_tests] } { continue } + +if ![runto_main] then { + fail "can't run to main" + return 0 +} + +set pyfile [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py] + +gdb_test "source ${pyfile}" "Python script imported" \ + "import python scripts" + +gdb_breakpoint [gdb_get_line_number "Break here."] +gdb_continue_to_breakpoint "Break here." + +set curr_pc [get_valueof "/x" "\$pc" "*unknown*"] + +gdb_test_no_output "python current_pc = ${curr_pc}" + +# The current pc will be something like 0x1234 with no leading zeros. +# However, in the disassembler output addresses are padded with zeros. +# This substitution changes 0x1234 to 0x0*1234, which can then be used +# as a regexp in the disassembler output matching. +set curr_pc_pattern [string replace ${curr_pc} 0 1 "0x0*"] + +# Grab the name of the current architecture, this is used in the tests +# patterns below. +set curr_arch [get_python_valueof "gdb.selected_inferior().architecture().name()" "*unknown*"] + +# Helper proc that removes all registered disassemblers. +proc py_remove_all_disassemblers {} { + gdb_test_no_output "python remove_all_python_disassemblers()" +} + +# A list of test plans. Each plan is a list of two elements, the +# first element is the name of a class in py-disasm.py, this is a +# disassembler class. The second element is a pattern that should be +# matched in the disassembler output. +# +# Each different disassembler tests some different feature of the +# Python disassembler API. +set addr_pattern "\r\n=> ${curr_pc_pattern} <\[^>\]+>:\\s+" +set base_pattern "${addr_pattern}nop" +set test_plans \ + [list \ + [list "" "${base_pattern}\r\n.*"] \ + [list "GlobalNullDisassembler" "${base_pattern}\r\n.*"] \ + [list "GlobalPreInfoDisassembler" "${base_pattern}\\s+## ad = $hex, ar = ${curr_arch}\r\n.*"] \ + [list "GlobalPostInfoDisassembler" "${base_pattern}\\s+## ad = $hex, ar = ${curr_arch}\r\n.*"] \ + [list "GlobalReadDisassembler" "${base_pattern}\\s+## bytes =( $hex)+\r\n.*"] \ + [list "GlobalAddrDisassembler" "${base_pattern}\\s+## addr = ${curr_pc_pattern} <\[^>\]+>\r\n.*"] \ + [list "NonMemoryErrorEarlyDisassembler" "${addr_pattern}Python Exception : non-memory error instead of a result\r\nnop\r\n.*"] \ + [list "NonMemoryErrorLateDisassembler" "${addr_pattern}Python Exception : non-memory error after builtin disassembler\r\nnop\r\n.*"] \ + [list "MemoryErrorEarlyDisassembler" "${base_pattern}\\s+## AFTER ERROR\r\n.*"] \ + [list "MemoryErrorLateDisassembler" "${addr_pattern}Cannot access memory at address ${curr_pc_pattern}"] \ + [list "RethrowMemoryErrorDisassembler" "${addr_pattern}Cannot access memory at address $hex"] \ + [list "FaultingMemorySourceDisassembler" "${addr_pattern}Cannot access memory at address ${curr_pc_pattern}"] \ + [list "FailingMemorySourceDisassembler" "${addr_pattern}Python Exception : the memory source failed\r\n\r\nCannot access memory at address ${curr_pc_pattern}"]] + +# Now execute each test plan. +foreach plan $test_plans { + set global_disassembler_name [lindex $plan 0] + set expected_pattern [lindex $plan 1] + + with_test_prefix "global_disassembler=${global_disassembler_name}" { + # Remove all existing disassemblers. + py_remove_all_disassemblers + + # If we have a disassembler to load, do it now. + if { $global_disassembler_name != "" } { + gdb_test_no_output "python add_global_disassembler($global_disassembler_name)" + } + + # Disassemble main, and check the disassembler output. + gdb_test "disassemble main" $expected_pattern + } +} + +# Check that the architecture specific disassemblers can override the +# global disassembler. +# +# First, register a global disassembler, and check it is in place. +with_test_prefix "GLOBAL tagging disassembler" { + py_remove_all_disassemblers + gdb_test_no_output "python gdb.disassembler.register_disassembler(TaggingDisassembler(\"GLOBAL\"), None)" + gdb_test "disassemble main" "${base_pattern}\\s+## tag = GLOBAL\r\n.*" +} + +# Now register an architecture specific disassembler, and check it +# overrides the global disassembler. +with_test_prefix "LOCAL tagging disassembler" { + gdb_test_no_output "python gdb.disassembler.register_disassembler(TaggingDisassembler(\"LOCAL\"), \"${curr_arch}\")" + gdb_test "disassemble main" "${base_pattern}\\s+## tag = LOCAL\r\n.*" +} + +# Now remove the architecture specific disassembler, and check that +# the global disassembler kicks back in. +with_test_prefix "GLOBAL tagging disassembler again" { + gdb_test_no_output "python gdb.disassembler.register_disassembler(None, \"${curr_arch}\")" + gdb_test "disassemble main" "${base_pattern}\\s+## tag = GLOBAL\r\n.*" +} + +# Check that a DisassembleInfo becomes invalid after the call into the +# disassembler. +with_test_prefix "DisassembleInfo becomes invalid" { + py_remove_all_disassemblers + gdb_test_no_output "python add_global_disassembler(GlobalCachingDisassembler)" + gdb_test "disassemble main" "${base_pattern}\\s+## CACHED\r\n.*" + gdb_test "python GlobalCachingDisassembler.check()" "PASS" +} + +# Test the memory source aspect of the builtin disassembler. +with_test_prefix "memory source api" { + py_remove_all_disassemblers + gdb_test_no_output "python gdb.disassembler.register_disassembler(analyzing_disassembler)" + gdb_test "disassemble main" "${base_pattern}\r\n.*" + gdb_test "python analyzing_disassembler.find_replacement_candidate()" \ + "Replace from $hex to $hex with NOP" + gdb_test "disassemble main" "${base_pattern}\r\n.*" \ + "second disassembler pass" + gdb_test "python analyzing_disassembler.check()" \ + "PASS" +} diff --git a/gdb/testsuite/gdb.python/py-disasm.py b/gdb/testsuite/gdb.python/py-disasm.py new file mode 100644 index 00000000000..a05244dbb1b --- /dev/null +++ b/gdb/testsuite/gdb.python/py-disasm.py @@ -0,0 +1,456 @@ +# Copyright (C) 2021-2022 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 . + +import gdb +import gdb.disassembler +import struct +import sys + +from gdb.disassembler import Disassembler, DisassemblerResult + +# A global, holds the program-counter address at which we should +# perform the extra disassembly that this script provides. +current_pc = None + + +def remove_all_python_disassemblers(): + for a in gdb.architecture_names(): + gdb.disassembler.register_disassembler(None, a) + gdb.disassembler.register_disassembler(None, None) + + +class TestDisassembler(Disassembler): + """A base class for disassemblers within this script to inherit from. + Implements the __call__ method and ensures we only do any + disassembly wrapping for the global CURRENT_PC.""" + + def __init__(self): + global current_pc + + super(TestDisassembler, self).__init__("TestDisassembler") + if current_pc == None: + raise gdb.GdbError("no current_pc set") + + def __call__(self, info): + global current_pc + + if info.address != current_pc: + return None + return self.disassemble(info) + + def disassemble(self, info): + raise NotImplementedError("override the disassemble method") + + +class GlobalPreInfoDisassembler(TestDisassembler): + """Check the attributes of DisassembleInfo before disassembly has occurred.""" + + def disassemble(self, info): + ad = info.address + ar = info.architecture + + if ad != current_pc: + raise gdb.GdbError("invalid address") + + if not isinstance(ar, gdb.Architecture): + raise gdb.GdbError("invalid architecture type") + + result = gdb.disassembler.builtin_disassemble(info) + + text = result.string + "\t## ad = 0x%x, ar = %s" % (ad, ar.name()) + return DisassemblerResult(result.length, text) + + +class GlobalPostInfoDisassembler(TestDisassembler): + """Check the attributes of DisassembleInfo after disassembly has occurred.""" + + def disassemble(self, info): + result = gdb.disassembler.builtin_disassemble(info) + + ad = info.address + ar = info.architecture + + if ad != current_pc: + raise gdb.GdbError("invalid address") + + if not isinstance(ar, gdb.Architecture): + raise gdb.GdbError("invalid architecture type") + + text = result.string + "\t## ad = 0x%x, ar = %s" % (ad, ar.name()) + return DisassemblerResult(result.length, text) + + +class GlobalReadDisassembler(TestDisassembler): + """Check the DisassembleInfo.read_memory method. Calls the builtin + disassembler, then reads all of the bytes of this instruction, and + adds them as a comment to the disassembler output.""" + + def disassemble(self, info): + result = gdb.disassembler.builtin_disassemble(info) + len = result.length + str = "" + for o in range(len): + if str != "": + str += " " + v = bytes(info.read_memory(1, o))[0] + if sys.version_info[0] < 3: + v = struct.unpack("= len(rb) or (offset + length) > len(rb): + raise gdb.MemoryError("invalid length and offset combination") + + # Return only the slice of the nop instruction as requested. + s = offset + e = offset + length + return rb[s:e] + + def read_memory(self, len, offset): + """Callback used from the builtin disassembler to read the contents of + memory.""" + + info = self._info + assert info is not None + + # If this request is within the region we are replacing with 'nop' + # instructions, then call the helper function to perform that + # replacement. + if self._start is not None: + assert self._end is not None + if info.address >= self._start and info.address < self._end: + return self._read_replacement(len, offset) + + # Otherwise, we just forward this request to the default read memory + # implementation. + return info.read_memory(len, offset) + + def find_replacement_candidate(self): + """Call this after the first disassembly pass. This identifies a suitable + instruction to replace with 'nop' instruction(s).""" + + if self._nop_index is None: + raise gdb.GdbError("no nop was found") + + nop_idx = self._nop_index + nop_length = self._pass_1_length[nop_idx] + + # First we look for an instruction that is larger than a nop + # instruction, but whose length is an exact multiple of the nop + # instruction's length. + replace_idx = None + for idx in range(len(self._pass_1_length)): + if ( + idx > 0 + and idx != nop_idx + and self._pass_1_insn[idx] != "nop" + and self._pass_1_length[idx] > self._pass_1_length[nop_idx] + and self._pass_1_length[idx] % self._pass_1_length[nop_idx] == 0 + ): + replace_idx = idx + break + + # If we still don't have a replacement candidate, then search again, + # this time looking for an instruciton that is the same length as a + # nop instruction. + if replace_idx is None: + for idx in range(len(self._pass_1_length)): + if ( + idx > 0 + and idx != nop_idx + and self._pass_1_insn[idx] != "nop" + and self._pass_1_length[idx] == self._pass_1_length[nop_idx] + ): + replace_idx = idx + break + + # Weird, the nop instruction must be larger than every other + # instruction, or all instructions are 'nop'? + if replace_idx is None: + raise gdb.GdbError("can't find an instruction to replace") + + # Record the instruction range that will be replaced with 'nop' + # instructions, and mark that we are now on the second pass. + self._start = self._pass_1_address[replace_idx] + self._end = self._pass_1_address[replace_idx] + self._pass_1_length[replace_idx] + self._first_pass = False + print("Replace from 0x%x to 0x%x with NOP" % (self._start, self._end)) + + # Finally, build the expected result. Create the _check list, which + # is a copy of _pass_1_insn, but replace the instruction we + # identified above with a series of 'nop' instructions. + self._check = list(self._pass_1_insn) + nop_count = int(self._pass_1_length[replace_idx] / self._pass_1_length[nop_idx]) + nops = ["nop"] * nop_count + self._check[replace_idx : (replace_idx + 1)] = nops + + def check(self): + """Call this after the second disassembler pass to validate the output.""" + if self._check != self._pass_2_insn: + print("APB, Check : %s" % self._check) + print("APB, Result: %s" % self._pass_2_insn) + raise gdb.GdbError("mismatch") + print("PASS") + + +# Create a global instance of the AnalyzingDisassembler. This isn't +# registered as a disassembler yet though, that is done from the +# py-diasm.exp later. +analyzing_disassembler = AnalyzingDisassembler("AnalyzingDisassembler") + + +def add_global_disassembler(dis_class): + """Create an instance of DIS_CLASS and register it as a global disassembler.""" + dis = dis_class() + gdb.disassembler.register_disassembler(dis, None) + + +# Start with all disassemblers removed. +remove_all_python_disassemblers() + +print("Python script imported") -- 2.25.4