aboutsummaryrefslogtreecommitdiff
path: root/gdb/testsuite/gdb.python
diff options
context:
space:
mode:
Diffstat (limited to 'gdb/testsuite/gdb.python')
-rw-r--r--gdb/testsuite/gdb.python/py-disasm.c25
-rw-r--r--gdb/testsuite/gdb.python/py-disasm.exp209
-rw-r--r--gdb/testsuite/gdb.python/py-disasm.py712
3 files changed, 946 insertions, 0 deletions
diff --git a/gdb/testsuite/gdb.python/py-disasm.c b/gdb/testsuite/gdb.python/py-disasm.c
new file mode 100644
index 0000000..ee0bb15
--- /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 <http://www.gnu.org/licenses/>. */
+
+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 0000000..1b9cd44
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-disasm.exp
@@ -0,0 +1,209 @@
+# 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/>.
+
+# 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 unknown_error_pattern "unknown disassembler error \\(error = -1\\)"
+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 "GdbErrorEarlyDisassembler" "${addr_pattern}GdbError instead of a result\r\n${unknown_error_pattern}"] \
+ [list "RuntimeErrorEarlyDisassembler" "${addr_pattern}Python Exception <class 'RuntimeError'>: RuntimeError instead of a result\r\n\r\n${unknown_error_pattern}"] \
+ [list "GdbErrorLateDisassembler" "${addr_pattern}GdbError after builtin disassembler\r\n${unknown_error_pattern}"] \
+ [list "RuntimeErrorLateDisassembler" "${addr_pattern}Python Exception <class 'RuntimeError'>: RuntimeError after builtin disassembler\r\n\r\n${unknown_error_pattern}"] \
+ [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 "ReadMemoryMemoryErrorDisassembler" "${addr_pattern}Cannot access memory at address ${curr_pc_pattern}"] \
+ [list "ReadMemoryGdbErrorDisassembler" "${addr_pattern}read_memory raised GdbError\r\n${unknown_error_pattern}"] \
+ [list "ReadMemoryRuntimeErrorDisassembler" "${addr_pattern}Python Exception <class 'RuntimeError'>: read_memory raised RuntimeError\r\n\r\n${unknown_error_pattern}"] \
+ [list "ReadMemoryCaughtMemoryErrorDisassembler" "${addr_pattern}nop\r\n.*"] \
+ [list "ReadMemoryCaughtGdbErrorDisassembler" "${addr_pattern}nop\r\n.*"] \
+ [list "ReadMemoryCaughtRuntimeErrorDisassembler" "${addr_pattern}nop\r\n.*"] \
+ [list "MemorySourceNotABufferDisassembler" "${addr_pattern}Python Exception <class 'TypeError'>: Result from read_memory is not a buffer\r\n\r\n${unknown_error_pattern}"] \
+ [list "MemorySourceBufferTooLongDisassembler" "${addr_pattern}Python Exception <class 'ValueError'>: Buffer returned from read_memory is sized $decimal instead of the expected $decimal\r\n\r\n${unknown_error_pattern}"] \
+ [list "ResultOfWrongType" "${addr_pattern}Python Exception <class 'TypeError'>: Result is not a DisassemblerResult.\r\n.*"] \
+ [list "ResultWithInvalidLength" "${addr_pattern}Python Exception <class 'ValueError'>: Invalid length attribute: length must be greater than 0.\r\n.*"] \
+ [list "ResultWithInvalidString" "${addr_pattern}Python Exception <class 'ValueError'>: String attribute must not be empty.\r\n.*"]]
+
+# 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 some errors relating to DisassemblerResult creation.
+with_test_prefix "DisassemblerResult errors" {
+ gdb_test "python gdb.disassembler.DisassemblerResult(0, 'abc')" \
+ [multi_line \
+ "ValueError: Length must be greater than 0." \
+ "Error while executing Python code."]
+ gdb_test "python gdb.disassembler.DisassemblerResult(-1, 'abc')" \
+ [multi_line \
+ "ValueError: Length must be greater than 0." \
+ "Error while executing Python code."]
+ gdb_test "python gdb.disassembler.DisassemblerResult(1, '')" \
+ [multi_line \
+ "ValueError: String must not be empty." \
+ "Error while executing Python code."]
+}
+
+# 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 analyzing_disassembler = add_global_disassembler(AnalyzingDisassembler)"
+ 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"
+}
+
+# Test the 'maint info python-disassemblers command.
+with_test_prefix "maint info python-disassemblers" {
+ py_remove_all_disassemblers
+ gdb_test "maint info python-disassemblers" "No Python disassemblers registered\\." \
+ "list disassemblers, none registered"
+ gdb_test_no_output "python disasm = add_global_disassembler(BuiltinDisassembler)"
+ gdb_test "maint info python-disassemblers" \
+ [multi_line \
+ "Architecture\\s+Disassember Name" \
+ "GLOBAL\\s+BuiltinDisassembler\\s+\\(Matches current architecture\\)"] \
+ "list disassemblers, single global disassembler"
+ gdb_test_no_output "python arch = gdb.selected_inferior().architecture().name()"
+ gdb_test_no_output "python gdb.disassembler.register_disassembler(disasm, arch)"
+ gdb_test "maint info python-disassemblers" \
+ [multi_line \
+ "Architecture\\s+Disassember Name" \
+ "\[^\r\n\]+BuiltinDisassembler\\s+\\(Matches current architecture\\)" \
+ "GLOBAL\\s+BuiltinDisassembler"] \
+ "list disassemblers, multiple disassemblers registered"
+}
+
+# Check the attempt to create a "new" DisassembleInfo object fails.
+with_test_prefix "Bad DisassembleInfo creation" {
+ gdb_test_no_output "python my_info = InvalidDisassembleInfo()"
+ gdb_test "python print(my_info.is_valid())" "True"
+ gdb_test "python gdb.disassembler.builtin_disassemble(my_info)" \
+ [multi_line \
+ "RuntimeError: DisassembleInfo is no longer valid\\." \
+ "Error while executing Python code\\."]
+}
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")