diff options
Diffstat (limited to 'gdb/testsuite/gdb.python')
-rw-r--r-- | gdb/testsuite/gdb.python/py-disasm.c | 25 | ||||
-rw-r--r-- | gdb/testsuite/gdb.python/py-disasm.exp | 209 | ||||
-rw-r--r-- | gdb/testsuite/gdb.python/py-disasm.py | 712 |
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") |