aboutsummaryrefslogtreecommitdiff
path: root/gdb/testsuite/gdb.python/py-disasm.py
diff options
context:
space:
mode:
Diffstat (limited to 'gdb/testsuite/gdb.python/py-disasm.py')
-rw-r--r--gdb/testsuite/gdb.python/py-disasm.py712
1 files changed, 712 insertions, 0 deletions
diff --git a/gdb/testsuite/gdb.python/py-disasm.py b/gdb/testsuite/gdb.python/py-disasm.py
new file mode 100644
index 0000000..ff7ffdb
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-disasm.py
@@ -0,0 +1,712 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+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
+
+
+# Remove all currently registered disassemblers.
+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().__init__("TestDisassembler")
+ self.__info = None
+ 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
+ self.__info = info
+ return self.disassemble(info)
+
+ def get_info(self):
+ return self.__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("<B", v)
+ str += "0x%02x" % v
+ text = result.string + "\t## bytes = %s" % str
+ return DisassemblerResult(result.length, text)
+
+
+class GlobalAddrDisassembler(TestDisassembler):
+ """Check the gdb.format_address method."""
+
+ def disassemble(self, info):
+ result = gdb.disassembler.builtin_disassemble(info)
+ arch = info.architecture
+ addr = info.address
+ program_space = info.progspace
+ str = gdb.format_address(addr, program_space, arch)
+ text = result.string + "\t## addr = %s" % str
+ return DisassemblerResult(result.length, text)
+
+
+class GdbErrorEarlyDisassembler(TestDisassembler):
+ """Raise a GdbError instead of performing any disassembly."""
+
+ def disassemble(self, info):
+ raise gdb.GdbError("GdbError instead of a result")
+
+
+class RuntimeErrorEarlyDisassembler(TestDisassembler):
+ """Raise a RuntimeError instead of performing any disassembly."""
+
+ def disassemble(self, info):
+ raise RuntimeError("RuntimeError instead of a result")
+
+
+class GdbErrorLateDisassembler(TestDisassembler):
+ """Raise a GdbError after calling the builtin disassembler."""
+
+ def disassemble(self, info):
+ result = gdb.disassembler.builtin_disassemble(info)
+ raise gdb.GdbError("GdbError after builtin disassembler")
+
+
+class RuntimeErrorLateDisassembler(TestDisassembler):
+ """Raise a RuntimeError after calling the builtin disassembler."""
+
+ def disassemble(self, info):
+ result = gdb.disassembler.builtin_disassemble(info)
+ raise RuntimeError("RuntimeError after builtin disassembler")
+
+
+class MemoryErrorEarlyDisassembler(TestDisassembler):
+ """Throw a memory error, ignore the error and disassemble."""
+
+ def disassemble(self, info):
+ tag = "## FAIL"
+ try:
+ info.read_memory(1, -info.address + 2)
+ except gdb.MemoryError:
+ tag = "## AFTER ERROR"
+ result = gdb.disassembler.builtin_disassemble(info)
+ text = result.string + "\t" + tag
+ return DisassemblerResult(result.length, text)
+
+
+class MemoryErrorLateDisassembler(TestDisassembler):
+ """Throw a memory error after calling the builtin disassembler, but
+ before we return a result."""
+
+ def disassemble(self, info):
+ result = gdb.disassembler.builtin_disassemble(info)
+ # The following read will throw an error.
+ info.read_memory(1, -info.address + 2)
+ return DisassemblerResult(1, "BAD")
+
+
+class RethrowMemoryErrorDisassembler(TestDisassembler):
+ """Catch and rethrow a memory error."""
+
+ def disassemble(self, info):
+ try:
+ info.read_memory(1, -info.address + 2)
+ except gdb.MemoryError as e:
+ raise gdb.MemoryError("cannot read code at address 0x2")
+ return DisassemblerResult(1, "BAD")
+
+
+class ResultOfWrongType(TestDisassembler):
+ """Return something that is not a DisassemblerResult from disassemble method"""
+
+ class Blah:
+ def __init__(self, length, string):
+ self.length = length
+ self.string = string
+
+ def disassemble(self, info):
+ return self.Blah(1, "ABC")
+
+
+class ResultWrapper(gdb.disassembler.DisassemblerResult):
+ def __init__(self, length, string, length_x=None, string_x=None):
+ super().__init__(length, string)
+ if length_x is None:
+ self.__length = length
+ else:
+ self.__length = length_x
+ if string_x is None:
+ self.__string = string
+ else:
+ self.__string = string_x
+
+ @property
+ def length(self):
+ return self.__length
+
+ @property
+ def string(self):
+ return self.__string
+
+
+class ResultWithInvalidLength(TestDisassembler):
+ """Return a result object with an invalid length."""
+
+ def disassemble(self, info):
+ result = gdb.disassembler.builtin_disassemble(info)
+ return ResultWrapper(result.length, result.string, 0)
+
+
+class ResultWithInvalidString(TestDisassembler):
+ """Return a result object with an empty string."""
+
+ def disassemble(self, info):
+ result = gdb.disassembler.builtin_disassemble(info)
+ return ResultWrapper(result.length, result.string, None, "")
+
+
+class TaggingDisassembler(TestDisassembler):
+ """A simple disassembler that just tags the output."""
+
+ def __init__(self, tag):
+ super().__init__()
+ self._tag = tag
+
+ def disassemble(self, info):
+ result = gdb.disassembler.builtin_disassemble(info)
+ text = result.string + "\t## tag = %s" % self._tag
+ return DisassemblerResult(result.length, text)
+
+
+class GlobalCachingDisassembler(TestDisassembler):
+ """A disassembler that caches the DisassembleInfo that is passed in,
+ as well as a copy of the original DisassembleInfo.
+
+ Once the call into the disassembler is complete then the
+ DisassembleInfo objects become invalid, and any calls into them
+ should trigger an exception."""
+
+ # This is where we cache the DisassembleInfo objects.
+ cached_insn_disas = []
+
+ class MyInfo(gdb.disassembler.DisassembleInfo):
+ def __init__(self, info):
+ super().__init__(info)
+
+ def disassemble(self, info):
+ """Disassemble the instruction, add a CACHED comment to the output,
+ and cache the DisassembleInfo so that it is not garbage collected."""
+ GlobalCachingDisassembler.cached_insn_disas.append(info)
+ GlobalCachingDisassembler.cached_insn_disas.append(self.MyInfo(info))
+ result = gdb.disassembler.builtin_disassemble(info)
+ text = result.string + "\t## CACHED"
+ return DisassemblerResult(result.length, text)
+
+ @staticmethod
+ def check():
+ """Check that all of the methods on the cached DisassembleInfo trigger an
+ exception."""
+ for info in GlobalCachingDisassembler.cached_insn_disas:
+ assert isinstance(info, gdb.disassembler.DisassembleInfo)
+ assert not info.is_valid()
+ try:
+ val = info.address
+ raise gdb.GdbError("DisassembleInfo.address is still valid")
+ except RuntimeError as e:
+ assert str(e) == "DisassembleInfo is no longer valid."
+ except:
+ raise gdb.GdbError(
+ "DisassembleInfo.address raised an unexpected exception"
+ )
+
+ try:
+ val = info.architecture
+ raise gdb.GdbError("DisassembleInfo.architecture is still valid")
+ except RuntimeError as e:
+ assert str(e) == "DisassembleInfo is no longer valid."
+ except:
+ raise gdb.GdbError(
+ "DisassembleInfo.architecture raised an unexpected exception"
+ )
+
+ try:
+ val = info.read_memory(1, 0)
+ raise gdb.GdbError("DisassembleInfo.read is still valid")
+ except RuntimeError as e:
+ assert str(e) == "DisassembleInfo is no longer valid."
+ except:
+ raise gdb.GdbError(
+ "DisassembleInfo.read raised an unexpected exception"
+ )
+
+ print("PASS")
+
+
+class GlobalNullDisassembler(TestDisassembler):
+ """A disassembler that does not change the output at all."""
+
+ def disassemble(self, info):
+ pass
+
+
+class ReadMemoryMemoryErrorDisassembler(TestDisassembler):
+ """Raise a MemoryError exception from the DisassembleInfo.read_memory
+ method."""
+
+ class MyInfo(gdb.disassembler.DisassembleInfo):
+ def __init__(self, info):
+ super().__init__(info)
+
+ def read_memory(self, length, offset):
+ # Throw a memory error with a specific address. We don't
+ # expect this address to show up in the output though.
+ raise gdb.MemoryError(0x1234)
+
+ def disassemble(self, info):
+ info = self.MyInfo(info)
+ return gdb.disassembler.builtin_disassemble(info)
+
+
+class ReadMemoryGdbErrorDisassembler(TestDisassembler):
+ """Raise a GdbError exception from the DisassembleInfo.read_memory
+ method."""
+
+ class MyInfo(gdb.disassembler.DisassembleInfo):
+ def __init__(self, info):
+ super().__init__(info)
+
+ def read_memory(self, length, offset):
+ raise gdb.GdbError("read_memory raised GdbError")
+
+ def disassemble(self, info):
+ info = self.MyInfo(info)
+ return gdb.disassembler.builtin_disassemble(info)
+
+
+class ReadMemoryRuntimeErrorDisassembler(TestDisassembler):
+ """Raise a RuntimeError exception from the DisassembleInfo.read_memory
+ method."""
+
+ class MyInfo(gdb.disassembler.DisassembleInfo):
+ def __init__(self, info):
+ super().__init__(info)
+
+ def read_memory(self, length, offset):
+ raise RuntimeError("read_memory raised RuntimeError")
+
+ def disassemble(self, info):
+ info = self.MyInfo(info)
+ return gdb.disassembler.builtin_disassemble(info)
+
+
+class ReadMemoryCaughtMemoryErrorDisassembler(TestDisassembler):
+ """Raise a MemoryError exception from the DisassembleInfo.read_memory
+ method, catch this in the outer disassembler."""
+
+ class MyInfo(gdb.disassembler.DisassembleInfo):
+ def __init__(self, info):
+ super().__init__(info)
+
+ def read_memory(self, length, offset):
+ raise gdb.MemoryError(0x1234)
+
+ def disassemble(self, info):
+ info = self.MyInfo(info)
+ try:
+ return gdb.disassembler.builtin_disassemble(info)
+ except gdb.MemoryError:
+ return None
+
+
+class ReadMemoryCaughtGdbErrorDisassembler(TestDisassembler):
+ """Raise a GdbError exception from the DisassembleInfo.read_memory
+ method, catch this in the outer disassembler."""
+
+ class MyInfo(gdb.disassembler.DisassembleInfo):
+ def __init__(self, info):
+ super().__init__(info)
+
+ def read_memory(self, length, offset):
+ raise gdb.GdbError("exception message")
+
+ def disassemble(self, info):
+ info = self.MyInfo(info)
+ try:
+ return gdb.disassembler.builtin_disassemble(info)
+ except gdb.GdbError as e:
+ if e.args[0] == "exception message":
+ return None
+ raise e
+
+
+class ReadMemoryCaughtRuntimeErrorDisassembler(TestDisassembler):
+ """Raise a RuntimeError exception from the DisassembleInfo.read_memory
+ method, catch this in the outer disassembler."""
+
+ class MyInfo(gdb.disassembler.DisassembleInfo):
+ def __init__(self, info):
+ super().__init__(info)
+
+ def read_memory(self, length, offset):
+ raise RuntimeError("exception message")
+
+ def disassemble(self, info):
+ info = self.MyInfo(info)
+ try:
+ return gdb.disassembler.builtin_disassemble(info)
+ except RuntimeError as e:
+ if e.args[0] == "exception message":
+ return None
+ raise e
+
+
+class MemorySourceNotABufferDisassembler(TestDisassembler):
+ class MyInfo(gdb.disassembler.DisassembleInfo):
+ def __init__(self, info):
+ super().__init__(info)
+
+ def read_memory(self, length, offset):
+ return 1234
+
+ def disassemble(self, info):
+ info = self.MyInfo(info)
+ return gdb.disassembler.builtin_disassemble(info)
+
+
+class MemorySourceBufferTooLongDisassembler(TestDisassembler):
+ """The read memory returns too many bytes."""
+
+ class MyInfo(gdb.disassembler.DisassembleInfo):
+ def __init__(self, info):
+ super().__init__(info)
+
+ def read_memory(self, length, offset):
+ buffer = super().read_memory(length, offset)
+ # Create a new memory view made by duplicating BUFFER. This
+ # will trigger an error as GDB expects a buffer of exactly
+ # LENGTH to be returned, while this will return a buffer of
+ # 2*LENGTH.
+ return memoryview(
+ bytes([int.from_bytes(x, "little") for x in (list(buffer[0:]) * 2)])
+ )
+
+ def disassemble(self, info):
+ info = self.MyInfo(info)
+ return gdb.disassembler.builtin_disassemble(info)
+
+
+class BuiltinDisassembler(Disassembler):
+ """Just calls the builtin disassembler."""
+
+ def __init__(self):
+ super().__init__("BuiltinDisassembler")
+
+ def __call__(self, info):
+ return gdb.disassembler.builtin_disassemble(info)
+
+
+class AnalyzingDisassembler(Disassembler):
+ class MyInfo(gdb.disassembler.DisassembleInfo):
+ """Wrapper around builtin DisassembleInfo type that overrides the
+ read_memory method."""
+
+ def __init__(self, info, start, end, nop_bytes):
+ """INFO is the DisassembleInfo we are wrapping. START and END are
+ addresses, and NOP_BYTES should be a memoryview object.
+
+ The length (END - START) should be the same as the length
+ of NOP_BYTES.
+
+ Any memory read requests outside the START->END range are
+ serviced normally, but any attempt to read within the
+ START->END range will return content from NOP_BYTES."""
+ super().__init__(info)
+ self._start = start
+ self._end = end
+ self._nop_bytes = nop_bytes
+
+ def _read_replacement(self, length, offset):
+ """Return a slice of the buffer representing the replacement nop
+ instructions."""
+
+ assert self._nop_bytes is not None
+ rb = self._nop_bytes
+
+ # If this request is outside of a nop instruction then we don't know
+ # what to do, so just raise a memory error.
+ if offset >= 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, length, offset=0):
+ """Callback used by the builtin disassembler to read the contents of
+ memory."""
+
+ # 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 self.address >= self._start and self.address < self._end:
+ return self._read_replacement(length, offset)
+
+ # Otherwise, we just forward this request to the default read memory
+ # implementation.
+ return super().read_memory(length, offset)
+
+ def __init__(self):
+ """Constructor."""
+ super().__init__("AnalyzingDisassembler")
+
+ # Details about the instructions found during the first disassembler
+ # pass.
+ self._pass_1_length = []
+ self._pass_1_insn = []
+ self._pass_1_address = []
+
+ # The start and end address for the instruction we will replace with
+ # one or more 'nop' instructions during pass two.
+ self._start = None
+ self._end = None
+
+ # The index in the _pass_1_* lists for where the nop instruction can
+ # be found, also, the buffer of bytes that make up a nop instruction.
+ self._nop_index = None
+ self._nop_bytes = None
+
+ # A flag that indicates if we are in the first or second pass of
+ # this disassembler test.
+ self._first_pass = True
+
+ # The disassembled instructions collected during the second pass.
+ self._pass_2_insn = []
+
+ # A copy of _pass_1_insn that has been modified to include the extra
+ # 'nop' instructions we plan to insert during the second pass. This
+ # is then checked against _pass_2_insn after the second disassembler
+ # pass has completed.
+ self._check = []
+
+ def __call__(self, info):
+ """Called to perform the disassembly."""
+
+ # Override the info object, this provides access to our
+ # read_memory function.
+ info = self.MyInfo(info, self._start, self._end, self._nop_bytes)
+ result = gdb.disassembler.builtin_disassemble(info)
+
+ # Record some informaiton about the first 'nop' instruction we find.
+ if self._nop_index is None and result.string == "nop":
+ self._nop_index = len(self._pass_1_length)
+ # The offset in the following read_memory call defaults to 0.
+ print("APB: Reading nop bytes")
+ self._nop_bytes = info.read_memory(result.length)
+
+ # Record information about each instruction that is disassembled.
+ # This test is performed in two passes, and we need different
+ # information in each pass.
+ if self._first_pass:
+ self._pass_1_length.append(result.length)
+ self._pass_1_insn.append(result.string)
+ self._pass_1_address.append(info.address)
+ else:
+ self._pass_2_insn.append(result.string)
+
+ return result
+
+ 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")
+
+
+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)
+ return dis
+
+
+class InvalidDisassembleInfo(gdb.disassembler.DisassembleInfo):
+ """An attempt to create a DisassembleInfo sub-class without calling
+ the parent class init method.
+
+ Attempts to use instances of this class should throw an error
+ saying that the DisassembleInfo is not valid, despite this class
+ having all of the required attributes.
+
+ The reason why this class will never be valid is that an internal
+ field (within the C++ code) can't be initialized without calling
+ the parent class init method."""
+
+ def __init__(self):
+ assert current_pc is not None
+
+ def is_valid(self):
+ return True
+
+ @property
+ def address(self):
+ global current_pc
+ return current_pc
+
+ @property
+ def architecture(self):
+ return gdb.selected_inferior().architecture()
+
+ @property
+ def progspace(self):
+ return gdb.selected_inferior().progspace
+
+
+# Start with all disassemblers removed.
+remove_all_python_disassemblers()
+
+print("Python script imported")