* This is equivalent to the CLI command: {@code break [LOC]}, or {@code watch [LOC]}, etc.
*
+ *
* Breakpoints in GDB can get pretty complicated. Depending on the location specification, the
* actual location of the breakpoint may change during the lifetime of an inferior. Take note of
* the breakpoint number to track those changes across breakpoint modification events.
diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/py/LICENSE b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/LICENSE
new file mode 100644
index 0000000000..c026b6b79a
--- /dev/null
+++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/LICENSE
@@ -0,0 +1,11 @@
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/py/README.md b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/README.md
new file mode 100644
index 0000000000..dece67a1e0
--- /dev/null
+++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/README.md
@@ -0,0 +1,3 @@
+# Ghidra Trace RMI
+
+Package for connecting GDB to Ghidra via Trace RMI.
\ No newline at end of file
diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/py/ghidragdb/__init__.py b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/ghidragdb/__init__.py
new file mode 100644
index 0000000000..4ec478ffc0
--- /dev/null
+++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/ghidragdb/__init__.py
@@ -0,0 +1,16 @@
+## ###
+# IP: GHIDRA
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+from . import util, commands, parameters
diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/py/ghidragdb/arch.py b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/ghidragdb/arch.py
new file mode 100644
index 0000000000..473639b076
--- /dev/null
+++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/ghidragdb/arch.py
@@ -0,0 +1,287 @@
+## ###
+# IP: GHIDRA
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+from ghidratrace.client import Address, RegVal
+
+import gdb
+
+# NOTE: This map is derived from the ldefs using a script
+language_map = {
+ 'aarch64': ['AARCH64:BE:64:v8A', 'AARCH64:LE:64:AppleSilicon', 'AARCH64:LE:64:v8A'],
+ 'aarch64:ilp32': ['AARCH64:BE:32:ilp32', 'AARCH64:LE:32:ilp32', 'AARCH64:LE:64:AppleSilicon'],
+ 'arm_any': ['ARM:BE:32:v8', 'ARM:BE:32:v8T', 'ARM:LE:32:v8', 'ARM:LE:32:v8T'],
+ 'armv2': ['ARM:BE:32:v4', 'ARM:LE:32:v4'],
+ 'armv2a': ['ARM:BE:32:v4', 'ARM:LE:32:v4'],
+ 'armv3': ['ARM:BE:32:v4', 'ARM:LE:32:v4'],
+ 'armv3m': ['ARM:BE:32:v4', 'ARM:LE:32:v4'],
+ 'armv4': ['ARM:BE:32:v4', 'ARM:LE:32:v4'],
+ 'armv4t': ['ARM:BE:32:v4t', 'ARM:LE:32:v4t'],
+ 'armv5': ['ARM:BE:32:v5', 'ARM:LE:32:v5'],
+ 'armv5t': ['ARM:BE:32:v5t', 'ARM:LE:32:v5t'],
+ 'armv5tej': ['ARM:BE:32:v5t', 'ARM:LE:32:v5t'],
+ 'armv6': ['ARM:BE:32:v6', 'ARM:LE:32:v6'],
+ 'armv6-m': ['ARM:BE:32:Cortex', 'ARM:LE:32:Cortex'],
+ 'armv6k': ['ARM:BE:32:Cortex', 'ARM:LE:32:Cortex'],
+ 'armv6kz': ['ARM:BE:32:Cortex', 'ARM:LE:32:Cortex'],
+ 'armv6s-m': ['ARM:BE:32:Cortex', 'ARM:LE:32:Cortex'],
+ 'armv7': ['ARM:BE:32:v7', 'ARM:LE:32:v7'],
+ 'armv7e-m': ['ARM:LE:32:Cortex'],
+ 'armv8-a': ['ARM:BE:32:v8', 'ARM:LE:32:v8'],
+ 'armv8-m.base': ['ARM:BE:32:v8', 'ARM:LE:32:v8'],
+ 'armv8-m.main': ['ARM:BE:32:v8', 'ARM:LE:32:v8'],
+ 'armv8-r': ['ARM:BE:32:v8', 'ARM:LE:32:v8'],
+ 'armv8.1-m.main': ['ARM:BE:32:v8', 'ARM:LE:32:v8'],
+ 'avr:107': ['avr8:LE:24:xmega'],
+ 'avr:31': ['avr8:LE:16:default'],
+ 'avr:51': ['avr8:LE:16:atmega256'],
+ 'avr:6': ['avr8:LE:16:atmega256'],
+ 'hppa2.0w': ['pa-risc:BE:32:default'],
+ 'i386:intel': ['x86:LE:32:default'],
+ 'i386:x86-64': ['x86:LE:64:default'],
+ 'i386:x86-64:intel': ['x86:LE:64:default'],
+ 'i8086': ['x86:LE:16:Protected Mode', 'x86:LE:16:Real Mode'],
+ 'iwmmxt': ['ARM:BE:32:v7', 'ARM:BE:32:v8', 'ARM:BE:32:v8T', 'ARM:LE:32:v7', 'ARM:LE:32:v8', 'ARM:LE:32:v8T'],
+ 'm68hc12': ['HC-12:BE:16:default'],
+ 'm68k': ['68000:BE:32:default'],
+ 'm68k:68020': ['68000:BE:32:MC68020'],
+ 'm68k:68030': ['68000:BE:32:MC68030'],
+ 'm9s12x': ['HCS-12:BE:24:default', 'HCS-12X:BE:24:default'],
+ 'mips:4000': ['MIPS:BE:32:default', 'MIPS:LE:32:default'],
+ 'mips:5000': ['MIPS:BE:64:64-32addr', 'MIPS:BE:64:default', 'MIPS:LE:64:64-32addr', 'MIPS:LE:64:default'],
+ 'mips:micromips': ['MIPS:BE:32:micro'],
+ 'msp:430X': ['TI_MSP430:LE:16:default'],
+ 'powerpc:403': ['PowerPC:BE:32:4xx', 'PowerPC:LE:32:4xx'],
+ 'powerpc:MPC8XX': ['PowerPC:BE:32:MPC8270', 'PowerPC:BE:32:QUICC', 'PowerPC:LE:32:QUICC'],
+ 'powerpc:common': ['PowerPC:BE:32:default', 'PowerPC:LE:32:default'],
+ 'powerpc:common64': ['PowerPC:BE:64:64-32addr', 'PowerPC:BE:64:default', 'PowerPC:LE:64:64-32addr', 'PowerPC:LE:64:default'],
+ 'powerpc:e500': ['PowerPC:BE:32:e500', 'PowerPC:LE:32:e500'],
+ 'powerpc:e500mc': ['PowerPC:BE:64:A2ALT', 'PowerPC:LE:64:A2ALT'],
+ 'powerpc:e500mc64': ['PowerPC:BE:64:A2-32addr', 'PowerPC:BE:64:A2ALT-32addr', 'PowerPC:LE:64:A2-32addr', 'PowerPC:LE:64:A2ALT-32addr'],
+ 'riscv:rv32': ['RISCV:LE:32:RV32G', 'RISCV:LE:32:RV32GC', 'RISCV:LE:32:RV32I', 'RISCV:LE:32:RV32IC', 'RISCV:LE:32:RV32IMC', 'RISCV:LE:32:default'],
+ 'riscv:rv64': ['RISCV:LE:64:RV64G', 'RISCV:LE:64:RV64GC', 'RISCV:LE:64:RV64I', 'RISCV:LE:64:RV64IC', 'RISCV:LE:64:default'],
+ 'sh4': ['SuperH4:BE:32:default', 'SuperH4:LE:32:default'],
+ 'sparc:v9b': ['sparc:BE:32:default', 'sparc:BE:64:default'],
+ 'xscale': ['ARM:BE:32:v6', 'ARM:LE:32:v6'],
+ 'z80': ['z80:LE:16:default', 'z8401x:LE:16:default']
+}
+
+data64_compiler_map = {
+ None: 'pointer64',
+}
+
+x86_compiler_map = {
+ 'GNU/Linux': 'gcc',
+ 'Windows': 'Visual Studio',
+ # This may seem wrong, but Ghidra cspecs really describe the ABI
+ 'Cygwin': 'Visual Studio',
+}
+
+compiler_map = {
+ 'DATA:BE:64:default': data64_compiler_map,
+ 'DATA:LE:64:default': data64_compiler_map,
+ 'x86:LE:32:default': x86_compiler_map,
+ 'x86:LE:64:default': x86_compiler_map,
+}
+
+
+def get_arch():
+ return gdb.selected_inferior().architecture().name()
+
+
+def get_endian():
+ parm = gdb.parameter('endian')
+ if parm != 'auto':
+ return parm
+ # Once again, we have to hack using the human-readable 'show'
+ show = gdb.execute('show endian', to_string=True)
+ if 'little' in show:
+ return 'little'
+ if 'big' in show:
+ return 'big'
+ return 'unrecognized'
+
+
+def get_osabi():
+ parm = gdb.parameter('osabi')
+ if not parm in ['auto', 'default']:
+ return parm
+ # We have to hack around the fact the GDB won't give us the current OS ABI
+ # via the API if it is "auto" or "default". Using "show", we can get it, but
+ # we have to parse output meant for a human. The current value will be on
+ # the top line, delimited by double quotes. It will be the last delimited
+ # thing on that line. ("auto" may appear earlier on the line.)
+ show = gdb.execute('show osabi', to_string=True)
+ line = show.split('\n')[0]
+ return line.split('"')[-2]
+
+
+def compute_ghidra_language():
+ # First, check if the parameter is set
+ lang = gdb.parameter('ghidra-language')
+ if lang != 'auto':
+ return lang
+
+ # Get the list of possible languages for the arch. We'll need to sift
+ # through them by endian and probably prefer default/simpler variants. The
+ # heuristic for "simpler" will be 'default' then shortest variant id.
+ arch = get_arch()
+ endian = get_endian()
+ lebe = ':BE:' if endian == 'big' else ':LE:'
+ if not arch in language_map:
+ return 'DATA' + lebe + '64:default'
+ langs = language_map[arch]
+ matched_endian = sorted(
+ (l for l in langs if lebe in l),
+ key=lambda l: 0 if l.endswith(':default') else len(l)
+ )
+ if len(matched_endian) > 0:
+ return matched_endian[0]
+ # NOTE: I'm disinclined to fall back to a language match with wrong endian.
+ return 'DATA' + lebe + '64:default'
+
+
+def compute_ghidra_compiler(lang):
+ # First, check if the parameter is set
+ comp = gdb.parameter('ghidra-compiler')
+ if comp != 'auto':
+ return comp
+
+ # Check if the selected lang has specific compiler recommendations
+ if not lang in compiler_map:
+ return 'default'
+ comp_map = compiler_map[lang]
+ osabi = get_osabi()
+ if osabi in comp_map:
+ return comp_map[osabi]
+ if None in comp_map:
+ return comp_map[None]
+ return 'default'
+
+
+def compute_ghidra_lcsp():
+ lang = compute_ghidra_language()
+ comp = compute_ghidra_compiler(lang)
+ return lang, comp
+
+
+class DefaultMemoryMapper(object):
+
+ def __init__(self, defaultSpace):
+ self.defaultSpace = defaultSpace
+
+ def map(self, inf: gdb.Inferior, offset: int):
+ if inf.num == 1:
+ space = self.defaultSpace
+ else:
+ space = f'{self.defaultSpace}{inf.num}'
+ return self.defaultSpace, Address(space, offset)
+
+ def map_back(self, inf: gdb.Inferior, address: Address) -> int:
+ if address.space == self.defaultSpace and inf.num == 1:
+ return address.offset
+ if address.space == f'{self.defaultSpace}{inf.num}':
+ return address.offset
+ raise ValueError(f"Address {address} is not in inferior {inf.num}")
+
+
+DEFAULT_MEMORY_MAPPER = DefaultMemoryMapper('ram')
+
+memory_mappers = {}
+
+
+def compute_memory_mapper(lang):
+ if not lang in memory_mappers:
+ return DEFAULT_MEMORY_MAPPER
+ return memory_mappers[lang]
+
+
+class DefaultRegisterMapper(object):
+
+ def __init__(self, byte_order):
+ if not byte_order in ['big', 'little']:
+ raise ValueError("Invalid byte_order: {}".format(byte_order))
+ self.byte_order = byte_order
+ self.union_winners = {}
+
+ def map_name(self, inf, name):
+ return name
+
+ def convert_value(self, value, type=None):
+ if type is None:
+ type = value.dynamic_type.strip_typedefs()
+ l = type.sizeof
+ # l - 1 because array() takes the max index, inclusive
+ # NOTE: Might like to pre-lookup 'unsigned char', but it depends on the
+ # architecture *at the time of lookup*.
+ cv = value.cast(gdb.lookup_type('unsigned char').array(l - 1))
+ rng = range(l)
+ if self.byte_order == 'little':
+ rng = reversed(rng)
+ return bytes(cv[i] for i in rng)
+
+ def map_value(self, inf, name, value):
+ try:
+ av = self.convert_value(value)
+ except gdb.error as e:
+ raise gdb.GdbError("Cannot convert {}'s value: '{}', type: '{}'"
+ .format(name, value, value.type))
+ return RegVal(self.map_name(inf, name), av)
+
+ def map_name_back(self, inf, name):
+ return name
+
+ def map_value_back(self, inf, name, value):
+ return RegVal(self.map_name_back(inf, name), value)
+
+
+class Intel_x86_64_RegisterMapper(DefaultRegisterMapper):
+
+ def __init__(self):
+ super().__init__('little')
+
+ def map_name(self, inf, name):
+ if name == 'eflags':
+ return 'rflags'
+ if name.startswith('zmm'):
+ # Ghidra only goes up to ymm, right now
+ return 'ymm' + name[3:]
+ return super().map_name(inf, name)
+
+ def map_value(self, inf, name, value):
+ rv = super().map_value(inf, name, value)
+ if rv.name.startswith('ymm') and len(rv.value) > 32:
+ return RegVal(rv.name, rv.value[-32:])
+ return rv
+
+ def map_name_back(self, inf, name):
+ if name == 'rflags':
+ return 'eflags'
+
+
+DEFAULT_BE_REGISTER_MAPPER = DefaultRegisterMapper('big')
+DEFAULT_LE_REGISTER_MAPPER = DefaultRegisterMapper('little')
+
+register_mappers = {
+ 'x86:LE:64:default': Intel_x86_64_RegisterMapper()
+}
+
+
+def compute_register_mapper(lang):
+ if not lang in register_mappers:
+ if ':BE:' in lang:
+ return DEFAULT_BE_REGISTER_MAPPER
+ if ':LE:' in lang:
+ return DEFAULT_LE_REGISTER_MAPPER
+ return register_mappers[lang]
diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/py/ghidragdb/commands.py b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/ghidragdb/commands.py
new file mode 100644
index 0000000000..3eca459ef1
--- /dev/null
+++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/ghidragdb/commands.py
@@ -0,0 +1,1456 @@
+## ###
+# IP: GHIDRA
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+from contextlib import contextmanager
+import inspect
+import os.path
+import socket
+import time
+
+from ghidratrace import sch
+from ghidratrace.client import Client, Address, AddressRange, TraceObject
+import psutil
+
+import gdb
+
+from . import arch, hooks, methods, util
+
+
+PAGE_SIZE = 4096
+
+AVAILABLES_PATH = 'Available'
+AVAILABLE_KEY_PATTERN = '[{pid}]'
+AVAILABLE_PATTERN = AVAILABLES_PATH + AVAILABLE_KEY_PATTERN
+BREAKPOINTS_PATH = 'Breakpoints'
+BREAKPOINT_KEY_PATTERN = '[{breaknum}]'
+BREAKPOINT_PATTERN = BREAKPOINTS_PATH + BREAKPOINT_KEY_PATTERN
+BREAK_LOC_KEY_PATTERN = '[{locnum}]'
+INFERIORS_PATH = 'Inferiors'
+INFERIOR_KEY_PATTERN = '[{infnum}]'
+INFERIOR_PATTERN = INFERIORS_PATH + INFERIOR_KEY_PATTERN
+INF_BREAKS_PATTERN = INFERIOR_PATTERN + '.Breakpoints'
+INF_BREAK_KEY_PATTERN = '[{breaknum}.{locnum}]'
+ENV_PATTERN = INFERIOR_PATTERN + '.Environment'
+THREADS_PATTERN = INFERIOR_PATTERN + '.Threads'
+THREAD_KEY_PATTERN = '[{tnum}]'
+THREAD_PATTERN = THREADS_PATTERN + THREAD_KEY_PATTERN
+STACK_PATTERN = THREAD_PATTERN + '.Stack'
+FRAME_KEY_PATTERN = '[{level}]'
+FRAME_PATTERN = STACK_PATTERN + FRAME_KEY_PATTERN
+REGS_PATTERN = FRAME_PATTERN + '.Registers'
+MEMORY_PATTERN = INFERIOR_PATTERN + '.Memory'
+REGION_KEY_PATTERN = '[{start:08x}]'
+REGION_PATTERN = MEMORY_PATTERN + REGION_KEY_PATTERN
+MODULES_PATTERN = INFERIOR_PATTERN + '.Modules'
+MODULE_KEY_PATTERN = '[{modpath}]'
+MODULE_PATTERN = MODULES_PATTERN + MODULE_KEY_PATTERN
+SECTIONS_ADD_PATTERN = '.Sections'
+SECTION_KEY_PATTERN = '[{secname}]'
+SECTION_ADD_PATTERN = SECTIONS_ADD_PATTERN + SECTION_KEY_PATTERN
+
+
+# TODO: Symbols
+
+
+class State(object):
+
+ def __init__(self):
+ self.reset_client()
+
+ def require_client(self):
+ if self.client is None:
+ raise gdb.GdbError("Not connected")
+ return self.client
+
+ def require_no_client(self):
+ if self.client is not None:
+ raise gdb.GdbError("Already connected")
+
+ def reset_client(self):
+ self.client = None
+ self.reset_trace()
+
+ def require_trace(self):
+ if self.trace is None:
+ raise gdb.GdbError("No trace active")
+ return self.trace
+
+ def require_no_trace(self):
+ if self.trace is not None:
+ raise gdb.GdbError("Trace already started")
+
+ def reset_trace(self):
+ self.trace = None
+ gdb.set_convenience_variable('_ghidra_tracing', False)
+ self.reset_tx()
+
+ def require_tx(self):
+ if self.tx is None:
+ raise gdb.GdbError("No transaction")
+ return self.tx
+
+ def require_no_tx(self):
+ if self.tx is not None:
+ raise gdb.GdbError("Transaction already started")
+
+ def reset_tx(self):
+ self.tx = None
+
+
+STATE = State()
+
+
+def install(cmd):
+ cmd()
+
+
+@install
+class GhidraPrefix(gdb.Command):
+ """Commands for connecting to Ghidra"""
+
+ def __init__(self):
+ super().__init__('ghidra', gdb.COMMAND_SUPPORT, prefix=True)
+
+
+@install
+class GhidraTracePrefix(gdb.Command):
+ """Commands for exporting data to a Ghidra trace"""
+
+ def __init__(self):
+ super().__init__('ghidra trace', gdb.COMMAND_DATA, prefix=True)
+
+
+@install
+class GhidraUtilPrefix(gdb.Command):
+ """Utility commands for testing with Ghidra"""
+
+ def __init__(self):
+ super().__init__('ghidra util', gdb.COMMAND_NONE, prefix=True)
+
+
+def cmd(cli_name, mi_name, cli_class, cli_repeat):
+
+ def _cmd(func):
+
+ class _CLICmd(gdb.Command):
+
+ def __init__(self):
+ super().__init__(cli_name, cli_class)
+
+ def invoke(self, argument, from_tty):
+ if not cli_repeat:
+ self.dont_repeat()
+ argv = gdb.string_to_argv(argument)
+ try:
+ func(*argv, is_mi=False, from_tty=from_tty)
+ except TypeError as e:
+ # TODO: This is a bit of a hack, but it works nicely
+ raise gdb.GdbError(
+ e.args[0].replace(func.__name__ + "()", "'" + cli_name + "'"))
+
+ _CLICmd.__doc__ = func.__doc__
+ _CLICmd()
+
+ class _MICmd(gdb.MICommand):
+
+ def __init__(self):
+ super().__init__(mi_name)
+
+ def invoke(self, argv):
+ try:
+ return func(*argv, is_mi=True)
+ except TypeError as e:
+ raise gdb.GdbError(e.args[0].replace(func.__name__ + "()",
+ mi_name))
+
+ _MICmd.__doc__ = func.__doc__
+ _MICmd()
+ return func
+
+ return _cmd
+
+
+@cmd('ghidra trace connect', '-ghidra-trace-connect', gdb.COMMAND_SUPPORT,
+ False)
+def ghidra_trace_connect(address, *, is_mi, **kwargs):
+ """
+ Connect GDB to Ghidra for tracing
+
+ Address must be of the form 'host:port'
+ """
+
+ STATE.require_no_client()
+ parts = address.split(':')
+ if len(parts) != 2:
+ raise gdb.GdbError("address must be in the form 'host:port'")
+ host, port = parts
+ try:
+ c = socket.socket()
+ c.connect((host, int(port)))
+ STATE.client = Client(c, methods.REGISTRY)
+ except ValueError:
+ raise gdb.GdbError("port must be numeric")
+
+
+@cmd('ghidra trace listen', '-ghidra-trace-listen', gdb.COMMAND_SUPPORT, False)
+def ghidra_trace_listen(address=None, *, is_mi, **kwargs):
+ """
+ Listen for Ghidra to connect for tracing
+
+ Takes an optional address for the host and port on which to listen. Either
+ the form 'host:port' or just 'port'. If omitted, it will bind to an
+ ephemeral port on all interfaces. If only the port is given, it will bind to
+ that port on all interfaces. This command will block until the connection is
+ established.
+ """
+
+ STATE.require_no_client()
+ if address is not None:
+ parts = address.split(':')
+ if len(parts) == 1:
+ host, port = '0.0.0.0', parts[0]
+ elif len(parts) == 2:
+ host, port = parts
+ else:
+ raise gdb.GdbError("address must be 'port' or 'host:port'")
+ else:
+ host, port = '0.0.0.0', 0
+ try:
+ s = socket.socket()
+ s.bind((host, int(port)))
+ host, port = s.getsockname()
+ s.listen(1)
+ gdb.write("Listening at {}:{}...\n".format(host, port))
+ c, (chost, cport) = s.accept()
+ s.close()
+ gdb.write("Connection from {}:{}\n".format(chost, cport))
+ STATE.client = Client(c, methods.REGISTRY)
+ except ValueError:
+ raise gdb.GdbError("port must be numeric")
+
+
+@cmd('ghidra trace disconnect', '-ghidra-trace-disconnect', gdb.COMMAND_SUPPORT,
+ False)
+def ghidra_trace_disconnect(*, is_mi, **kwargs):
+ """Disconnect GDB from Ghidra for tracing"""
+
+ STATE.require_client().close()
+ STATE.reset_client()
+
+
+def compute_name():
+ progname = gdb.selected_inferior().progspace.filename
+ if progname is None:
+ return 'gdb/noname'
+ else:
+ return 'gdb/' + progname.split('/')[-1]
+
+
+def start_trace(name):
+ language, compiler = arch.compute_ghidra_lcsp()
+ STATE.trace = STATE.client.create_trace(name, language, compiler)
+ # TODO: Is adding an attribute like this recommended in Python?
+ STATE.trace.memory_mapper = arch.compute_memory_mapper(language)
+ STATE.trace.register_mapper = arch.compute_register_mapper(language)
+
+ parent = os.path.dirname(inspect.getfile(inspect.currentframe()))
+ schema_fn = os.path.join(parent, 'schema.xml')
+ with open(schema_fn, 'r') as schema_file:
+ schema_xml = schema_file.read()
+ with STATE.trace.open_tx("Create Root Object"):
+ root = STATE.trace.create_root_object(schema_xml, 'Session')
+ root.set_value('_display', 'GNU gdb ' + util.GDB_VERSION.full)
+ gdb.set_convenience_variable('_ghidra_tracing', True)
+
+
+@cmd('ghidra trace start', '-ghidra-trace-start', gdb.COMMAND_DATA, False)
+def ghidra_trace_start(name=None, *, is_mi, **kwargs):
+ """Start a Trace in Ghidra"""
+
+ STATE.require_client()
+ if name is None:
+ name = compute_name()
+ STATE.require_no_trace()
+ start_trace(name)
+
+
+@cmd('ghidra trace stop', '-ghidra-trace-stop', gdb.COMMAND_DATA, False)
+def ghidra_trace_stop(*, is_mi, **kwargs):
+ """Stop the Trace in Ghidra"""
+
+ STATE.require_trace().close()
+ STATE.reset_trace()
+
+
+@cmd('ghidra trace restart', '-ghidra-trace-restart', gdb.COMMAND_DATA, False)
+def ghidra_trace_restart(name=None, *, is_mi, **kwargs):
+ """Restart or start the Trace in Ghidra"""
+
+ STATE.require_client()
+ if STATE.trace is not None:
+ STATE.trace.close()
+ STATE.reset_trace()
+ if name is None:
+ name = compute_name()
+ start_trace(name)
+
+
+@cmd('ghidra trace info', '-ghidra-trace-info', gdb.COMMAND_STATUS, True)
+def ghidra_trace_info(*, is_mi, **kwargs):
+ """Get info about the Ghidra connection"""
+
+ result = {}
+ if STATE.client is None:
+ if not is_mi:
+ gdb.write("Not connected to Ghidra\n")
+ return
+ host, port = STATE.client.s.getpeername()
+ if is_mi:
+ result['connection'] = "{}:{}".format(host, port)
+ else:
+ gdb.write("Connected to Ghidra at {}:{}\n".format(host, port))
+ if STATE.trace is None:
+ if is_mi:
+ result['tracing'] = False
+ else:
+ gdb.write("No trace\n")
+ return
+ if is_mi:
+ result['tracing'] = True
+ else:
+ gdb.write("Trace active\n")
+ return result
+
+
+@cmd('ghidra trace lcsp', '-ghidra-trace-lcsp', gdb.COMMAND_STATUS, True)
+def ghidra_trace_info_lcsp(*, is_mi, **kwargs):
+ """
+ Get the selected Ghidra language-compiler-spec pair. Even when
+ 'show ghidra language' is 'auto' and/or 'show ghidra compiler' is 'auto',
+ this command provides the current actual language and compiler spec.
+ """
+
+ language, compiler = arch.compute_ghidra_lcsp()
+ if is_mi:
+ return {'language': language, 'compiler': compiler}
+ else:
+ gdb.write("Selected Ghidra language: {}\n".format(language))
+ gdb.write("Selected Ghidra compiler: {}\n".format(compiler))
+
+
+@cmd('ghidra trace tx-start', '-ghidra-trace-tx-start', gdb.COMMAND_DATA, False)
+def ghidra_trace_txstart(description, *, is_mi, **kwargs):
+ """
+ Start a transaction on the trace
+ """
+
+ STATE.require_no_tx()
+ STATE.tx = STATE.require_trace().start_tx(description, undoable=False)
+
+
+@cmd('ghidra trace tx-commit', '-ghidra-trace-tx-commit', gdb.COMMAND_DATA,
+ False)
+def ghidra_trace_txcommit(*, is_mi, **kwargs):
+ """
+ Commit the current transaction
+ """
+
+ STATE.require_tx().commit()
+ STATE.reset_tx()
+
+
+@cmd('ghidra trace tx-abort', '-ghidra-trace-tx-abort', gdb.COMMAND_DATA, False)
+def ghidra_trace_txabort(*, is_mi, **kwargs):
+ """
+ Abort the current transaction
+
+ Use only in emergencies.
+ """
+
+ tx = STATE.require_tx()
+ gdb.write("Aborting trace transaction!\n")
+ tx.abort()
+ STATE.reset_tx()
+
+
+@contextmanager
+def open_tracked_tx(description):
+ with STATE.require_trace().open_tx(description) as tx:
+ STATE.tx = tx
+ yield tx
+ STATE.reset_tx()
+
+
+@cmd('ghidra trace tx-open', '-ghidra-trace-tx-open', gdb.COMMAND_DATA, False)
+def ghidra_trace_tx(description, command, *, is_mi, **kwargs):
+ """
+ Run a command with an open transaction
+
+ If possible, use this in the following idiom to ensure your transactions
+ are closed:
+
+ define my-cmd
+ ghidra trace put...
+ ghidra trace put...
+ end
+ ghidra trace tx-open "My tx" "my-cmd"
+
+ If you instead do:
+
+ ghidra trace tx-start "My tx"
+ ghidra trace put...
+ ghidra trace put...
+ ghidra trace tx-commit
+
+ and something goes wrong with one of the puts, the transaction may never be
+ closed, leading to further crashes when trying to start a new transaction.
+ """
+
+ with open_tracked_tx(description):
+ gdb.execute(command)
+
+
+@cmd('ghidra trace save', '-ghidra-trace-save', gdb.COMMAND_DATA, False)
+def ghidra_trace_save(*, is_mi, **kwargs):
+ """
+ Save the current trace
+ """
+
+ STATE.require_trace().save()
+
+
+@cmd('ghidra trace new-snap', '-ghidra-trace-new-snap', gdb.COMMAND_DATA, False)
+def ghidra_trace_new_snap(description, *, is_mi, **kwargs):
+ """
+ Create a new snapshot
+
+ Subsequent modifications to machine state will affect the new snapshot.
+ """
+
+ STATE.require_tx()
+ return {'snap': STATE.require_trace().snapshot(description)}
+
+# TODO: A convenience var for the current snapshot
+# Will need to update it on:
+# ghidra trace snapshot/set-snap
+# inferior ? (only if per-inferior tracing.... I don't think I'm doing that.)
+# ghidra trace trace start/stop/restart
+
+
+@cmd('ghidra trace set-snap', '-ghidra-trace-set-snap', gdb.COMMAND_DATA, False)
+def ghidra_trace_set_snap(snap, *, is_mi, **kwargs):
+ """
+ Go to a snapshot
+
+ Subsequent modifications to machine state will affect the given snapshot.
+ """
+
+ STATE.require_trace().set_snap(int(gdb.parse_and_eval(snap)))
+
+
+def put_bytes(start, end, pages, is_mi, from_tty):
+ trace = STATE.require_trace()
+ if pages:
+ start = start // PAGE_SIZE * PAGE_SIZE
+ end = (end + PAGE_SIZE - 1) // PAGE_SIZE * PAGE_SIZE
+ inf = gdb.selected_inferior()
+ buf = bytes(inf.read_memory(start, end - start))
+
+ base, addr = trace.memory_mapper.map(inf, start)
+ if base != addr.space:
+ trace.create_overlay_space(base, addr.space)
+
+ count = trace.put_bytes(addr, buf)
+ if from_tty and not is_mi:
+ gdb.write("Wrote {} bytes\n".format(count))
+ return {'count': count}
+
+
+def eval_address(address):
+ try:
+ return int(gdb.parse_and_eval(address))
+ except gdb.error as e:
+ raise gdb.GdbError("Cannot convert '{}' to address".format(address))
+
+
+def eval_range(address, length):
+ start = eval_address(address)
+ try:
+ end = start + int(gdb.parse_and_eval(length))
+ except gdb.error as e:
+ raise gdb.GdbError("Cannot convert '{}' to length".format(length))
+ return start, end
+
+
+def putmem(address, length, pages=True, is_mi=False, from_tty=True):
+ start, end = eval_range(address, length)
+ return put_bytes(start, end, pages, is_mi, from_tty)
+
+
+@cmd('ghidra trace putmem', '-ghidra-trace-putmem', gdb.COMMAND_DATA, True)
+def ghidra_trace_putmem(address, length, pages=True, *, is_mi, from_tty=True, **kwargs):
+ """
+ Record the given block of memory into the Ghidra trace.
+ """
+
+ STATE.require_tx()
+ return putmem(address, length, pages, is_mi, from_tty)
+
+
+@cmd('ghidra trace putval', '-ghidra-trace-putval', gdb.COMMAND_DATA, True)
+def ghidra_trace_putval(value, pages=True, *, is_mi, from_tty=True, **kwargs):
+ """
+ Record the given value into the Ghidra trace, if it's in memory.
+ """
+
+ STATE.require_tx()
+ val = gdb.parse_and_eval(value)
+ try:
+ start = int(val.address)
+ except gdb.error as e:
+ raise gdb.GdbError("Value '{}' has no address".format(value))
+ end = start + int(val.dynamic_type.sizeof)
+ return put_bytes(start, end, pages, is_mi, from_tty)
+
+
+@cmd('ghidra trace putmem-state', '-ghidra-trace-putmem-state', gdb.COMMAND_DATA, True)
+def ghidra_trace_putmem_state(address, length, state, *, is_mi, **kwargs):
+ """
+ Set the state of the given range of memory in the Ghidra trace.
+ """
+
+ STATE.require_tx()
+ STATE.trace.validate_state(state)
+ start, end = eval_range(address, length)
+ inf = gdb.selected_inferior()
+ base, addr = STATE.trace.memory_mapper.map(inf, start)
+ if base != addr.space:
+ trace.create_overlay_space(base, addr.space)
+ STATE.trace.set_memory_state(addr.extend(end - start), state)
+
+
+@cmd('ghidra trace delmem', '-ghidra-trace-delmem', gdb.COMMAND_DATA, True)
+def ghidra_trace_delmem(address, length, *, is_mi, **kwargs):
+ """
+ Delete the given range of memory from the Ghidra trace.
+
+ Why would you do this? Keep in mind putmem quantizes to full pages by
+ default, usually to take advantage of spatial locality. This command does
+ not quantize. You must do that yourself, if necessary.
+ """
+
+ STATE.require_tx()
+ start, end = eval_range(address, length)
+ inf = gdb.selected_inferior()
+ base, addr = STATE.trace.memory_mapper.map(inf, start)
+ # Do not create the space. We're deleting stuff.
+ STATE.trace.delete_bytes(addr.extend(end - start))
+
+
+def putreg(frame, reg_descs):
+ inf = gdb.selected_inferior()
+ space = REGS_PATTERN.format(infnum=inf.num, tnum=gdb.selected_thread().num,
+ level=frame.level())
+ STATE.trace.create_overlay_space('register', space)
+ robj = STATE.trace.create_object(space)
+ robj.insert()
+ mapper = STATE.trace.register_mapper
+ values = []
+ for desc in reg_descs:
+ v = frame.read_register(desc)
+ values.append(mapper.map_value(inf, desc.name, v))
+ # TODO: Memorize registers that failed for this arch, and omit later.
+ return {'missing': STATE.trace.put_registers(space, values)}
+
+
+@cmd('ghidra trace putreg', '-ghidra-trace-putreg', gdb.COMMAND_DATA, True)
+def ghidra_trace_putreg(group='all', *, is_mi, **kwargs):
+ """
+ Record the given register group for the current frame into the Ghidra trace.
+
+ If no group is specified, 'all' is assumed.
+ """
+
+ STATE.require_tx()
+ frame = gdb.selected_frame()
+ return putreg(frame, frame.architecture().registers(group))
+
+
+@cmd('ghidra trace delreg', '-ghidra-trace-delreg', gdb.COMMAND_DATA, True)
+def ghidra_trace_delreg(group='all', *, is_mi, **kwargs):
+ """
+ Delete the given register group for the curent frame from the Ghidra trace.
+
+ Why would you do this? If no group is specified, 'all' is assumed.
+ """
+
+ STATE.require_tx()
+ inf = gdb.selected_inferior()
+ frame = gdb.selected_frame()
+ space = 'Inferiors[{}].Threads[{}].Stack[{}].Registers'.format(
+ inf.num, gdb.selected_thread().num, frame.level()
+ )
+ mapper = STATE.trace.register_mapper
+ names = []
+ for desc in frame.architecture().registers(group):
+ names.append(mapper.map_name(inf, desc.name))
+ return STATE.trace.delete_registers(space, names)
+
+
+@cmd('ghidra trace create-obj', '-ghidra-trace-create-obj', gdb.COMMAND_DATA,
+ False)
+def ghidra_trace_create_obj(path, *, is_mi, from_tty=True, **kwargs):
+ """
+ Create an object in the Ghidra trace.
+
+ The new object is in a detached state, so it may not be immediately
+ recognized by the Debugger GUI. Use 'ghidra trace insert-obj' to finish the
+ object, after all its required attributes are set.
+ """
+
+ STATE.require_tx()
+ obj = STATE.trace.create_object(path)
+ if from_tty and not is_mi:
+ gdb.write("Created object: id={}, path='{}'\n".format(obj.id, obj.path))
+ return {'id': obj.id, 'path': obj.path}
+
+
+@cmd('ghidra trace insert-obj', '-ghidra-trace-insert-obj', gdb.COMMAND_DATA,
+ True)
+def ghidra_trace_insert_obj(path, *, is_mi, from_tty=True, **kwargs):
+ """
+ Insert an object into the Ghidra trace.
+ """
+
+ # NOTE: id parameter is probably not necessary, since this command is for
+ # humans.
+ STATE.require_tx()
+ span = STATE.trace.proxy_object_path(path).insert()
+ if from_tty and not is_mi:
+ gdb.write("Inserted object: lifespan={}\n".format(span))
+ return {'lifespan': span}
+
+
+@cmd('ghidra trace remove-obj', '-ghidra-trace-remove-obj', gdb.COMMAND_DATA,
+ True)
+def ghidra_trace_remove_obj(path, *, is_mi, from_tty=True, **kwargs):
+ """
+ Remove an object from the Ghidra trace.
+
+ This does not delete the object. It just removes it from the tree for the
+ current snap and onwards.
+ """
+
+ # NOTE: id parameter is probably not necessary, since this command is for
+ # humans.
+ STATE.require_tx()
+ STATE.trace.proxy_object_path(path).remove()
+
+
+def to_bytes(value, type):
+ min, max = type.range()
+ return bytes(int(value[i]) for i in range(min, max + 1))
+
+
+def to_string(value, type, encoding, full):
+ if full:
+ min, max = type.range()
+ return value.string(encoding=encoding, length=max - min + 1)
+ else:
+ return value.string(encoding=encoding)
+
+
+def to_bool_list(value, type):
+ min, max = type.range()
+ return [bool(value[i]) for i in range(min, max + 1)]
+
+
+def to_int_list(value, type):
+ min, max = type.range()
+ return [int(value[i]) for i in range(min, max + 1)]
+
+
+def eval_value(value, schema=None):
+ try:
+ val = gdb.parse_and_eval(value)
+ except gdb.error as e:
+ raise gdb.error(f"Could not evaluate '{value}': {e}")
+ type = val.dynamic_type.strip_typedefs()
+ if type.code == gdb.TYPE_CODE_VOID:
+ return None, sch.VOID
+ elif type.code == gdb.TYPE_CODE_BOOL:
+ return bool(val), sch.BOOL
+ elif type.code == gdb.TYPE_CODE_INT:
+ if schema is not None:
+ return int(val), schema
+ # These sizes are defined by the Trace database, i.e., Java types
+ elif type.sizeof == 1:
+ return int(val), sch.BYTE
+ elif type.sizeof == 2:
+ return int(val), sch.SHORT
+ elif type.sizeof == 4:
+ return int(val), sch.INT
+ elif type.sizeof == 8:
+ return int(val), sch.LONG
+ elif type.code == gdb.TYPE_CODE_CHAR:
+ return chr(val), sch.CHAR
+ elif type.code == gdb.TYPE_CODE_ARRAY:
+ etype = type.target().strip_typedefs()
+ if etype.code == gdb.TYPE_CODE_BOOL:
+ return to_bool_list(val, type), sch.BOOL_ARR
+ elif etype.code == gdb.TYPE_CODE_INT:
+ if etype.sizeof == 1:
+ if schema == sch.BYTE_ARR:
+ return to_bytes(val, type), schema
+ elif schema == sch.CHAR_ARR:
+ return to_string(val, type, 'utf-8', full=True), schema
+ return to_string(val, type, 'utf-8', full=False), sch.STRING
+ elif etype.sizeof == 2:
+ if schema is None:
+ if etype.name == 'wchar_t':
+ return to_string(val, type, 'utf-16', full=False), sch.STRING
+ schema = sch.SHORT_ARR
+ elif schema == sch.CHAR_ARR:
+ return to_string(val, type, 'utf-16', full=True), schema
+ return to_int_list(val, type), schema
+ elif etype.sizeof == 4:
+ if schema is None:
+ if etype.name == 'wchar_t':
+ return to_string(val, type, 'utf-32', full=False), sch.STRING
+ schema = sch.INT_ARR
+ elif schema == sch.CHAR_ARR:
+ return to_string(val, type, 'utf-32', full=True), schema
+ return to_int_list(val, type), schema
+ elif schema is not None:
+ return to_int_list(val, type), schema
+ elif etype.sizeof == 8:
+ return to_int_list(val, type), sch.LONG_ARR
+ elif etype.code == gdb.TYPE_CODE_STRING:
+ return val.to_string_list(val), sch.STRING_ARR
+ # TODO: Array of C strings?
+ elif type.code == gdb.TYPE_CODE_STRING:
+ return val.string(), sch.STRING
+ elif type.code == gdb.TYPE_CODE_PTR:
+ offset = int(val)
+ inf = gdb.selected_inferior()
+ base, addr = STATE.trace.memory_mapper.map(inf, offset)
+ return (base, addr), sch.ADDRESS
+ raise ValueError(
+ "Cannot convert ({}): '{}', value='{}'".format(schema, value, val))
+
+
+@cmd('ghidra trace set-value', '-ghidra-trace-set-value', gdb.COMMAND_DATA, True)
+def ghidra_trace_set_value(path, key, value, schema=None, *, is_mi, **kwargs):
+ """
+ Set a value (attribute or element) in the Ghidra trace's object tree.
+
+ A void value implies removal. NOTE: The type of an expression may be
+ subject to GDB's current language. e.g., there is no 'bool' in C. You may
+ have to change to C++ if you need this type. Alternatively, you can use the
+ Python API.
+ """
+
+ # NOTE: id parameter is probably not necessary, since this command is for
+ # humans.
+ # TODO: path and key are two separate parameters.... This is mostly to
+ # spare me from porting path parsing to Python, but it may also be useful
+ # if we ever allow ids here, since the id would be for the object, not the
+ # complete value path.
+ schema = None if schema is None else sch.Schema(schema)
+ STATE.require_tx()
+ if schema == sch.OBJECT:
+ val = STATE.trace.proxy_object_path(value)
+ else:
+ val, schema = eval_value(value, schema)
+ if schema == sch.ADDRESS:
+ base, addr = val
+ val = addr
+ if base != addr.space:
+ trace.create_overlay_space(base, addr.space)
+ STATE.trace.proxy_object_path(path).set_value(key, val, schema)
+
+
+@cmd('ghidra trace retain-values', '-ghidra-trace-retain-values',
+ gdb.COMMAND_DATA, True)
+def ghidra_trace_retain_values(path, *keys, is_mi, **kwargs):
+ """
+ Retain only those keys listed, settings all others to null.
+
+ Takes a list of keys to retain. The first argument may optionally be one of
+ the following:
+
+ --elements To set all other elements to null (default)
+ --attributes To set all other attributes to null
+ --both To set all other values (elements and attributes) to null
+
+ If, for some reason, one of the keys to retain would be mistaken for this
+ switch, then the switch is required. Only the first argument is taken as the
+ switch. All others are taken as keys.
+ """
+
+ STATE.require_tx()
+ kinds = 'elements'
+ if keys[0] == '--elements':
+ kinds = 'elements'
+ keys = keys[1:]
+ elif keys[0] == '--attributes':
+ kinds = 'attributes'
+ keys = keys[1:]
+ elif keys[0] == '--both':
+ kinds = 'both'
+ keys = keys[1:]
+ elif keys[0].startswith('--'):
+ raise gdb.GdbError("Invalid argument: " + keys[0])
+ STATE.trace.proxy_object_path(path).retain_values(keys, kinds=kinds)
+
+
+@cmd('ghidra trace get-obj', '-ghidra-trace-get-obj', gdb.COMMAND_DATA, True)
+def ghidra_trace_get_obj(path, *, is_mi, **kwargs):
+ """
+ Get an object descriptor by its canonical path.
+
+ This isn't the most informative, but it will at least confirm whether an
+ object exists and provide its id.
+ """
+
+ trace = STATE.require_trace()
+ object = trace.get_object(path)
+ if not is_mi:
+ gdb.write("{}\t{}\n".format(object.id, object.path))
+ return object
+
+
+class TableColumn(object):
+ def __init__(self, head):
+ self.head = head
+ self.contents = [head]
+ self.is_last = False
+
+ def add_data(self, data):
+ self.contents.append(str(data))
+
+ def finish(self):
+ self.width = max(len(d) for d in self.contents) + 1
+
+ def print_cell(self, i):
+ gdb.write(
+ self.contents[i] if self.is_last else self.contents[i].ljust(self.width))
+
+
+class Tabular(object):
+ def __init__(self, heads):
+ self.columns = [TableColumn(h) for h in heads]
+ self.columns[-1].is_last = True
+ self.num_rows = 1
+
+ def add_row(self, datas):
+ for c, d in zip(self.columns, datas):
+ c.add_data(d)
+ self.num_rows += 1
+
+ def print_table(self):
+ for c in self.columns:
+ c.finish()
+ for rn in range(self.num_rows):
+ for c in self.columns:
+ c.print_cell(rn)
+ gdb.write('\n')
+
+
+def val_repr(value):
+ if isinstance(value, TraceObject):
+ return value.path
+ elif isinstance(value, Address):
+ return '{}:{:08x}'.format(value.space, value.offset)
+ return repr(value)
+
+
+def print_values(values):
+ table = Tabular(['Parent', 'Key', 'Span', 'Value', 'Type'])
+ for v in values:
+ table.add_row(
+ [v.parent.path, v.key, v.span, val_repr(v.value), v.schema])
+ table.print_table()
+
+
+@cmd('ghidra trace get-values', '-ghidra-trace-get-values', gdb.COMMAND_DATA, True)
+def ghidra_trace_get_values(pattern, *, is_mi, **kwargs):
+ """
+ List all values matching a given path pattern.
+ """
+
+ trace = STATE.require_trace()
+ values = trace.get_values(pattern)
+ if not is_mi:
+ print_values(values)
+ return values
+
+
+@cmd('ghidra trace get-values-rng', '-ghidra-trace-get-values-rng',
+ gdb.COMMAND_DATA, True)
+def ghidra_trace_get_values_rng(address, length, *, is_mi, **kwargs):
+ """
+ List all values intersecting a given address range.
+ """
+
+ trace = STATE.require_trace()
+ start, end = eval_range(address, length)
+ inf = gdb.selected_inferior()
+ base, addr = trace.memory_mapper.map(inf, start)
+ # Do not create the space. We're querying. No tx.
+ values = trace.get_values_intersecting(addr.extend(end - start))
+ if not is_mi:
+ print_values(values)
+ return values
+
+
+def activate(path=None):
+ trace = STATE.require_trace()
+ if path is None:
+ inf = gdb.selected_inferior()
+ t = gdb.selected_thread()
+ if t is None:
+ path = INFERIOR_PATTERN.format(infnum=inf.num)
+ else:
+ frame = gdb.selected_frame()
+ path = FRAME_PATTERN.format(
+ infnum=inf.num, tnum=t.num, level=frame.level())
+ trace.proxy_object_path(path).activate()
+
+
+@cmd('ghidra trace activate', '-ghidra-trace-activate', gdb.COMMAND_STATUS,
+ True)
+def ghidra_trace_activate(path=None, *, is_mi, **kwargs):
+ """
+ Activate an object in Ghidra's GUI.
+
+ This has no effect if the current trace is not current in Ghidra. If path is
+ omitted, this will activate the current frame.
+ """
+
+ activate(path)
+
+
+@cmd('ghidra trace disassemble', '-ghidra-trace-disassemble', gdb.COMMAND_DATA,
+ True)
+def ghidra_trace_disassemble(address, *, is_mi, from_tty=True, **kwargs):
+ """
+ Disassemble starting at the given seed.
+
+ Disassembly proceeds linearly and terminates at the first branch or unknown
+ memory encountered.
+ """
+
+ STATE.require_tx()
+ start = eval_address(address)
+ inf = gdb.selected_inferior()
+ base, addr = STATE.trace.memory_mapper.map(inf, start)
+ if base != addr.space:
+ trace.create_overlay_space(base, addr.space)
+
+ length = STATE.trace.disassemble(addr)
+ if from_tty and not is_mi:
+ gdb.write("Disassembled {} bytes\n".format(length))
+ return {'length': length}
+
+
+def compute_inf_state(inf):
+ threads = inf.threads()
+ if not threads:
+ # TODO: Distinguish INACTIVE from TERMINATED
+ return 'INACTIVE'
+ for t in threads:
+ if t.is_running():
+ return 'RUNNING'
+ return 'STOPPED'
+
+
+def put_inferiors():
+ # TODO: Attributes like _exit_code, _state?
+ # _state would be derived from threads
+ keys = []
+ for inf in gdb.inferiors():
+ ipath = INFERIOR_PATTERN.format(infnum=inf.num)
+ keys.append(INFERIOR_KEY_PATTERN.format(infnum=inf.num))
+ infobj = STATE.trace.create_object(ipath)
+ istate = compute_inf_state(inf)
+ infobj.set_value('_state', istate)
+ infobj.insert()
+ STATE.trace.proxy_object_path(INFERIORS_PATH).retain_values(keys)
+
+
+@cmd('ghidra trace put-inferiors', '-ghidra-trace-put-inferiors',
+ gdb.COMMAND_DATA, True)
+def ghidra_trace_put_inferiors(*, is_mi, **kwargs):
+ """
+ Put the list of inferiors into the trace's Inferiors list.
+ """
+
+ STATE.require_tx()
+ with STATE.client.batch() as b:
+ put_inferiors()
+
+
+def put_available():
+ # TODO: Compared to -list-thread-groups --available:
+ # Is that always from the host, or can that pslist a remote target?
+ # psutil will always be from the host.
+ keys = []
+ for proc in psutil.process_iter():
+ ppath = AVAILABLE_PATTERN.format(pid=proc.pid)
+ procobj = STATE.trace.create_object(ppath)
+ keys.append(AVAILABLE_KEY_PATTERN.format(pid=proc.pid))
+ procobj.set_value('_pid', proc.pid)
+ procobj.set_value('_display', '{} {}'.format(proc.pid, proc.name))
+ procobj.insert()
+ STATE.trace.proxy_object_path(AVAILABLES_PATH).retain_values(keys)
+
+
+@cmd('ghidra trace put-available', '-ghidra-trace-put-available',
+ gdb.COMMAND_DATA, True)
+def ghidra_trace_put_available(*, is_mi, **kwargs):
+ """
+ Put the list of available processes into the trace's Available list.
+ """
+
+ STATE.require_tx()
+ with STATE.client.batch() as b:
+ put_available()
+
+
+def put_single_breakpoint(b, ibobj, inf, ikeys):
+ mapper = STATE.trace.memory_mapper
+ bpath = BREAKPOINT_PATTERN.format(breaknum=b.number)
+ brkobj = STATE.trace.create_object(bpath)
+ if b.type == gdb.BP_BREAKPOINT:
+ brkobj.set_value('_expression', b.location)
+ brkobj.set_value('_kinds', 'SW_EXECUTE')
+ elif b.type == gdb.BP_HARDWARE_BREAKPOINT:
+ brkobj.set_value('_expression', b.location)
+ brkobj.set_value('_kinds', 'HW_EXECUTE')
+ elif b.type == gdb.BP_WATCHPOINT:
+ brkobj.set_value('_expression', b.expression)
+ brkobj.set_value('_kinds', 'WRITE')
+ elif b.type == gdb.BP_HARDWARE_WATCHPOINT:
+ brkobj.set_value('_expression', b.expression)
+ brkobj.set_value('_kinds', 'WRITE')
+ elif b.type == gdb.BP_READ_WATCHPOINT:
+ brkobj.set_value('_expression', b.expression)
+ brkobj.set_value('_kinds', 'READ')
+ elif b.type == gdb.BP_ACCESS_WATCHPOINT:
+ brkobj.set_value('_expression', b.expression)
+ brkobj.set_value('_kinds', 'READ,WRITE')
+ else:
+ brkobj.set_value('_expression', '(unknown)')
+ brkobj.set_value('_kinds', '')
+ brkobj.set_value('Commands', b.commands)
+ brkobj.set_value('Condition', b.condition)
+ brkobj.set_value('Hit Count', b.hit_count)
+ brkobj.set_value('Ignore Count', b.ignore_count)
+ brkobj.set_value('Pending', b.pending)
+ brkobj.set_value('Silent', b.silent)
+ brkobj.set_value('Temporary', b.temporary)
+ # TODO: "_threads"?
+ keys = []
+ locs = util.BREAKPOINT_LOCATION_INFO_READER.get_locations(b)
+ hooks.BRK_STATE.update_brkloc_count(b, len(locs))
+ for i, l in enumerate(locs):
+ # Retain the key, even if not for this inferior
+ k = BREAK_LOC_KEY_PATTERN.format(locnum=i+1)
+ keys.append(k)
+ if inf.num not in l.thread_groups:
+ continue
+ locobj = STATE.trace.create_object(bpath + k)
+ ik = INF_BREAK_KEY_PATTERN.format(breaknum=b.number, locnum=i+1)
+ ikeys.append(ik)
+ if b.location is not None: # Implies execution break
+ base, addr = mapper.map(inf, l.address)
+ if base != addr.space:
+ STATE.trace.create_overlay_space(base, addr.space)
+ locobj.set_value('_range', addr.extend(1))
+ elif b.expression is not None: # Implies watchpoint
+ expr = b.expression
+ if expr.startswith('-location '):
+ expr = expr[len('-location '):]
+ try:
+ address = int(gdb.parse_and_eval('&({})'.format(expr)))
+ base, addr = mapper.map(inf, address)
+ if base != addr.space:
+ STATE.trace.create_overlay_space(base, addr.space)
+ size = int(gdb.parse_and_eval(
+ 'sizeof({})'.format(expr)))
+ locobj.set_value('_range', addr.extend(size))
+ except Exception as e:
+ gdb.write("Error: Could not get range for breakpoint {}: {}\n".format(
+ ik, e), stream=gdb.STDERR)
+ else: # I guess it's a catchpoint
+ pass
+ locobj.insert()
+ ibobj.set_value(ik, locobj)
+ brkobj.retain_values(keys)
+ brkobj.insert()
+
+
+def put_breakpoints():
+ inf = gdb.selected_inferior()
+ ibpath = INF_BREAKS_PATTERN.format(infnum=inf.num)
+ ibobj = STATE.trace.create_object(ibpath)
+ keys = []
+ ikeys = []
+ for b in gdb.breakpoints():
+ keys.append(BREAKPOINT_KEY_PATTERN.format(breaknum=b.number))
+ put_single_breakpoint(b, ibobj, inf, ikeys)
+ ibobj.insert()
+ STATE.trace.proxy_object_path(BREAKPOINTS_PATH).retain_values(keys)
+ ibobj.retain_values(ikeys)
+
+
+@cmd('ghidra trace put-breakpoints', '-ghidra-trace-put-breakpoints',
+ gdb.COMMAND_DATA, True)
+def ghidra_trace_put_breakpoints(*, is_mi, **kwargs):
+ """
+ Put the current inferior's breakpoints into the trace.
+ """
+
+ STATE.require_tx()
+ with STATE.client.batch() as b:
+ put_breakpoints()
+
+
+def put_environment():
+ inf = gdb.selected_inferior()
+ epath = ENV_PATTERN.format(infnum=inf.num)
+ envobj = STATE.trace.create_object(epath)
+ envobj.set_value('_debugger', 'gdb')
+ envobj.set_value('_arch', arch.get_arch())
+ envobj.set_value('_os', arch.get_osabi())
+ envobj.set_value('_endian', arch.get_endian())
+
+
+@cmd('ghidra trace put-environment', '-ghidra-trace-put-environment',
+ gdb.COMMAND_DATA, True)
+def ghidra_trace_put_environment(*, is_mi, **kwargs):
+ """
+ Put some environment indicators into the Ghidra trace.
+ """
+
+ STATE.require_tx()
+ with STATE.client.batch() as b:
+ put_environment()
+
+
+def put_regions():
+ inf = gdb.selected_inferior()
+ try:
+ regions = util.REGION_INFO_READER.get_regions()
+ except Exception:
+ regions = []
+ if len(regions) == 0 and gdb.selected_thread() is not None:
+ regions = [util.REGION_INFO_READER.full_mem()]
+ mapper = STATE.trace.memory_mapper
+ keys = []
+ for r in regions:
+ rpath = REGION_PATTERN.format(infnum=inf.num, start=r.start)
+ keys.append(REGION_KEY_PATTERN.format(start=r.start))
+ regobj = STATE.trace.create_object(rpath)
+ start_base, start_addr = mapper.map(inf, r.start)
+ if start_base != start_addr.space:
+ STATE.trace.create_overlay_space(start_base, start_addr.space)
+ regobj.set_value('_range', start_addr.extend(r.end - r.start))
+ regobj.set_value('_readable', r.perms == None or 'r' in r.perms)
+ regobj.set_value('_writable', r.perms == None or 'w' in r.perms)
+ regobj.set_value('_executable', r.perms == None or 'x' in r.perms)
+ regobj.set_value('_offset', r.offset)
+ regobj.set_value('_objfile', r.objfile)
+ regobj.insert()
+ STATE.trace.proxy_object_path(
+ MEMORY_PATTERN.format(infnum=inf.num)).retain_values(keys)
+
+
+@cmd('ghidra trace put-regions', '-ghidra-trace-put-regions', gdb.COMMAND_DATA,
+ True)
+def ghidra_trace_put_regions(*, is_mi, **kwargs):
+ """
+ Read the memory map, if applicable, and write to the trace's Regions.
+ """
+
+ STATE.require_tx()
+ with STATE.client.batch() as b:
+ put_regions()
+
+
+def put_modules():
+ inf = gdb.selected_inferior()
+ modules = util.MODULE_INFO_READER.get_modules()
+ mapper = STATE.trace.memory_mapper
+ mod_keys = []
+ for mk, m in modules.items():
+ mpath = MODULE_PATTERN.format(infnum=inf.num, modpath=mk)
+ modobj = STATE.trace.create_object(mpath)
+ mod_keys.append(MODULE_KEY_PATTERN.format(modpath=mk))
+ modobj.set_value('_module_name', m.name)
+ base_base, base_addr = mapper.map(inf, m.base)
+ if base_base != base_addr.space:
+ STATE.trace.create_overlay_space(base_base, base_addr.space)
+ modobj.set_value('_range', base_addr.extend(m.max - m.base))
+ sec_keys = []
+ for sk, s in m.sections.items():
+ spath = mpath + SECTION_ADD_PATTERN.format(secname=sk)
+ secobj = STATE.trace.create_object(spath)
+ sec_keys.append(SECTION_KEY_PATTERN.format(secname=sk))
+ start_base, start_addr = mapper.map(inf, s.start)
+ if start_base != start_addr.space:
+ STATE.trace.create_overlay_space(
+ start_base, start_addr.space)
+ secobj.set_value('_range', start_addr.extend(s.end - s.start))
+ secobj.set_value('_offset', s.offset)
+ secobj.set_value('_attrs', s.attrs, schema=sch.STRING_ARR)
+ secobj.insert()
+ # In case there are no sections, we must still insert the module
+ modobj.insert()
+ STATE.trace.proxy_object_path(
+ mpath + SECTIONS_ADD_PATTERN).retain_values(sec_keys)
+ STATE.trace.proxy_object_path(MODULES_PATTERN.format(
+ infnum=inf.num)).retain_values(mod_keys)
+
+
+@cmd('ghidra trace put-modules', '-ghidra-trace-put-modules', gdb.COMMAND_DATA,
+ True)
+def ghidra_trace_put_modules(*, is_mi, **kwargs):
+ """
+ Gather object files, if applicable, and write to the trace's Modules.
+ """
+
+ STATE.require_tx()
+ with STATE.client.batch() as b:
+ put_modules()
+
+
+def convert_state(t):
+ if t.is_exited():
+ return 'TERMINATED'
+ if t.is_running():
+ return 'RUNNING'
+ if t.is_stopped():
+ return 'STOPPED'
+ return 'INACTIVE'
+
+
+def convert_tid(t):
+ if t[1] == 0:
+ return t[2]
+ return t[1]
+
+
+@contextmanager
+def restore_frame():
+ f = gdb.selected_frame()
+ yield
+ f.select()
+
+
+def newest_frame(f):
+ while f.newer() is not None:
+ f = f.newer()
+ return f
+
+
+def compute_thread_display(t):
+ out = gdb.execute('info thread {}'.format(t.num), to_string=True)
+ line = out.strip().split('\n')[-1].strip().replace('\\s+', ' ')
+ if line.startswith('*'):
+ line = line[1:].strip()
+ return line
+
+
+def put_threads():
+ radix = gdb.parameter('output-radix')
+ inf = gdb.selected_inferior()
+ keys = []
+ for t in inf.threads():
+ tpath = THREAD_PATTERN.format(infnum=inf.num, tnum=t.num)
+ tobj = STATE.trace.create_object(tpath)
+ keys.append(THREAD_KEY_PATTERN.format(tnum=t.num))
+ tobj.set_value('_state', convert_state(t))
+ tobj.set_value('_name', t.name)
+ tid = convert_tid(t.ptid)
+ tobj.set_value('_tid', tid)
+ tidstr = ('0x{:x}' if radix ==
+ 16 else '0{:o}' if radix == 8 else '{}').format(tid)
+ tobj.set_value('_short_display', '[{}.{}:{}]'.format(
+ inf.num, t.num, tidstr))
+ tobj.set_value('_display', compute_thread_display(t))
+ tobj.insert()
+ STATE.trace.proxy_object_path(
+ THREADS_PATTERN.format(infnum=inf.num)).retain_values(keys)
+
+
+def put_event_thread():
+ inf = gdb.selected_inferior()
+ # Assumption: Event thread is selected by gdb upon stopping
+ t = gdb.selected_thread()
+ if t is not None:
+ tpath = THREAD_PATTERN.format(infnum=inf.num, tnum=t.num)
+ tobj = STATE.trace.proxy_object_path(tpath)
+ else:
+ tobj = None
+ STATE.trace.proxy_object_path('').set_value('_event_thread', tobj)
+
+
+@cmd('ghidra trace put-threads', '-ghidra-trace-put-threads', gdb.COMMAND_DATA,
+ True)
+def ghidra_trace_put_threads(*, is_mi, **kwargs):
+ """
+ Put the current inferior's threads into the Ghidra trace
+ """
+
+ STATE.require_tx()
+ with STATE.client.batch() as b:
+ put_threads()
+
+
+def put_frames():
+ inf = gdb.selected_inferior()
+ mapper = STATE.trace.memory_mapper
+ t = gdb.selected_thread()
+ if t is None:
+ return
+ bt = gdb.execute('bt', to_string=True).strip().split('\n')
+ f = newest_frame(gdb.selected_frame())
+ keys = []
+ while f is not None:
+ fpath = FRAME_PATTERN.format(
+ infnum=inf.num, tnum=t.num, level=f.level())
+ fobj = STATE.trace.create_object(fpath)
+ keys.append(FRAME_KEY_PATTERN.format(level=f.level()))
+ base, pc = mapper.map(inf, f.pc())
+ if base != pc.space:
+ STATE.trace.create_overlay_space(base, pc.space)
+ fobj.set_value('_pc', pc)
+ fobj.set_value('_func', str(f.function()))
+ fobj.set_value(
+ '_display', bt[f.level()].strip().replace('\\s+', ' '))
+ f = f.older()
+ fobj.insert()
+ STATE.trace.proxy_object_path(STACK_PATTERN.format(
+ infnum=inf.num, tnum=t.num)).retain_values(keys)
+
+
+@cmd('ghidra trace put-frames', '-ghidra-trace-put-frames', gdb.COMMAND_DATA,
+ True)
+def ghidra_trace_put_frames(*, is_mi, **kwargs):
+ """
+ Put the current thread's frames into the Ghidra trace.
+ """
+
+ STATE.require_tx()
+ with STATE.client.batch() as b:
+ put_frames()
+
+
+@cmd('ghidra trace put-all', '-ghidra-trace-put-all', gdb.COMMAND_DATA, True)
+def ghidra_trace_put_all(*, is_mi, **kwargs):
+ """
+ Put everything currently selected into the Ghidra trace
+ """
+
+ STATE.require_tx()
+ with STATE.client.batch() as b:
+ ghidra_trace_putreg(is_mi=is_mi)
+ ghidra_trace_putmem("$pc", "1", is_mi=is_mi)
+ ghidra_trace_putmem("$sp", "1", is_mi=is_mi)
+ put_inferiors()
+ put_environment()
+ put_regions()
+ put_modules()
+ put_threads()
+ put_frames()
+ put_breakpoints()
+
+
+@cmd('ghidra trace install-hooks', '-ghidra-trace-install-hooks',
+ gdb.COMMAND_SUPPORT, False)
+def ghidra_trace_install_hooks(*, is_mi, **kwargs):
+ """
+ Install hooks to trace in Ghidra.
+ """
+
+ hooks.install_hooks()
+
+
+@cmd('ghidra trace remove-hooks', '-ghidra-trace-remove-hooks',
+ gdb.COMMAND_SUPPORT, False)
+def ghidra_trace_remove_hooks(*, is_mi, **kwargs):
+ """
+ Remove hooks to trace in Ghidra.
+
+ Using this directly is not recommended, unless it seems the hooks are
+ preventing gdb or other extensions from operating. Removing hooks will break
+ trace synchronization until they are replaced.
+ """
+
+ hooks.remove_hooks()
+
+
+@cmd('ghidra trace sync-enable', '-ghidra-trace-sync-enable',
+ gdb.COMMAND_SUPPORT, True)
+def ghidra_trace_sync_enable(*, is_mi, **kwargs):
+ """
+ Synchronize the current inferior with the Ghidra trace
+
+ This will automatically install hooks if necessary. The goal is to record
+ the current frame, thread, and inferior into the trace immediately, and then
+ to append the trace upon stopping and/or selecting new frames. This action
+ is effective only for the current inferior. This command must be executed
+ for each individual inferior you'd like to synchronize. In older versions of
+ gdb, certain events cannot be hooked. In that case, you may need to execute
+ certain "trace put" commands manually, or go without.
+
+ This will have no effect unless or until you start a trace.
+ """
+
+ hooks.install_hooks()
+ hooks.enable_current_inferior()
+
+
+@cmd('ghidra trace sync-disable', '-ghidra-trace-sync-disable',
+ gdb.COMMAND_SUPPORT, True)
+def ghidra_trace_sync_disable(*, is_mi, **kwargs):
+ """
+ Cease synchronizing the current inferior with the Ghidra trace.
+
+ This is the opposite of 'ghidra trace sync-disable', except it will not
+ automatically remove hooks.
+ """
+
+ hooks.disable_current_inferior()
+
+
+@cmd('ghidra util wait-stopped', '-ghidra-util-wait-stopped', gdb.COMMAND_NONE, False)
+def ghidra_util_wait_stopped(timeout='1', *, is_mi, **kwargs):
+ """
+ Spin wait until the selected thread is stopped.
+ """
+
+ timeout = int(timeout)
+ start = time.time()
+ t = gdb.selected_thread()
+ if t is None:
+ return
+ while t.is_running():
+ t = gdb.selected_thread() # I suppose it could change
+ time.sleep(0.1)
+ if time.time() - start > timeout:
+ raise gdb.GdbError('Timed out waiting for thread to stop')
diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/py/ghidragdb/hooks.py b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/ghidragdb/hooks.py
new file mode 100644
index 0000000000..e60115d114
--- /dev/null
+++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/ghidragdb/hooks.py
@@ -0,0 +1,540 @@
+## ###
+# IP: GHIDRA
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+import time
+
+import gdb
+
+from . import commands
+
+
+class GhidraHookPrefix(gdb.Command):
+ """Commands for exporting data to a Ghidra trace"""
+
+ def __init__(self):
+ super().__init__('ghidra-hook', gdb.COMMAND_NONE, prefix=True)
+
+
+GhidraHookPrefix()
+
+
+class HookState(object):
+ __slots__ = ('installed', 'mem_catchpoint', 'batch')
+
+ def __init__(self):
+ self.installed = False
+ self.mem_catchpoint = None
+ self.batch = None
+
+ def ensure_batch(self):
+ if self.batch is None:
+ self.batch = commands.STATE.client.start_batch()
+
+ def end_batch(self):
+ if self.batch is None:
+ return
+ commands.STATE.client.end_batch()
+ self.batch = None
+
+
+class InferiorState(object):
+ __slots__ = ('first', 'regions', 'modules', 'threads', 'breaks', 'visited')
+
+ def __init__(self):
+ self.first = True
+ # For things we can detect changes to between stops
+ self.regions = False
+ self.modules = False
+ self.threads = False
+ self.breaks = False
+ # For frames and threads that have already been synced since last stop
+ self.visited = set()
+
+ def record(self, description=None):
+ first = self.first
+ self.first = False
+ if description is not None:
+ commands.STATE.trace.snapshot(description)
+ if first:
+ commands.put_inferiors()
+ commands.put_environment()
+ if self.threads:
+ commands.put_threads()
+ self.threads = False
+ thread = gdb.selected_thread()
+ if thread is not None:
+ if first or thread not in self.visited:
+ commands.put_frames()
+ self.visited.add(thread)
+ frame = gdb.selected_frame()
+ hashable_frame = (thread, frame.level())
+ if first or hashable_frame not in self.visited:
+ commands.putreg(frame, frame.architecture().registers())
+ commands.putmem("$pc", "1", from_tty=False)
+ commands.putmem("$sp", "1", from_tty=False)
+ self.visited.add(hashable_frame)
+ if first or self.regions or self.threads or self.modules:
+ # Sections, memory syscalls, or stack allocations
+ commands.put_regions()
+ self.regions = False
+ if first or self.modules:
+ commands.put_modules()
+ self.modules = False
+ if first or self.breaks:
+ commands.put_breakpoints()
+ self.breaks = False
+
+ def record_continued(self):
+ commands.put_inferiors()
+ commands.put_threads()
+
+ def record_exited(self, exit_code):
+ inf = gdb.selected_inferior()
+ ipath = commands.INFERIOR_PATTERN.format(infnum=inf.num)
+ infobj = commands.STATE.trace.proxy_object_path(ipath)
+ infobj.set_value('_exit_code', exit_code)
+ infobj.set_value('_state', 'TERMINATED')
+
+
+class BrkState(object):
+ __slots__ = ('break_loc_counts',)
+
+ def __init__(self):
+ self.break_loc_counts = {}
+
+ def update_brkloc_count(self, b, count):
+ self.break_loc_counts[b] = count
+
+ def get_brkloc_count(self, b):
+ return self.break_loc_counts.get(b, 0)
+
+ def del_brkloc_count(self, b):
+ if b not in self.break_loc_counts:
+ return 0 # TODO: Print a warning?
+ count = self.break_loc_counts[b]
+ del self.break_loc_counts[b]
+ return count
+
+
+HOOK_STATE = HookState()
+BRK_STATE = BrkState()
+INF_STATES = {}
+
+
+def on_new_inferior(event):
+ trace = commands.STATE.trace
+ if trace is None:
+ return
+ HOOK_STATE.ensure_batch()
+ with trace.open_tx("New Inferior {}".format(event.inferior.num)):
+ commands.put_inferiors() # TODO: Could put just the one....
+
+
+def on_inferior_selected():
+ inf = gdb.selected_inferior()
+ if inf.num not in INF_STATES:
+ return
+ trace = commands.STATE.trace
+ if trace is None:
+ return
+ HOOK_STATE.ensure_batch()
+ with trace.open_tx("Inferior {} selected".format(inf.num)):
+ INF_STATES[inf.num].record()
+ commands.activate()
+
+
+def on_inferior_deleted(event):
+ trace = commands.STATE.trace
+ if trace is None:
+ return
+ if event.inferior.num in INF_STATES:
+ del INF_STATES[event.inferior.num]
+ HOOK_STATE.ensure_batch()
+ with trace.open_tx("Inferior {} deleted".format(event.inferior.num)):
+ commands.put_inferiors() # TODO: Could just delete the one....
+
+
+def on_new_thread(event):
+ inf = gdb.selected_inferior()
+ if inf.num not in INF_STATES:
+ return
+ INF_STATES[inf.num].threads = True
+ # TODO: Syscall clone/exit to detect thread destruction?
+
+
+def on_thread_selected():
+ inf = gdb.selected_inferior()
+ if inf.num not in INF_STATES:
+ return
+ trace = commands.STATE.trace
+ if trace is None:
+ return
+ t = gdb.selected_thread()
+ HOOK_STATE.ensure_batch()
+ with trace.open_tx("Thread {}.{} selected".format(inf.num, t.num)):
+ INF_STATES[inf.num].record()
+ commands.activate()
+
+
+def on_frame_selected():
+ inf = gdb.selected_inferior()
+ if inf.num not in INF_STATES:
+ return
+ trace = commands.STATE.trace
+ if trace is None:
+ return
+ t = gdb.selected_thread()
+ f = gdb.selected_frame()
+ HOOK_STATE.ensure_batch()
+ with trace.open_tx("Frame {}.{}.{} selected".format(inf.num, t.num, f.level())):
+ INF_STATES[inf.num].record()
+ commands.activate()
+
+
+def on_syscall_memory():
+ inf = gdb.selected_inferior()
+ if inf.num not in INF_STATES:
+ return
+ INF_STATES[inf.num].regions = True
+
+
+def on_memory_changed(event):
+ inf = gdb.selected_inferior()
+ if inf.num not in INF_STATES:
+ return
+ trace = commands.STATE.trace
+ if trace is None:
+ return
+ HOOK_STATE.ensure_batch()
+ with trace.open_tx("Memory *0x{:08x} changed".format(event.address)):
+ commands.put_bytes(event.address, event.address + event.length,
+ pages=False, is_mi=False, from_tty=False)
+
+
+def on_register_changed(event):
+ gdb.write("Register changed: {}".format(dir(event)))
+ inf = gdb.selected_inferior()
+ if inf.num not in INF_STATES:
+ return
+ trace = commands.STATE.trace
+ if trace is None:
+ return
+ # I'd rather have a descriptor!
+ # TODO: How do I get the descriptor from the number?
+ # For now, just record the lot
+ HOOK_STATE.ensure_batch()
+ with trace.open_tx("Register {} changed".format(event.regnum)):
+ commands.putreg(event.frame, event.frame.architecture().registers())
+
+
+def on_cont(event):
+ inf = gdb.selected_inferior()
+ if inf.num not in INF_STATES:
+ return
+ trace = commands.STATE.trace
+ if trace is None:
+ return
+ state = INF_STATES[inf.num]
+ HOOK_STATE.ensure_batch()
+ with trace.open_tx("Continued"):
+ state.record_continued()
+
+
+def on_stop(event):
+ if hasattr(event, 'breakpoints') and HOOK_STATE.mem_catchpoint in event.breakpoints:
+ return
+ inf = gdb.selected_inferior()
+ if inf.num not in INF_STATES:
+ return
+ trace = commands.STATE.trace
+ if trace is None:
+ return
+ state = INF_STATES[inf.num]
+ state.visited.clear()
+ HOOK_STATE.ensure_batch()
+ with trace.open_tx("Stopped"):
+ state.record("Stopped")
+ commands.put_event_thread()
+ commands.activate()
+ HOOK_STATE.end_batch()
+
+
+def on_exited(event):
+ inf = gdb.selected_inferior()
+ if inf.num not in INF_STATES:
+ return
+ trace = commands.STATE.trace
+ if trace is None:
+ return
+ state = INF_STATES[inf.num]
+ state.visited.clear()
+ description = "Exited"
+ if hasattr(event, 'exit_code'):
+ description += " with code {}".format(event.exit_code)
+ HOOK_STATE.ensure_batch()
+ with trace.open_tx(description):
+ state.record(description)
+ if hasattr(event, 'exit_code'):
+ state.record_exited(event.exit_code)
+ commands.put_event_thread()
+ commands.activate()
+ HOOK_STATE.end_batch()
+
+
+def notify_others_breaks(inf):
+ for num, state in INF_STATES.items():
+ if num != inf.num:
+ state.breaks = True
+
+
+def modules_changed():
+ # Assumption: affects the current inferior
+ inf = gdb.selected_inferior()
+ if inf.num not in INF_STATES:
+ return
+ INF_STATES[inf.num].modules = True
+
+
+def on_clear_objfiles(event):
+ modules_changed()
+
+
+def on_new_objfile(event):
+ modules_changed()
+
+
+def on_free_objfile(event):
+ modules_changed()
+
+
+def on_breakpoint_created(b):
+ inf = gdb.selected_inferior()
+ notify_others_breaks(inf)
+ if inf.num not in INF_STATES:
+ return
+ trace = commands.STATE.trace
+ if trace is None:
+ return
+ ibpath = commands.INF_BREAKS_PATTERN.format(infnum=inf.num)
+ HOOK_STATE.ensure_batch()
+ with trace.open_tx("Breakpoint {} created".format(b.number)):
+ ibobj = trace.create_object(ibpath)
+ # Do not use retain_values or it'll remove other locs
+ commands.put_single_breakpoint(b, ibobj, inf, [])
+ ibobj.insert()
+
+
+def on_breakpoint_modified(b):
+ inf = gdb.selected_inferior()
+ notify_others_breaks(inf)
+ if inf.num not in INF_STATES:
+ return
+ old_count = BRK_STATE.get_brkloc_count(b)
+ trace = commands.STATE.trace
+ if trace is None:
+ return
+ ibpath = commands.INF_BREAKS_PATTERN.format(infnum=inf.num)
+ HOOK_STATE.ensure_batch()
+ with trace.open_tx("Breakpoint {} modified".format(b.number)):
+ ibobj = trace.create_object(ibpath)
+ commands.put_single_breakpoint(b, ibobj, inf, [])
+ new_count = BRK_STATE.get_brkloc_count(b)
+ # NOTE: Location may not apply to inferior, but whatever.
+ for i in range(new_count, old_count):
+ ikey = commands.INF_BREAK_KEY_PATTERN.format(
+ breaknum=b.number, locnum=i+1)
+ ibobj.set_value(ikey, None)
+
+
+def on_breakpoint_deleted(b):
+ inf = gdb.selected_inferior()
+ notify_others_breaks(inf)
+ if inf.num not in INF_STATES:
+ return
+ old_count = BRK_STATE.del_brkloc_count(b)
+ trace = commands.STATE.trace
+ if trace is None:
+ return
+ bpath = commands.BREAKPOINT_PATTERN.format(breaknum=b.number)
+ ibobj = trace.proxy_object_path(
+ commands.INF_BREAKS_PATTERN.format(infnum=inf.num))
+ HOOK_STATE.ensure_batch()
+ with trace.open_tx("Breakpoint {} modified".format(b.number)):
+ trace.proxy_object_path(bpath).remove(tree=True)
+ for i in range(old_count):
+ ikey = commands.INF_BREAK_KEY_PATTERN.format(
+ breaknum=b.number, locnum=i+1)
+ ibobj.set_value(ikey, None)
+
+
+def on_before_prompt():
+ HOOK_STATE.end_batch()
+
+
+# This will be called by a catchpoint
+class GhidraTraceEventMemoryCommand(gdb.Command):
+ def __init__(self):
+ super().__init__('ghidra-hook event-memory', gdb.COMMAND_NONE)
+
+ def invoke(self, argument, from_tty):
+ self.dont_repeat()
+ on_syscall_memory()
+
+
+GhidraTraceEventMemoryCommand()
+
+
+def cmd_hook(name):
+ def _cmd_hook(func):
+ class _ActiveCommand(gdb.Command):
+ def __init__(self):
+ # It seems we can't hook commands using the Python API....
+ super().__init__(f"ghidra-hook def-{name}", gdb.COMMAND_USER)
+ gdb.execute(f"""
+ define {name}
+ ghidra-hook def-{name}
+ end
+ """)
+
+ def invoke(self, argument, from_tty):
+ self.dont_repeat()
+ func()
+
+ def _unhook_command():
+ gdb.execute(f"""
+ define {name}
+ end
+ """)
+ func.hook = _ActiveCommand
+ func.unhook = _unhook_command
+ return func
+ return _cmd_hook
+
+
+@cmd_hook('hookpost-inferior')
+def hook_inferior():
+ on_inferior_selected()
+
+
+@cmd_hook('hookpost-thread')
+def hook_thread():
+ on_thread_selected()
+
+
+@cmd_hook('hookpost-frame')
+def hook_frame():
+ on_frame_selected()
+
+
+# TODO: Checks and workarounds for events missing in gdb 8
+def install_hooks():
+ if HOOK_STATE.installed:
+ return
+ HOOK_STATE.installed = True
+
+ gdb.events.new_inferior.connect(on_new_inferior)
+ hook_inferior.hook()
+ gdb.events.inferior_deleted.connect(on_inferior_deleted)
+
+ gdb.events.new_thread.connect(on_new_thread)
+ hook_thread.hook()
+ hook_frame.hook()
+
+ # Respond to user-driven state changes: (Not target-driven)
+ gdb.events.memory_changed.connect(on_memory_changed)
+ gdb.events.register_changed.connect(on_register_changed)
+ # Respond to target-driven memory map changes:
+ # group:memory is actually a bit broad, but will probably port better
+ # One alternative is to name all syscalls that cause a change....
+ # Ones we could probably omit:
+ # msync,
+ # (Deals in syncing file-backed pages to disk.)
+ # mlock, munlock, mlockall, munlockall, mincore, madvise,
+ # (Deal in paging. Doesn't affect valid addresses.)
+ # mbind, get_mempolicy, set_mempolicy, migrate_pages, move_pages
+ # (All NUMA stuff)
+ #
+ if HOOK_STATE.mem_catchpoint is not None:
+ HOOK_STATE.mem_catchpoint.enabled = True
+ else:
+ breaks_before = set(gdb.breakpoints())
+ gdb.execute("""
+ catch syscall group:memory
+ commands
+ silent
+ ghidra-hook event-memory
+ cont
+ end
+ """)
+ HOOK_STATE.mem_catchpoint = (
+ set(gdb.breakpoints()) - breaks_before).pop()
+
+ gdb.events.cont.connect(on_cont)
+ gdb.events.stop.connect(on_stop)
+ gdb.events.exited.connect(on_exited) # Inferior exited
+
+ gdb.events.clear_objfiles.connect(on_clear_objfiles)
+ gdb.events.free_objfile.connect(on_free_objfile)
+ gdb.events.new_objfile.connect(on_new_objfile)
+
+ gdb.events.breakpoint_created.connect(on_breakpoint_created)
+ gdb.events.breakpoint_deleted.connect(on_breakpoint_deleted)
+ gdb.events.breakpoint_modified.connect(on_breakpoint_modified)
+
+ gdb.events.before_prompt.connect(on_before_prompt)
+
+
+def remove_hooks():
+ if not HOOK_STATE.installed:
+ return
+ HOOK_STATE.installed = False
+
+ gdb.events.new_inferior.disconnect(on_new_inferior)
+ hook_inferior.unhook()
+ gdb.events.inferior_deleted.disconnect(on_inferior_deleted)
+
+ gdb.events.new_thread.disconnect(on_new_thread)
+ hook_thread.unhook()
+ hook_frame.unhook()
+
+ gdb.events.memory_changed.disconnect(on_memory_changed)
+ gdb.events.register_changed.disconnect(on_register_changed)
+ HOOK_STATE.mem_catchpoint.enabled = False
+
+ gdb.events.cont.disconnect(on_cont)
+ gdb.events.stop.disconnect(on_stop)
+ gdb.events.exited.disconnect(on_exited) # Inferior exited
+
+ gdb.events.clear_objfiles.disconnect(on_clear_objfiles)
+ gdb.events.free_objfile.disconnect(on_free_objfile)
+ gdb.events.new_objfile.disconnect(on_new_objfile)
+
+ gdb.events.breakpoint_created.disconnect(on_breakpoint_created)
+ gdb.events.breakpoint_deleted.disconnect(on_breakpoint_deleted)
+ gdb.events.breakpoint_modified.disconnect(on_breakpoint_modified)
+
+ gdb.events.before_prompt.disconnect(on_before_prompt)
+
+
+def enable_current_inferior():
+ inf = gdb.selected_inferior()
+ INF_STATES[inf.num] = InferiorState()
+
+
+def disable_current_inferior():
+ inf = gdb.selected_inferior()
+ if inf.num in INF_STATES:
+ # Silently ignore already disabled
+ del INF_STATES[inf.num]
diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/py/ghidragdb/methods.py b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/ghidragdb/methods.py
new file mode 100644
index 0000000000..3779016263
--- /dev/null
+++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/ghidragdb/methods.py
@@ -0,0 +1,653 @@
+## ###
+# IP: GHIDRA
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+from concurrent.futures import Future, Executor
+import re
+
+from ghidratrace import sch
+from ghidratrace.client import MethodRegistry, ParamDesc, Address, AddressRange
+
+import gdb
+
+from . import commands, hooks, util
+
+
+class GdbExecutor(Executor):
+ def submit(self, fn, *args, **kwargs):
+ fut = Future()
+
+ def _exec():
+ try:
+ result = fn(*args, **kwargs)
+ hooks.HOOK_STATE.end_batch()
+ fut.set_result(result)
+ except Exception as e:
+ fut.set_exception(e)
+
+ gdb.post_event(_exec)
+ return fut
+
+
+REGISTRY = MethodRegistry(GdbExecutor())
+
+
+def extre(base, ext):
+ return re.compile(base.pattern + ext)
+
+
+AVAILABLE_PATTERN = re.compile('Available\[(?P\\d*)\]')
+BREAKPOINT_PATTERN = re.compile('Breakpoints\[(?P\\d*)\]')
+BREAK_LOC_PATTERN = extre(BREAKPOINT_PATTERN, '\[(?P\\d*)\]')
+INFERIOR_PATTERN = re.compile('Inferiors\[(?P\\d*)\]')
+INF_BREAKS_PATTERN = extre(INFERIOR_PATTERN, '\.Breakpoints')
+ENV_PATTERN = extre(INFERIOR_PATTERN, '\.Environment')
+THREADS_PATTERN = extre(INFERIOR_PATTERN, '\.Threads')
+THREAD_PATTERN = extre(THREADS_PATTERN, '\[(?P\\d*)\]')
+STACK_PATTERN = extre(THREAD_PATTERN, '\.Stack')
+FRAME_PATTERN = extre(STACK_PATTERN, '\[(?P\\d*)\]')
+REGS_PATTERN = extre(FRAME_PATTERN, '.Registers')
+MEMORY_PATTERN = extre(INFERIOR_PATTERN, '\.Memory')
+MODULES_PATTERN = extre(INFERIOR_PATTERN, '\.Modules')
+
+
+def find_availpid_by_pattern(pattern, object, err_msg):
+ mat = pattern.fullmatch(object.path)
+ if mat is None:
+ raise TypeError(f"{object} is not {err_msg}")
+ pid = int(mat['pid'])
+ return pid
+
+
+def find_availpid_by_obj(object):
+ return find_availpid_by_pattern(AVAILABLE_PATTERN, object, "an Available")
+
+
+def find_inf_by_num(infnum):
+ for inf in gdb.inferiors():
+ if inf.num == infnum:
+ return inf
+ raise KeyError(f"Inferiors[{infnum}] does not exist")
+
+
+def find_inf_by_pattern(object, pattern, err_msg):
+ mat = pattern.fullmatch(object.path)
+ if mat is None:
+ raise TypeError(f"{object} is not {err_msg}")
+ infnum = int(mat['infnum'])
+ return find_inf_by_num(infnum)
+
+
+def find_inf_by_obj(object):
+ return find_inf_by_pattern(object, INFERIOR_PATTERN, "an Inferior")
+
+
+def find_inf_by_infbreak_obj(object):
+ return find_inf_by_pattern(object, INF_BREAKS_PATTERN,
+ "a BreakpointLocationContainer")
+
+
+def find_inf_by_env_obj(object):
+ return find_inf_by_pattern(object, ENV_PATTERN, "an Environment")
+
+
+def find_inf_by_threads_obj(object):
+ return find_inf_by_pattern(object, THREADS_PATTERN, "a ThreadContainer")
+
+
+def find_inf_by_mem_obj(object):
+ return find_inf_by_pattern(object, MEMORY_PATTERN, "a Memory")
+
+
+def find_inf_by_modules_obj(object):
+ return find_inf_by_pattern(object, MODULES_PATTERN, "a ModuleContainer")
+
+
+def find_thread_by_num(inf, tnum):
+ for t in inf.threads():
+ if t.num == tnum:
+ return t
+ raise KeyError(f"Inferiors[{inf.num}].Threads[{tnum}] does not exist")
+
+
+def find_thread_by_pattern(pattern, object, err_msg):
+ mat = pattern.fullmatch(object.path)
+ if mat is None:
+ raise TypeError(f"{object} is not {err_msg}")
+ infnum = int(mat['infnum'])
+ tnum = int(mat['tnum'])
+ inf = find_inf_by_num(infnum)
+ return find_thread_by_num(inf, tnum)
+
+
+def find_thread_by_obj(object):
+ return find_thread_by_pattern(THREAD_PATTERN, object, "a Thread")
+
+
+def find_thread_by_stack_obj(object):
+ return find_thread_by_pattern(STACK_PATTERN, object, "a Stack")
+
+
+def find_frame_by_level(thread, level):
+ # Because threads don't have any attribute to get at frames
+ thread.switch()
+ f = gdb.selected_frame()
+
+ # Navigate up or down, because I can't just get by level
+ down = level - f.level()
+ while down > 0:
+ f = f.older()
+ if f is None:
+ raise KeyError(
+ f"Inferiors[{thread.inferior.num}].Threads[{thread.num}].Stack[{level}] does not exist")
+ down -= 1
+ while down < 0:
+ f = f.newer()
+ if f is None:
+ raise KeyError(
+ f"Inferiors[{thread.inferior.num}].Threads[{thread.num}].Stack[{level}] does not exist")
+ down += 1
+ assert f.level() == level
+ return f
+
+
+def find_frame_by_pattern(pattern, object, err_msg):
+ mat = pattern.fullmatch(object.path)
+ if mat is None:
+ raise TypeError(f"{object} is not {err_msg}")
+ infnum = int(mat['infnum'])
+ tnum = int(mat['tnum'])
+ level = int(mat['level'])
+ inf = find_inf_by_num(infnum)
+ t = find_thread_by_num(inf, tnum)
+ return find_frame_by_level(t, level)
+
+
+def find_frame_by_obj(object):
+ return find_frame_by_pattern(FRAME_PATTERN, object, "a StackFrame")
+
+
+def find_frame_by_regs_obj(object):
+ return find_frame_by_pattern(REGS_PATTERN, object,
+ "a RegisterValueContainer")
+
+
+# Because there's no method to get a register by name....
+def find_reg_by_name(f, name):
+ for reg in f.architecture().registers():
+ if reg.name == name:
+ return reg
+ raise KeyError(f"No such register: {name}")
+
+
+# Oof. no gdb/Python method to get breakpoint by number
+# I could keep my own cache in a dict, but why?
+def find_bpt_by_number(breaknum):
+ # TODO: If len exceeds some threshold, use binary search?
+ for b in gdb.breakpoints():
+ if b.number == breaknum:
+ return b
+ raise KeyError(f"Breakpoints[{breaknum}] does not exist")
+
+
+def find_bpt_by_pattern(pattern, object, err_msg):
+ mat = pattern.fullmatch(object.path)
+ if mat is None:
+ raise TypeError(f"{object} is not {err_msg}")
+ breaknum = int(mat['breaknum'])
+ return find_bpt_by_number(breaknum)
+
+
+def find_bpt_by_obj(object):
+ return find_bpt_by_pattern(BREAKPOINT_PATTERN, object, "a BreakpointSpec")
+
+
+def find_bptlocnum_by_pattern(pattern, object, err_msg):
+ mat = pattern.fullmatch(object.path)
+ if mat is None:
+ raise TypError(f"{object} is not {err_msg}")
+ breaknum = int(mat['breaknum'])
+ locnum = int(mat['locnum'])
+ return breaknum, locnum
+
+
+def find_bptlocnum_by_obj(object):
+ return find_bptlocnum_by_pattern(BREAK_LOC_PATTERN, object,
+ "a BreakpointLocation")
+
+
+def find_bpt_loc_by_obj(object):
+ breaknum, locnum = find_bptlocnum_by_obj(object)
+ bpt = find_bpt_by_number(breaknum)
+ # Requires gdb-13.1 or later
+ return bpt.locations[locnum - 1] # Display is 1-up
+
+
+def switch_inferior(inferior):
+ if gdb.selected_inferior().num == inferior.num:
+ return
+ gdb.execute("inferior {}".format(inferior.num))
+
+
+@REGISTRY.method
+def execute(cmd: str, to_string: bool=False):
+ """Execute a CLI command."""
+ return gdb.execute(cmd, to_string=to_string)
+
+
+@REGISTRY.method(action='refresh')
+def refresh_available(node: sch.Schema('AvailableContainer')):
+ """List processes on gdb's host system."""
+ with commands.open_tracked_tx('Refresh Available'):
+ gdb.execute('ghidra trace put-available')
+
+
+@REGISTRY.method(action='refresh')
+def refresh_breakpoints(node: sch.Schema('BreakpointContainer')):
+ """
+ Refresh the list of breakpoints (including locations for the current
+ inferior).
+ """
+ with commands.open_tracked_tx('Refresh Breakpoints'):
+ gdb.execute('ghidra trace put-breakpoints')
+
+
+@REGISTRY.method(action='refresh')
+def refresh_inferiors(node: sch.Schema('InferiorContainer')):
+ """Refresh the list of inferiors."""
+ with commands.open_tracked_tx('Refresh Inferiors'):
+ gdb.execute('ghidra trace put-inferiors')
+
+
+@REGISTRY.method(action='refresh')
+def refresh_inf_breakpoints(node: sch.Schema('BreakpointLocationContainer')):
+ """
+ Refresh the breakpoint locations for the inferior.
+
+ In the course of refreshing the locations, the breakpoint list will also be
+ refreshed.
+ """
+ switch_inferior(find_inf_by_infbreak_obj(node))
+ with commands.open_tracked_tx('Refresh Breakpoint Locations'):
+ gdb.execute('ghidra trace put-breakpoints')
+
+
+@REGISTRY.method(action='refresh')
+def refresh_environment(node: sch.Schema('Environment')):
+ """Refresh the environment descriptors (arch, os, endian)."""
+ switch_inferior(find_inf_by_env_obj(node))
+ with commands.open_tracked_tx('Refresh Environment'):
+ gdb.execute('ghidra trace put-environment')
+
+
+@REGISTRY.method(action='refresh')
+def refresh_threads(node: sch.Schema('ThreadContainer')):
+ """Refresh the list of threads in the inferior."""
+ switch_inferior(find_inf_by_threads_obj(node))
+ with commands.open_tracked_tx('Refresh Threads'):
+ gdb.execute('ghidra trace put-threads')
+
+
+@REGISTRY.method(action='refresh')
+def refresh_stack(node: sch.Schema('Stack')):
+ """Refresh the backtrace for the thread."""
+ find_thread_by_stack_obj(node).switch()
+ with commands.open_tracked_tx('Refresh Stack'):
+ gdb.execute('ghidra trace put-frames')
+
+
+@REGISTRY.method(action='refresh')
+def refresh_registers(node: sch.Schema('RegisterValueContainer')):
+ """Refresh the register values for the frame."""
+ find_frame_by_regs_obj(node).select()
+ # TODO: Groups?
+ with commands.open_tracked_tx('Refresh Registers'):
+ gdb.execute('ghidra trace putreg')
+
+
+@REGISTRY.method(action='refresh')
+def refresh_mappings(node: sch.Schema('Memory')):
+ """Refresh the list of memory regions for the inferior."""
+ switch_inferior(find_inf_by_mem_obj(node))
+ with commands.open_tracked_tx('Refresh Memory Regions'):
+ gdb.execute('ghidra trace put-regions')
+
+
+@REGISTRY.method(action='refresh')
+def refresh_modules(node: sch.Schema('ModuleContainer')):
+ """
+ Refresh the modules and sections list for the inferior.
+
+ This will refresh the sections for all modules, not just the selected one.
+ """
+ switch_inferior(find_inf_by_modules_obj(node))
+ with commands.open_tracked_tx('Refresh Modules'):
+ gdb.execute('ghidra trace put-modules')
+
+
+@REGISTRY.method(action='activate')
+def activate_inferior(inferior: sch.Schema('Inferior')):
+ """Switch to the inferior."""
+ switch_inferior(find_inf_by_obj(inferior))
+
+
+@REGISTRY.method(action='activate')
+def activate_thread(thread: sch.Schema('Thread')):
+ """Switch to the thread."""
+ find_thread_by_obj(thread).switch()
+
+
+@REGISTRY.method(action='activate')
+def activate_frame(frame: sch.Schema('StackFrame')):
+ """Select the frame."""
+ find_frame_by_obj(frame).select()
+
+
+@REGISTRY.method
+def add_inferior(container: sch.Schema('InferiorContainer')):
+ """Add a new inferior."""
+ gdb.execute('add-inferior')
+
+
+@REGISTRY.method(action='delete')
+def delete_inferior(inferior: sch.Schema('Inferior')):
+ """Remove the inferior."""
+ inf = find_inf_by_obj(inferior)
+ gdb.execute(f'remove-inferior {inf.num}')
+
+
+# TODO: Separate method for each of core, exec, remote, etc...?
+@REGISTRY.method
+def connect(inferior: sch.Schema('Inferior'), spec: str):
+ """Connect to a target machine or process."""
+ switch_inferior(find_inf_by_obj(inferior))
+ gdb.execute(f'target {spec}')
+
+
+@REGISTRY.method(action='attach')
+def attach_obj(inferior: sch.Schema('Inferior'), target: sch.Schema('Attachable')):
+ """Attach the inferior to the given target."""
+ switch_inferior(find_inf_by_obj(inferior))
+ pid = find_availpid_by_obj(target)
+ gdb.execute(f'attach {pid}')
+
+
+@REGISTRY.method(action='attach')
+def attach_pid(inferior: sch.Schema('Inferior'), pid: int):
+ """Attach the inferior to the given target."""
+ switch_inferior(find_inf_by_obj(inferior))
+ gdb.execute(f'attach {pid}')
+
+
+@REGISTRY.method
+def detach(inferior: sch.Schema('Inferior')):
+ """Detach the inferior's target."""
+ switch_inferior(find_inf_by_obj(inferior))
+ gdb.execute('detach')
+
+
+@REGISTRY.method(action='launch')
+def launch_main(inferior: sch.Schema('Inferior'),
+ file: ParamDesc(str, display='File'),
+ args: ParamDesc(str, display='Arguments')=''):
+ """
+ Start a native process with the given command line, stopping at 'main'
+ (start).
+
+ If 'main' is not defined in the file, this behaves like 'run'.
+ """
+ switch_inferior(find_inf_by_obj(inferior))
+ gdb.execute(f'''
+ file {file}
+ set args {args}
+ start
+ ''')
+
+
+@REGISTRY.method(action='launch', condition=util.GDB_VERSION.major >= 9)
+def launch_loader(inferior: sch.Schema('Inferior'),
+ file: ParamDesc(str, display='File'),
+ args: ParamDesc(str, display='Arguments')=''):
+ """
+ Start a native process with the given command line, stopping at first
+ instruction (starti).
+ """
+ switch_inferior(find_inf_by_obj(inferior))
+ gdb.execute(f'''
+ file {file}
+ set args {args}
+ starti
+ ''')
+
+
+@REGISTRY.method(action='launch')
+def launch_run(inferior: sch.Schema('Inferior'),
+ file: ParamDesc(str, display='File'),
+ args: ParamDesc(str, display='Arguments')=''):
+ """
+ Run a native process with the given command line (run).
+
+ The process will not stop until it hits one of your breakpoints, or it is
+ signaled.
+ """
+ switch_inferior(find_inf_by_obj(inferior))
+ gdb.execute(f'''
+ file {file}
+ set args {args}
+ run
+ ''')
+
+
+@REGISTRY.method
+def kill(inferior: sch.Schema('Inferior')):
+ """Kill execution of the inferior."""
+ switch_inferior(find_inf_by_obj(inferior))
+ gdb.execute('kill')
+
+
+@REGISTRY.method
+def resume(inferior: sch.Schema('Inferior')):
+ """Continue execution of the inferior."""
+ switch_inferior(find_inf_by_obj(inferior))
+ gdb.execute('continue')
+
+
+@REGISTRY.method
+def interrupt():
+ """Interrupt the execution of the debugged program."""
+ gdb.execute('interrupt')
+
+
+@REGISTRY.method
+def step_into(thread: sch.Schema('Thread'), n: ParamDesc(int, display='N')=1):
+ """Step one instruction exactly (stepi)."""
+ find_thread_by_obj(thread).switch()
+ gdb.execute('stepi')
+
+
+@REGISTRY.method
+def step_over(thread: sch.Schema('Thread'), n: ParamDesc(int, display='N')=1):
+ """Step one instruction, but proceed through subroutine calls (nexti)."""
+ find_thread_by_obj(thread).switch()
+ gdb.execute('nexti')
+
+
+@REGISTRY.method
+def step_out(thread: sch.Schema('Thread')):
+ """Execute until the current stack frame returns (finish)."""
+ find_thread_by_obj(thread).switch()
+ gdb.execute('finish')
+
+
+@REGISTRY.method(action='step_ext')
+def step_advance(thread: sch.Schema('Thread'), address: Address):
+ """Continue execution up to the given address (advance)."""
+ t = find_thread_by_obj(thread)
+ t.switch()
+ offset = thread.trace.memory_mapper.map_back(t.inferior, address)
+ gdb.execute(f'advance *0x{offset:x}')
+
+
+@REGISTRY.method(action='step_ext')
+def step_return(thread: sch.Schema('Thread'), value: int=None):
+ """Skip the remainder of the current function (return)."""
+ find_thread_by_obj(thread).switch()
+ if value is None:
+ gdb.execute('return')
+ else:
+ gdb.execute(f'return {value}')
+
+
+@REGISTRY.method(action='break_sw_execute')
+def break_sw_execute_address(inferior: sch.Schema('Inferior'), address: Address):
+ """Set a breakpoint (break)."""
+ inf = find_inf_by_obj(inferior)
+ offset = inferior.trace.memory_mapper.map_back(inf, address)
+ gdb.execute(f'break *0x{offset:x}')
+
+
+@REGISTRY.method(action='break_sw_execute')
+def break_sw_execute_expression(expression: str):
+ """Set a breakpoint (break)."""
+ # TODO: Escape?
+ gdb.execute(f'break {expression}')
+
+
+@REGISTRY.method(action='break_hw_execute')
+def break_hw_execute_address(inferior: sch.Schema('Inferior'), address: Address):
+ """Set a hardware-assisted breakpoint (hbreak)."""
+ inf = find_inf_by_obj(inferior)
+ offset = inferior.trace.memory_mapper.map_back(inf, address)
+ gdb.execute(f'hbreak *0x{offset:x}')
+
+
+@REGISTRY.method(action='break_hw_execute')
+def break_hw_execute_expression(expression: str):
+ """Set a hardware-assisted breakpoint (hbreak)."""
+ # TODO: Escape?
+ gdb.execute(f'hbreak {expression}')
+
+
+@REGISTRY.method(action='break_read')
+def break_read_range(inferior: sch.Schema('Inferior'), range: AddressRange):
+ """Set a read watchpoint (rwatch)."""
+ inf = find_inf_by_obj(inferior)
+ offset_start = inferior.trace.memory_mapper.map_back(
+ inf, Address(range.space, range.min))
+ gdb.execute(
+ f'rwatch -location *((char(*)[{range.length()}]) 0x{offset_start:x})')
+
+
+@REGISTRY.method(action='break_read')
+def break_read_expression(expression: str):
+ """Set a read watchpoint (rwatch)."""
+ gdb.execute(f'rwatch {expression}')
+
+
+@REGISTRY.method(action='break_write')
+def break_write_range(inferior: sch.Schema('Inferior'), range: AddressRange):
+ """Set a watchpoint (watch)."""
+ inf = find_inf_by_obj(inferior)
+ offset_start = inferior.trace.memory_mapper.map_back(
+ inf, Address(range.space, range.min))
+ gdb.execute(
+ f'watch -location *((char(*)[{range.length()}]) 0x{offset_start:x})')
+
+
+@REGISTRY.method(action='break_write')
+def break_write_expression(expression: str):
+ """Set a watchpoint (watch)."""
+ gdb.execute(f'watch {expression}')
+
+
+@REGISTRY.method(action='break_access')
+def break_access_range(inferior: sch.Schema('Inferior'), range: AddressRange):
+ """Set an access watchpoint (awatch)."""
+ inf = find_inf_by_obj(inferior)
+ offset_start = inferior.trace.memory_mapper.map_back(
+ inf, Address(range.space, range.min))
+ gdb.execute(
+ f'awatch -location *((char(*)[{range.length()}]) 0x{offset_start:x})')
+
+
+@REGISTRY.method(action='break_access')
+def break_access_expression(expression: str):
+ """Set an access watchpoint (awatch)."""
+ gdb.execute(f'awatch {expression}')
+
+
+@REGISTRY.method(action='break_ext')
+def break_event(spec: str):
+ """Set a catchpoint (catch)."""
+ gdb.execute(f'catch {spec}')
+
+
+@REGISTRY.method(action='toggle')
+def toggle_breakpoint(breakpoint: sch.Schema('BreakpointSpec'), enabled: bool):
+ """Toggle a breakpoint."""
+ bpt = find_bpt_by_obj(breakpoint)
+ bpt.enabled = enabled
+
+
+@REGISTRY.method(action='toggle', condition=util.GDB_VERSION.major >= 13)
+def toggle_breakpoint_location(location: sch.Schema('BreakpointLocation'), enabled: bool):
+ """Toggle a breakpoint location."""
+ loc = find_bpt_loc_by_obj(location)
+ loc.enabled = enabled
+
+
+@REGISTRY.method(action='toggle', condition=util.GDB_VERSION.major < 13)
+def toggle_breakpoint_location(location: sch.Schema('BreakpointLocation'), enabled: bool):
+ """Toggle a breakpoint location."""
+ bptnum, locnum = find_bptlocnum_by_obj(location)
+ cmd = 'enable' if enabled else 'disable'
+ gdb.execute(f'{cmd} {bptnum}.{locnum}')
+
+
+@REGISTRY.method(action='delete')
+def delete_breakpoint(breakpoint: sch.Schema('BreakpointSpec')):
+ """Delete a breakpoint."""
+ bpt = find_bpt_by_obj(breakpoint)
+ bpt.delete()
+
+
+@REGISTRY.method
+def read_mem(inferior: sch.Schema('Inferior'), range: AddressRange):
+ """Read memory."""
+ inf = find_inf_by_obj(inferior)
+ offset_start = inferior.trace.memory_mapper.map_back(
+ inf, Address(range.space, range.min))
+ with commands.open_tracked_tx('Read Memory'):
+ gdb.execute(f'ghidra trace putmem 0x{offset_start:x} {range.length()}')
+
+
+@REGISTRY.method
+def write_mem(inferior: sch.Schema('Inferior'), address: Address, data: bytes):
+ """Write memory."""
+ inf = find_inf_by_obj(inferior)
+ offset = inferior.trace.memory_mapper.map_back(inf, address)
+ inf.write_memory(offset, data)
+
+
+@REGISTRY.method
+def write_reg(frame: sch.Schema('Frame'), name: str, value: bytes):
+ """Write a register."""
+ f = find_frame_by_obj(frame)
+ f.select()
+ inf = gdb.selected_inferior()
+ mname, mval = frame.trace.register_mapper.map_value_back(inf, name, value)
+ reg = find_reg_by_name(f, mname)
+ size = int(gdb.parse_and_eval(f'sizeof(${mname})'))
+ arr = '{' + ','.join(str(b) for b in mval) + '}'
+ gdb.execute(f'set ((unsigned char[{size}])${mname}) = {arr}')
diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/py/ghidragdb/parameters.py b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/ghidragdb/parameters.py
new file mode 100644
index 0000000000..e68b998db7
--- /dev/null
+++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/ghidragdb/parameters.py
@@ -0,0 +1,46 @@
+## ###
+# IP: GHIDRA
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+import gdb
+
+# TODO: I don't know how to register a custom parameter prefix. I would rather
+# these were 'ghidra language' and 'ghidra compiler'
+
+
+class GhidraLanguageParameter(gdb.Parameter):
+ """
+ The language id for Ghidra traces. Set this to 'auto' to try to derive it
+ from 'show arch' and 'show endian'. Otherwise, set it to a Ghidra
+ LanguageID.
+ """
+
+ def __init__(self):
+ super().__init__('ghidra-language', gdb.COMMAND_DATA, gdb.PARAM_STRING)
+ self.value = 'auto'
+GhidraLanguageParameter()
+
+
+class GhidraCompilerParameter(gdb.Parameter):
+ """
+ The compiler spec id for Ghidra traces. Set this to 'auto' to try to derive
+ it from 'show osabi'. Otherwise, set it to a Ghidra CompilerSpecID. Note
+ that valid compiler spec ids depend on the language id.
+ """
+
+ def __init__(self):
+ super().__init__('ghidra-compiler', gdb.COMMAND_DATA, gdb.PARAM_STRING)
+ self.value = 'auto'
+GhidraCompilerParameter()
+
diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/py/ghidragdb/schema.xml b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/ghidragdb/schema.xml
new file mode 100644
index 0000000000..9f9c092b74
--- /dev/null
+++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/ghidragdb/schema.xml
@@ -0,0 +1,413 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/py/ghidragdb/util.py b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/ghidragdb/util.py
new file mode 100644
index 0000000000..caec011b32
--- /dev/null
+++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/ghidragdb/util.py
@@ -0,0 +1,286 @@
+## ###
+# IP: GHIDRA
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+from collections import namedtuple
+import re
+
+import gdb
+
+
+GdbVersion = namedtuple('GdbVersion', ['full', 'major', 'minor'])
+
+
+def _compute_gdb_ver():
+ blurb = gdb.execute('show version', to_string=True)
+ top = blurb.split('\n')[0]
+ full = top.split(' ')[-1]
+ major, minor = full.split('.')[:2]
+ return GdbVersion(full, int(major), int(minor))
+
+
+GDB_VERSION = _compute_gdb_ver()
+
+MODULES_CMD_V8 = 'maintenance info sections ALLOBJ'
+MODULES_CMD_V11 = 'maintenance info sections -all-objects'
+OBJFILE_PATTERN_V8 = re.compile("\\s*Object file: (?P.*)")
+OBJFILE_PATTERN_V11 = re.compile(
+ "\\s*((Object)|(Exec)) file: `(?P.*)', file type (?P.*)")
+OBJFILE_SECTION_PATTERN_V8 = re.compile("\\s*" +
+ "0x(?P[0-9A-Fa-f]+)\\s*->\\s*" +
+ "0x(?P[0-9A-Fa-f]+)\\s+at\\s+" +
+ "0x(?P[0-9A-Fa-f]+)\\s*:\\s*" +
+ "(?P\\S+)\\s+" +
+ "(?P.*)")
+OBJFILE_SECTION_PATTERN_V9 = re.compile("\\s*" +
+ "\\[\\s*(?P\\d+)\\]\\s+" +
+ "0x(?P[0-9A-Fa-f]+)\\s*->\\s*" +
+ "0x(?P[0-9A-Fa-f]+)\\s+at\\s+" +
+ "0x(?P[0-9A-Fa-f]+)\\s*:\\s*" +
+ "(?P\\S+)\\s+" +
+ "(?P.*)")
+GNU_DEBUGDATA_PREFIX = ".gnu_debugdata for "
+
+
+class Module(namedtuple('BaseModule', ['name', 'base', 'max', 'sections'])):
+ pass
+
+
+class Section(namedtuple('BaseSection', ['name', 'start', 'end', 'offset', 'attrs'])):
+ def better(self, other):
+ start = self.start if self.start != 0 else other.start
+ end = self.end if self.end != 0 else other.end
+ offset = self.offset if self.offset != 0 else other.offset
+ attrs = dict.fromkeys(self.attrs)
+ attrs.update(dict.fromkeys(other.attrs))
+ return Section(self.name, start, end, offset, list(attrs))
+
+
+def try_hexint(val, name):
+ try:
+ return int(val, 16)
+ except ValueError:
+ gdb.write("Invalid {}: {}".format(name, val), stream=gdb.STDERR)
+ return 0
+
+
+# AFAICT, Objfile does not give info about load addresses :(
+class ModuleInfoReader(object):
+ def name_from_line(self, line):
+ mat = self.objfile_pattern.fullmatch(line)
+ if mat is None:
+ return None
+ n = mat['name']
+ if n.startswith(GNU_DEBUGDATA_PREFIX):
+ return None
+ return None if mat is None else mat['name']
+
+ def section_from_line(self, line):
+ mat = self.section_pattern.fullmatch(line)
+ if mat is None:
+ return None
+ start = try_hexint(mat['vmaS'], 'section start')
+ end = try_hexint(mat['vmaE'], 'section end')
+ offset = try_hexint(mat['offset'], 'section offset')
+ name = mat['name']
+ attrs = [a for a in mat['attrs'].split(' ') if a != '']
+ return Section(name, start, end, offset, attrs)
+
+ def finish_module(self, name, sections):
+ alloc = {k: s for k, s in sections.items() if 'ALLOC' in s.attrs}
+ if len(alloc) == 0:
+ return Module(name, 0, 0, alloc)
+ # TODO: This may not be the module base, depending on headers
+ base_addr = min(s.start - s.offset for s in alloc.values())
+ max_addr = max(s.end for s in alloc.values())
+ return Module(name, base_addr, max_addr, alloc)
+
+ def get_modules(self):
+ modules = {}
+ out = gdb.execute(self.cmd, to_string=True)
+ name = None
+ sections = None
+ for line in out.split('\n'):
+ n = self.name_from_line(line)
+ if n is not None:
+ if name is not None:
+ modules[name] = self.finish_module(name, sections)
+ name = n
+ sections = {}
+ continue
+ if name is None:
+ # Don't waste time parsing if no module
+ continue
+ s = self.section_from_line(line)
+ if s is not None:
+ if s.name in sections:
+ s = s.better(sections[s.name])
+ sections[s.name] = s
+ if name is not None:
+ modules[name] = self.finish_module(name, sections)
+ return modules
+
+
+class ModuleInfoReaderV8(ModuleInfoReader):
+ cmd = MODULES_CMD_V8
+ objfile_pattern = OBJFILE_PATTERN_V8
+ section_pattern = OBJFILE_SECTION_PATTERN_V8
+
+
+class ModuleInfoReaderV9(ModuleInfoReader):
+ cmd = MODULES_CMD_V8
+ objfile_pattern = OBJFILE_PATTERN_V8
+ section_pattern = OBJFILE_SECTION_PATTERN_V9
+
+
+class ModuleInfoReaderV11(ModuleInfoReader):
+ cmd = MODULES_CMD_V11
+ objfile_pattern = OBJFILE_PATTERN_V11
+ section_pattern = OBJFILE_SECTION_PATTERN_V9
+
+
+def _choose_module_info_reader():
+ if GDB_VERSION.major == 8:
+ return ModuleInfoReaderV8()
+ elif GDB_VERSION.major == 9:
+ return ModuleInfoReaderV9()
+ elif GDB_VERSION.major == 10:
+ return ModuleInfoReaderV9()
+ elif GDB_VERSION.major == 11:
+ return ModuleInfoReaderV11()
+ elif GDB_VERSION.major == 12:
+ return ModuleInfoReaderV11()
+ elif GDB_VERSION.major > 12:
+ return ModuleInfoReaderV11()
+ else:
+ raise gdb.GdbError(
+ "GDB version not recognized by ghidragdb: " + GDB_VERSION.full)
+
+
+MODULE_INFO_READER = _choose_module_info_reader()
+
+
+REGIONS_CMD = 'info proc mappings'
+REGION_PATTERN_V8 = re.compile("\\s*" +
+ "0x(?P[0-9,A-F,a-f]+)\\s+" +
+ "0x(?P[0-9,A-F,a-f]+)\\s+" +
+ "0x(?P[0-9,A-F,a-f]+)\\s+" +
+ "0x(?P[0-9,A-F,a-f]+)\\s+" +
+ "(?P.*)")
+REGION_PATTERN_V12 = re.compile("\\s*" +
+ "0x(?P[0-9,A-F,a-f]+)\\s+" +
+ "0x(?P[0-9,A-F,a-f]+)\\s+" +
+ "0x(?P[0-9,A-F,a-f]+)\\s+" +
+ "0x(?P[0-9,A-F,a-f]+)\\s+" +
+ "(?P[rwsxp\\-]+)\\s+" +
+ "(?P.*)")
+
+
+class Region(namedtuple('BaseRegion', ['start', 'end', 'offset', 'perms', 'objfile'])):
+ pass
+
+
+class RegionInfoReader(object):
+ def region_from_line(self, line):
+ mat = self.region_pattern.fullmatch(line)
+ if mat is None:
+ return None
+ start = try_hexint(mat['start'], 'region start')
+ end = try_hexint(mat['end'], 'region end')
+ offset = try_hexint(mat['offset'], 'region offset')
+ perms = self.get_region_perms(mat)
+ objfile = mat['objfile']
+ return Region(start, end, offset, perms, objfile)
+
+ def get_regions(self):
+ regions = []
+ out = gdb.execute(self.cmd, to_string=True)
+ for line in out.split('\n'):
+ r = self.region_from_line(line)
+ if r is None:
+ continue
+ regions.append(r)
+ return regions
+
+ def full_mem(self):
+ # TODO: This may not work for Harvard architectures
+ sizeptr = int(gdb.parse_and_eval('sizeof(void*)')) * 8
+ return Region(0, 1 << sizeptr, 0, None, 'full memory')
+
+
+class RegionInfoReaderV8(RegionInfoReader):
+ cmd = REGIONS_CMD
+ region_pattern = REGION_PATTERN_V8
+
+ def get_region_perms(self, mat):
+ return None
+
+
+class RegionInfoReaderV12(RegionInfoReader):
+ cmd = REGIONS_CMD
+ region_pattern = REGION_PATTERN_V12
+
+ def get_region_perms(self, mat):
+ return mat['perms']
+
+
+def _choose_region_info_reader():
+ if 8 <= GDB_VERSION.major < 12:
+ return RegionInfoReaderV8()
+ elif GDB_VERSION.major >= 12:
+ return RegionInfoReaderV12()
+ else:
+ raise gdb.GdbError(
+ "GDB version not recognized by ghidragdb: " + GDB_VERSION.full)
+
+
+REGION_INFO_READER = _choose_region_info_reader()
+
+
+BREAK_LOCS_CMD = 'info break {}'
+BREAK_PATTERN = re.compile('')
+BREAK_LOC_PATTERN = re.compile('')
+
+
+class BreakpointLocation(namedtuple('BaseBreakpointLocation', ['address', 'enabled', 'thread_groups'])):
+ pass
+
+
+class BreakpointLocationInfoReaderV8(object):
+ def breakpoint_from_line(self, line):
+ pass
+
+ def location_from_line(self, line):
+ pass
+
+ def get_locations(self, breakpoint):
+ pass
+
+
+class BreakpointLocationInfoReaderV13(object):
+ def get_locations(self, breakpoint):
+ return breakpoint.locations
+
+
+def _choose_breakpoint_location_info_reader():
+ if 8 <= GDB_VERSION.major < 13:
+ return BreakpointLocationInfoReaderV8()
+ elif GDB_VERSION.major >= 13:
+ return BreakpointLocationInfoReaderV13()
+ else:
+ raise gdb.GdbError(
+ "GDB version not recognized by ghidragdb: " + GDB_VERSION.full)
+
+
+BREAKPOINT_LOCATION_INFO_READER = _choose_breakpoint_location_info_reader()
diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/py/pyproject.toml b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/pyproject.toml
new file mode 100644
index 0000000000..2bee84cb02
--- /dev/null
+++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/pyproject.toml
@@ -0,0 +1,25 @@
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "ghidragdb"
+version = "10.4"
+authors = [
+ { name="Ghidra Development Team" },
+]
+description = "Ghidra's Plugin for gdb"
+readme = "README.md"
+requires-python = ">=3.7"
+classifiers = [
+ "Programming Language :: Python :: 3",
+ "License :: OSI Approved :: Apache Software License",
+ "Operating System :: OS Independent",
+]
+dependencies = [
+ "ghidratrace==10.4",
+]
+
+[project.urls]
+"Homepage" = "https://github.com/NationalSecurityAgency/ghidra"
+"Bug Tracker" = "https://github.com/NationalSecurityAgency/ghidra/issues"
diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/py/tests/EMPTY b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/tests/EMPTY
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/GdbLinuxSpecimen.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/GdbLinuxSpecimen.java
index b372d173ef..3f62c29376 100644
--- a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/GdbLinuxSpecimen.java
+++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/GdbLinuxSpecimen.java
@@ -30,49 +30,49 @@ import ghidra.dbg.util.ShellUtils;
public enum GdbLinuxSpecimen implements DebuggerTestSpecimen, DebuggerModelTestUtils {
SLEEP {
@Override
- String getCommandLine() {
+ public String getCommandLine() {
return DummyProc.which("expTraceableSleep");
}
},
FORK_EXIT {
@Override
- String getCommandLine() {
+ public String getCommandLine() {
return DummyProc.which("expFork");
}
},
CLONE_EXIT {
@Override
- String getCommandLine() {
+ public String getCommandLine() {
return DummyProc.which("expCloneExit");
}
},
PRINT {
@Override
- String getCommandLine() {
+ public String getCommandLine() {
return DummyProc.which("expPrint");
}
},
REGISTERS {
@Override
- String getCommandLine() {
+ public String getCommandLine() {
return DummyProc.which("expRegisters");
}
},
SPIN_STRIPPED {
@Override
- String getCommandLine() {
+ public String getCommandLine() {
return DummyProc.which("expSpin.stripped");
}
},
STACK {
@Override
- String getCommandLine() {
+ public String getCommandLine() {
return DummyProc.which("expStack");
}
};
- abstract String getCommandLine();
+ public abstract String getCommandLine();
@Override
public DummyProc runDummy() throws Throwable {
diff --git a/Ghidra/Debug/Debugger-agent-lldb/build.gradle b/Ghidra/Debug/Debugger-agent-lldb/build.gradle
index 8b05a63bd7..61e2cc10f4 100644
--- a/Ghidra/Debug/Debugger-agent-lldb/build.gradle
+++ b/Ghidra/Debug/Debugger-agent-lldb/build.gradle
@@ -20,6 +20,7 @@ apply from: "$rootProject.projectDir/gradle/nativeProject.gradle"
apply from: "$rootProject.projectDir/gradle/distributableGhidraModule.gradle"
apply from: "$rootProject.projectDir/gradle/debugger/hasExecutableJar.gradle"
+apply from: "$rootProject.projectDir/gradle/debugger/hasPythonPackage.gradle"
apply plugin: 'eclipse'
eclipse.project.name = 'Debug Debugger-agent-lldb'
@@ -33,6 +34,8 @@ dependencies {
testImplementation project(path: ':Framework-AsyncComm', configuration: 'testArtifacts')
testImplementation project(path: ':Framework-Debugging', configuration: 'testArtifacts')
testImplementation project(path: ':Debugger-gadp', configuration: 'testArtifacts')
+
+ pypkgInstall project(path: ':Debugger-rmi-trace', configuration: 'pypkgInstall')
}
tasks.nodepJar {
diff --git a/Ghidra/Debug/Debugger-agent-lldb/certification.manifest b/Ghidra/Debug/Debugger-agent-lldb/certification.manifest
index be44694b3e..9297aca74d 100644
--- a/Ghidra/Debug/Debugger-agent-lldb/certification.manifest
+++ b/Ghidra/Debug/Debugger-agent-lldb/certification.manifest
@@ -5,7 +5,9 @@
.project||NONE||reviewed||END|
Module.manifest||GHIDRA||||END|
build.gradle||GHIDRA||||END|
-data/InstructionsForBuildingLLDBInterface.txt||GHIDRA||||END|
src/llvm-project/lldb/bindings/java/java-typemaps.swig||Apache License 2.0 with LLVM Exceptions||||END|
src/llvm-project/lldb/bindings/java/java.swig||Apache License 2.0 with LLVM Exceptions||||END|
-src/llvm-project/lldb/build_script||GHIDRA||||END|
+src/main/py/LICENSE||GHIDRA||||END|
+src/main/py/README.md||GHIDRA||||END|
+src/main/py/ghidralldb/schema.xml||GHIDRA||||END|
+src/main/py/pyproject.toml||GHIDRA||||END|
diff --git a/Ghidra/Debug/Debugger-agent-lldb/src/main/py/LICENSE b/Ghidra/Debug/Debugger-agent-lldb/src/main/py/LICENSE
new file mode 100644
index 0000000000..c026b6b79a
--- /dev/null
+++ b/Ghidra/Debug/Debugger-agent-lldb/src/main/py/LICENSE
@@ -0,0 +1,11 @@
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/Ghidra/Debug/Debugger-agent-lldb/src/main/py/README.md b/Ghidra/Debug/Debugger-agent-lldb/src/main/py/README.md
new file mode 100644
index 0000000000..1d6d83faa7
--- /dev/null
+++ b/Ghidra/Debug/Debugger-agent-lldb/src/main/py/README.md
@@ -0,0 +1,3 @@
+# Ghidra Trace RMI
+
+Package for connecting LLDB to Ghidra via Trace RMI.
\ No newline at end of file
diff --git a/Ghidra/Debug/Debugger-agent-lldb/src/main/py/ghidralldb/__init__.py b/Ghidra/Debug/Debugger-agent-lldb/src/main/py/ghidralldb/__init__.py
new file mode 100644
index 0000000000..7e7e1e1053
--- /dev/null
+++ b/Ghidra/Debug/Debugger-agent-lldb/src/main/py/ghidralldb/__init__.py
@@ -0,0 +1,16 @@
+## ###
+# IP: GHIDRA
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+from . import util, commands
diff --git a/Ghidra/Debug/Debugger-agent-lldb/src/main/py/ghidralldb/arch.py b/Ghidra/Debug/Debugger-agent-lldb/src/main/py/ghidralldb/arch.py
new file mode 100644
index 0000000000..d4055719b3
--- /dev/null
+++ b/Ghidra/Debug/Debugger-agent-lldb/src/main/py/ghidralldb/arch.py
@@ -0,0 +1,261 @@
+## ###
+# IP: GHIDRA
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+from ghidratrace.client import Address, RegVal
+
+import lldb
+
+from . import util
+
+# NOTE: This map is derived from the ldefs using a script
+language_map = {
+ 'aarch64': ['AARCH64:BE:64:v8A', 'AARCH64:LE:64:AppleSilicon', 'AARCH64:LE:64:v8A'],
+ 'armv7': ['ARM:BE:32:v7', 'ARM:LE:32:v7'],
+ 'armv7k': ['ARM:BE:32:v7', 'ARM:LE:32:v7'],
+ 'armv7s': ['ARM:BE:32:v7', 'ARM:LE:32:v7'],
+ 'arm64': ['ARM:BE:64:v8', 'ARM:LE:64:v8'],
+ 'arm64_32': ['ARM:BE:32:v8', 'ARM:LE:32:v8'],
+ 'arm64e': ['ARM:BE:64:v8', 'ARM:LE:64:v8'],
+ 'i386': ['x86:LE:32:default'],
+ 'thumbv7': ['ARM:BE:32:v7', 'ARM:LE:32:v7'],
+ 'thumbv7k': ['ARM:BE:32:v7', 'ARM:LE:32:v7'],
+ 'thumbv7s': ['ARM:BE:32:v7', 'ARM:LE:32:v7'],
+ 'x86_64': ['x86:LE:64:default'],
+ 'wasm32': ['x86:LE:64:default'],
+}
+
+data64_compiler_map = {
+ None: 'pointer64',
+}
+
+x86_compiler_map = {
+ 'freebsd': 'gcc',
+ 'linux': 'gcc',
+ 'netbsd': 'gcc',
+ 'ps4': 'gcc',
+ 'ios': 'clang',
+ 'macosx': 'clang',
+ 'tvos': 'clang',
+ 'watchos': 'clang',
+ 'windows': 'Visual Studio',
+ # This may seem wrong, but Ghidra cspecs really describe the ABI
+ 'Cygwin': 'Visual Studio',
+}
+
+compiler_map = {
+ 'DATA:BE:64:default': data64_compiler_map,
+ 'DATA:LE:64:default': data64_compiler_map,
+ 'x86:LE:32:default': x86_compiler_map,
+ 'x86:LE:64:default': x86_compiler_map,
+}
+
+
+def get_arch():
+ triple = util.get_target().triple
+ if triple is None:
+ return "x86_64"
+ return triple.split('-')[0]
+
+
+def get_endian():
+ parm = util.get_convenience_variable('endian')
+ if parm != 'auto':
+ return parm
+ # Once again, we have to hack using the human-readable 'show'
+ order = util.get_target().GetByteOrder()
+ if order is lldb.eByteOrderLittle:
+ return 'little'
+ if order is lldb.eByteOrderBig:
+ return 'big'
+ if order is lldb.eByteOrderPDP:
+ return 'pdp'
+ return 'unrecognized'
+
+
+def get_osabi():
+ parm = util.get_convenience_variable('osabi')
+ if not parm in ['auto', 'default']:
+ return parm
+ # We have to hack around the fact the LLDB won't give us the current OS ABI
+ # via the API if it is "auto" or "default". Using "show", we can get it, but
+ # we have to parse output meant for a human. The current value will be on
+ # the top line, delimited by double quotes. It will be the last delimited
+ # thing on that line. ("auto" may appear earlier on the line.)
+ triple = util.get_target().triple
+ # this is an unfortunate feature of the tests
+ if triple is None:
+ return "linux"
+ return triple.split('-')[2]
+
+
+def compute_ghidra_language():
+ # First, check if the parameter is set
+ lang = util.get_convenience_variable('ghidra-language')
+ if lang != 'auto':
+ return lang
+
+ # Get the list of possible languages for the arch. We'll need to sift
+ # through them by endian and probably prefer default/simpler variants. The
+ # heuristic for "simpler" will be 'default' then shortest variant id.
+ arch = get_arch()
+ endian = get_endian()
+ lebe = ':BE:' if endian == 'big' else ':LE:'
+ if not arch in language_map:
+ return 'DATA' + lebe + '64:default'
+ langs = language_map[arch]
+ matched_endian = sorted(
+ (l for l in langs if lebe in l),
+ key=lambda l: 0 if l.endswith(':default') else len(l)
+ )
+ if len(matched_endian) > 0:
+ return matched_endian[0]
+ # NOTE: I'm disinclined to fall back to a language match with wrong endian.
+ return 'DATA' + lebe + '64:default'
+
+
+def compute_ghidra_compiler(lang):
+ # First, check if the parameter is set
+ comp = util.get_convenience_variable('ghidra-compiler')
+ if comp != 'auto':
+ return comp
+
+ # Check if the selected lang has specific compiler recommendations
+ if not lang in compiler_map:
+ return 'default'
+ comp_map = compiler_map[lang]
+ osabi = get_osabi()
+ if osabi in comp_map:
+ return comp_map[osabi]
+ if None in comp_map:
+ return comp_map[None]
+ return 'default'
+
+
+def compute_ghidra_lcsp():
+ lang = compute_ghidra_language()
+ comp = compute_ghidra_compiler(lang)
+ return lang, comp
+
+
+class DefaultMemoryMapper(object):
+
+ def __init__(self, defaultSpace):
+ self.defaultSpace = defaultSpace
+
+ def map(self, proc: lldb.SBProcess, offset: int):
+ space = self.defaultSpace
+ return self.defaultSpace, Address(space, offset)
+
+ def map_back(self, proc: lldb.SBProcess, address: Address) -> int:
+ if address.space == self.defaultSpace:
+ return address.offset
+ raise ValueError(f"Address {address} is not in process {proc.GetProcessID()}")
+
+
+DEFAULT_MEMORY_MAPPER = DefaultMemoryMapper('ram')
+
+memory_mappers = {}
+
+
+def compute_memory_mapper(lang):
+ if not lang in memory_mappers:
+ return DEFAULT_MEMORY_MAPPER
+ return memory_mappers[lang]
+
+
+class DefaultRegisterMapper(object):
+
+ def __init__(self, byte_order):
+ if not byte_order in ['big', 'little']:
+ raise ValueError("Invalid byte_order: {}".format(byte_order))
+ self.byte_order = byte_order
+ self.union_winners = {}
+
+ def map_name(self, proc, name):
+ return name
+
+ """
+ def convert_value(self, value, type=None):
+ if type is None:
+ type = value.dynamic_type.strip_typedefs()
+ l = type.sizeof
+ # l - 1 because array() takes the max index, inclusive
+ # NOTE: Might like to pre-lookup 'unsigned char', but it depends on the
+ # architecture *at the time of lookup*.
+ cv = value.cast(lldb.lookup_type('unsigned char').array(l - 1))
+ rng = range(l)
+ if self.byte_order == 'little':
+ rng = reversed(rng)
+ return bytes(cv[i] for i in rng)
+ """
+
+ def map_value(self, proc, name, value):
+ try:
+ ### TODO: this seems half-baked
+ av = value.to_bytes(8, "big")
+ except e:
+ raise ValueError("Cannot convert {}'s value: '{}', type: '{}'"
+ .format(name, value, value.type))
+ return RegVal(self.map_name(proc, name), av)
+
+ def map_name_back(self, proc, name):
+ return name
+
+ def map_value_back(self, proc, name, value):
+ return RegVal(self.map_name_back(proc, name), value)
+
+
+class Intel_x86_64_RegisterMapper(DefaultRegisterMapper):
+
+ def __init__(self):
+ super().__init__('little')
+
+ def map_name(self, proc, name):
+ if name is None:
+ return 'UNKNOWN'
+ if name == 'eflags':
+ return 'rflags'
+ if name.startswith('zmm'):
+ # Ghidra only goes up to ymm, right now
+ return 'ymm' + name[3:]
+ return super().map_name(proc, name)
+
+ def map_value(self, proc, name, value):
+ rv = super().map_value(proc, name, value)
+ if rv.name.startswith('ymm') and len(rv.value) > 32:
+ return RegVal(rv.name, rv.value[-32:])
+ return rv
+
+ def map_name_back(self, proc, name):
+ if name == 'rflags':
+ return 'eflags'
+
+
+DEFAULT_BE_REGISTER_MAPPER = DefaultRegisterMapper('big')
+DEFAULT_LE_REGISTER_MAPPER = DefaultRegisterMapper('little')
+
+register_mappers = {
+ 'x86:LE:64:default': Intel_x86_64_RegisterMapper()
+}
+
+
+def compute_register_mapper(lang):
+ if not lang in register_mappers:
+ if ':BE:' in lang:
+ return DEFAULT_BE_REGISTER_MAPPER
+ if ':LE:' in lang:
+ return DEFAULT_LE_REGISTER_MAPPER
+ return register_mappers[lang]
+
diff --git a/Ghidra/Debug/Debugger-agent-lldb/src/main/py/ghidralldb/commands.py b/Ghidra/Debug/Debugger-agent-lldb/src/main/py/ghidralldb/commands.py
new file mode 100644
index 0000000000..b35857b5dc
--- /dev/null
+++ b/Ghidra/Debug/Debugger-agent-lldb/src/main/py/ghidralldb/commands.py
@@ -0,0 +1,1487 @@
+## ###
+# IP: GHIDRA
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+from contextlib import contextmanager
+import inspect
+import os.path
+import socket
+import time
+import sys
+
+from ghidratrace import sch
+from ghidratrace.client import Client, Address, AddressRange, TraceObject
+import psutil
+
+import lldb
+
+from . import arch, hooks, methods, util
+
+PAGE_SIZE = 4096
+
+DEFAULT_REGISTER_BANK = "General Purpose Registers"
+
+AVAILABLES_PATH = 'Available'
+AVAILABLE_KEY_PATTERN = '[{pid}]'
+AVAILABLE_PATTERN = AVAILABLES_PATH + AVAILABLE_KEY_PATTERN
+BREAKPOINTS_PATH = 'Breakpoints'
+BREAKPOINT_KEY_PATTERN = '[{breaknum}]'
+BREAKPOINT_PATTERN = BREAKPOINTS_PATH + BREAKPOINT_KEY_PATTERN
+WATCHPOINTS_PATH = 'Watchpoints'
+WATCHPOINT_KEY_PATTERN = '[{watchnum}]'
+WATCHPOINT_PATTERN = WATCHPOINTS_PATH + WATCHPOINT_KEY_PATTERN
+BREAK_LOC_KEY_PATTERN = '[{locnum}]'
+PROCESSES_PATH = 'Processes'
+PROCESS_KEY_PATTERN = '[{procnum}]'
+PROCESS_PATTERN = PROCESSES_PATH + PROCESS_KEY_PATTERN
+PROC_WATCHES_PATTERN = PROCESS_PATTERN + '.Watchpoints'
+PROC_WATCH_KEY_PATTERN = PROC_WATCHES_PATTERN + '[{watchnum}]'
+PROC_BREAKS_PATTERN = PROCESS_PATTERN + '.Breakpoints'
+PROC_BREAK_KEY_PATTERN = '[{breaknum}.{locnum}]'
+ENV_PATTERN = PROCESS_PATTERN + '.Environment'
+THREADS_PATTERN = PROCESS_PATTERN + '.Threads'
+THREAD_KEY_PATTERN = '[{tnum}]'
+THREAD_PATTERN = THREADS_PATTERN + THREAD_KEY_PATTERN
+STACK_PATTERN = THREAD_PATTERN + '.Stack'
+FRAME_KEY_PATTERN = '[{level}]'
+FRAME_PATTERN = STACK_PATTERN + FRAME_KEY_PATTERN
+REGS_PATTERN = FRAME_PATTERN + '.Registers'
+BANK_PATTERN = REGS_PATTERN + '.{bank}'
+MEMORY_PATTERN = PROCESS_PATTERN + '.Memory'
+REGION_KEY_PATTERN = '[{start:08x}]'
+REGION_PATTERN = MEMORY_PATTERN + REGION_KEY_PATTERN
+MODULES_PATTERN = PROCESS_PATTERN + '.Modules'
+MODULE_KEY_PATTERN = '[{modpath}]'
+MODULE_PATTERN = MODULES_PATTERN + MODULE_KEY_PATTERN
+SECTIONS_ADD_PATTERN = '.Sections'
+SECTION_KEY_PATTERN = '[{secname}]'
+SECTION_ADD_PATTERN = SECTIONS_ADD_PATTERN + SECTION_KEY_PATTERN
+
+# TODO: Symbols
+
+
+class State(object):
+
+ def __init__(self):
+ self.reset_client()
+
+ def require_client(self):
+ if self.client is None:
+ raise RuntimeError("Not connected")
+ return self.client
+
+ def require_no_client(self):
+ if self.client is not None:
+ raise RuntimeError("Already connected")
+
+ def reset_client(self):
+ self.client = None
+ self.reset_trace()
+
+ def require_trace(self):
+ if self.trace is None:
+ raise RuntimeError("No trace active")
+ return self.trace
+
+ def require_no_trace(self):
+ if self.trace is not None:
+ raise RuntimeError("Trace already started")
+
+ def reset_trace(self):
+ self.trace = None
+ util.set_convenience_variable('_ghidra_tracing', "false")
+ self.reset_tx()
+
+ def require_tx(self):
+ if self.tx is None:
+ raise RuntimeError("No transaction")
+ return self.tx
+
+ def require_no_tx(self):
+ if self.tx is not None:
+ raise RuntimeError("Transaction already started")
+
+ def reset_tx(self):
+ self.tx = None
+
+
+STATE = State()
+
+if __name__ == '__main__':
+ lldb.SBDebugger.InitializeWithErrorHandling();
+ lldb.debugger = lldb.SBDebugger.Create()
+elif lldb.debugger:
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_connect "ghidra_trace_connect"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_listen "ghidra_trace_listen"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_disconnect "ghidra_trace_disconnect"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_start "ghidra_trace_start"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_stop "ghidra_trace_stop"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_restart "ghidra_trace_restart"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_info "ghidra_trace_info"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_info_lcsp "ghidra_trace_info_lcsp"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_txstart "ghidra_trace_txstart"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_txcommit "ghidra_trace_txcommit"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_txabort "ghidra_trace_txabort"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_txopen "ghidra_trace_txopen"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_save "ghidra_trace_save"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_new_snap "ghidra_trace_new_snap"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_set_snap "ghidra_trace_set_snap"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_putmem "ghidra_trace_putmem"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_putval "ghidra_trace_putval"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_putmem_state "ghidra_trace_putmem_state"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_delmem "ghidra_trace_delmem"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_putreg "ghidra_trace_putreg"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_delreg "ghidra_trace_delreg"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_create_obj "ghidra_trace_create_obj"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_insert_obj "ghidra_trace_insert_obj"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_remove_obj "ghidra_trace_remove_obj"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_set_value "ghidra_trace_set_value"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_retain_values "ghidra_trace_retain_values"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_get_obj "ghidra_trace_get_obj"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_get_values "ghidra_trace_get_values"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_get_values_rng "ghidra_trace_get_values_rng"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_activate "ghidra_trace_activate"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_disassemble "ghidra_trace_disassemble"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_put_processes "ghidra_trace_put_processes"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_put_available "ghidra_trace_put_available"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_put_breakpoints "ghidra_trace_put_breakpoints"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_put_watchpoints "ghidra_trace_put_watchpoints"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_put_environment "ghidra_trace_put_environment"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_put_regions "ghidra_trace_put_regions"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_put_modules "ghidra_trace_put_modules"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_put_threads "ghidra_trace_put_threads"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_put_frames "ghidra_trace_put_frames"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_put_all "ghidra_trace_put_all"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_install_hooks "ghidra_trace_install_hooks"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_remove_hooks "ghidra_trace_remove_hooks"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_sync_enable "ghidra_trace_sync_enable"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_sync_disable "ghidra_trace_sync_disable"')
+ lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_util_mark "_mark_"')
+ #lldb.debugger.HandleCommand('target stop-hook add -P ghidralldb.hooks.StopHook')
+ lldb.debugger.SetAsync(True)
+ print("Commands loaded.")
+
+def ghidra_trace_connect(debugger, command, result, internal_dict):
+ """
+ Connect LLDB to Ghidra for tracing
+
+ Address must be of the form 'host:port'
+ """
+
+ STATE.require_no_client()
+ address = command if len(command) > 0 else None
+ if address is None:
+ raise RuntimeError("'ghidra_trace_connect': missing required argument 'address'")
+
+ parts = address.split(':')
+ if len(parts) != 2:
+ raise RuntimeError("address must be in the form 'host:port'")
+ host, port = parts
+ try:
+ c = socket.socket()
+ c.connect((host, int(port)))
+ STATE.client = Client(c, methods.REGISTRY)
+ except ValueError:
+ raise RuntimeError("port must be numeric")
+
+
+def ghidra_trace_listen(debugger, command, result, internal_dict):
+ """
+ Listen for Ghidra to connect for tracing
+
+ Takes an optional address for the host and port on which to listen. Either
+ the form 'host:port' or just 'port'. If omitted, it will bind to an
+ ephemeral port on all interfaces. If only the port is given, it will bind to
+ that port on all interfaces. This command will block until the connection is
+ established.
+ """
+
+ STATE.require_no_client()
+ address = command if len(command) > 0 else None
+ if address is not None:
+ parts = address.split(':')
+ if len(parts) == 1:
+ host, port = '0.0.0.0', parts[0]
+ elif len(parts) == 2:
+ host, port = parts
+ else:
+ raise RuntimeError("address must be 'port' or 'host:port'")
+ else:
+ host, port = '0.0.0.0', 0
+ try:
+ s = socket.socket()
+ s.bind((host, int(port)))
+ host, port = s.getsockname()
+ s.listen(1)
+ print("Listening at {}:{}...\n".format(host, port))
+ c, (chost, cport) = s.accept()
+ s.close()
+ print("Connection from {}:{}\n".format(chost, cport))
+ STATE.client = Client(c, methods.REGISTRY)
+ except ValueError:
+ raise RuntimeError("port must be numeric")
+
+
+def ghidra_trace_disconnect(debugger, command, result, internal_dict):
+ """Disconnect LLDB from Ghidra for tracing"""
+
+ STATE.require_client().close()
+ STATE.reset_client()
+
+
+def compute_name():
+ target = lldb.debugger.GetTargetAtIndex(0)
+ progname = target.executable.basename
+ if progname is None:
+ return 'lldb/noname'
+ else:
+ return 'lldb/' + progname.split('/')[-1]
+
+
+def start_trace(name):
+ language, compiler = arch.compute_ghidra_lcsp()
+ STATE.trace = STATE.client.create_trace(name, language, compiler)
+ # TODO: Is adding an attribute like this recommended in Python?
+ STATE.trace.memory_mapper = arch.compute_memory_mapper(language)
+ STATE.trace.register_mapper = arch.compute_register_mapper(language)
+
+ parent = os.path.dirname(inspect.getfile(inspect.currentframe()))
+ schema_fn = os.path.join(parent, 'schema.xml')
+ with open(schema_fn, 'r') as schema_file:
+ schema_xml = schema_file.read()
+ with STATE.trace.open_tx("Create Root Object"):
+ root = STATE.trace.create_root_object(schema_xml, 'Session')
+ root.set_value('_display', 'GNU lldb ' + util.LLDB_VERSION.full)
+ util.set_convenience_variable('_ghidra_tracing', "true")
+
+
+def ghidra_trace_start(debugger, command, result, internal_dict):
+ """Start a Trace in Ghidra"""
+
+ STATE.require_client()
+ name = command if len(command) > 0 else compute_name()
+ #if name is None:
+ # name = compute_name()
+ STATE.require_no_trace()
+ start_trace(name)
+
+
+def ghidra_trace_stop(debugger, command, result, internal_dict):
+ """Stop the Trace in Ghidra"""
+
+ STATE.require_trace().close()
+ STATE.reset_trace()
+
+
+def ghidra_trace_restart(debugger, command, result, internal_dict):
+ """Restart or start the Trace in Ghidra"""
+
+ STATE.require_client()
+ if STATE.trace is not None:
+ STATE.trace.close()
+ STATE.reset_trace()
+ name = command if len(command) > 0 else compute_name()
+ #if name is None:
+ # name = compute_name()
+ start_trace(name)
+
+
+def ghidra_trace_info(debugger, command, result, internal_dict):
+ """Get info about the Ghidra connection"""
+
+ result = {}
+ if STATE.client is None:
+ print("Not connected to Ghidra\n")
+ return
+ host, port = STATE.client.s.getpeername()
+ print("Connected to Ghidra at {}:{}\n".format(host, port))
+ if STATE.trace is None:
+ print("No trace\n")
+ return
+ print("Trace active\n")
+ return result
+
+
+def ghidra_trace_info_lcsp(debugger, command, result, internal_dict):
+ """
+ Get the selected Ghidra language-compiler-spec pair. Even when
+ 'show ghidra language' is 'auto' and/or 'show ghidra compiler' is 'auto',
+ this command provides the current actual language and compiler spec.
+ """
+
+ language, compiler = arch.compute_ghidra_lcsp()
+ print("Selected Ghidra language: {}\n".format(language))
+ print("Selected Ghidra compiler: {}\n".format(compiler))
+
+
+def ghidra_trace_txstart(debugger, command, result, internal_dict):
+ """
+ Start a transaction on the trace
+ """
+
+ description = command
+ STATE.require_no_tx()
+ STATE.tx = STATE.require_trace().start_tx(description, undoable=False)
+
+
+def ghidra_trace_txcommit(debugger, command, result, internal_dict):
+ """
+ Commit the current transaction
+ """
+
+ STATE.require_tx().commit()
+ STATE.reset_tx()
+
+
+def ghidra_trace_txabort(debugger, command, result, internal_dict):
+ """
+ Abort the current transaction
+
+ Use only in emergencies.
+ """
+
+ tx = STATE.require_tx()
+ print("Aborting trace transaction!\n")
+ tx.abort()
+ STATE.reset_tx()
+
+
+@contextmanager
+def open_tracked_tx(description):
+ with STATE.require_trace().open_tx(description) as tx:
+ STATE.tx = tx
+ yield tx
+ STATE.reset_tx()
+
+
+def ghidra_trace_txopen(debugger, command, result, internal_dict):
+ """
+ Run a command with an open transaction
+
+ If possible, use this in the following idiom to ensure your transactions
+ are closed:
+
+ define my-cmd
+ ghidra_trace_put...
+ ghidra_trace_put...
+ end
+ ghidra_trace_tx-open "My tx" "my-cmd"
+
+ If you instead do:
+
+ ghidra_trace_tx-start "My tx"
+ ghidra_trace_put...
+ ghidra_trace_put...
+ ghidra_trace_tx-commit
+
+ and something goes wrong with one of the puts, the transaction may never be
+ closed, leading to further crashes when trying to start a new transaction.
+ """
+
+ items = command.split(" ");
+ description = items[0]
+ command = items[1]
+ with open_tracked_tx(description):
+ lldb.debugger.HandleCommand(command);
+
+
+def ghidra_trace_save(debugger, command, result, internal_dict):
+ """
+ Save the current trace
+ """
+
+ STATE.require_trace().save()
+
+
+def ghidra_trace_new_snap(debugger, command, result, internal_dict):
+ """
+ Create a new snapshot
+
+ Subsequent modifications to machine state will affect the new snapshot.
+ """
+
+ description = str(command)
+ STATE.require_tx()
+ return {'snap': STATE.require_trace().snapshot(description)}
+
+# TODO: A convenience var for the current snapshot
+# Will need to update it on:
+# ghidra_trace_snapshot/set-snap
+# process ? (only if per-process tracing.... I don't think I'm doing that.)
+# ghidra_trace_trace start/stop/restart
+
+
+def ghidra_trace_set_snap(debugger, command, result, internal_dict):
+ """
+ Go to a snapshot
+
+ Subsequent modifications to machine state will affect the given snapshot.
+ """
+
+ snap = command
+ eval = util.get_eval(snap)
+ if eval.IsValid():
+ snap = eval.GetValueAsUnsigned()
+
+ STATE.require_trace().set_snap(int(snap))
+
+
+def put_bytes(start, end, pages, from_tty):
+ trace = STATE.require_trace()
+ if pages:
+ start = start // PAGE_SIZE * PAGE_SIZE
+ end = (end + PAGE_SIZE - 1) // PAGE_SIZE * PAGE_SIZE
+ proc = util.get_process()
+ error = lldb.SBError()
+ if end - start <= 0:
+ return {'count': 0}
+ buf = proc.ReadMemory(start, end - start, error)
+
+ count = 0
+ if error.Success() and buf is not None:
+ base, addr = trace.memory_mapper.map(proc, start)
+ if base != addr.space:
+ trace.create_overlay_space(base, addr.space)
+ count = trace.put_bytes(addr, buf)
+ if from_tty:
+ print("Wrote {} bytes\n".format(count))
+ return {'count': count}
+
+
+def eval_address(address):
+ try:
+ return util.parse_and_eval(address)
+ except e:
+ raise RuntimeError("Cannot convert '{}' to address".format(address))
+
+
+def eval_range(address, length):
+ start = eval_address(address)
+ try:
+ end = start + util.parse_and_eval(length)
+ except e:
+ raise RuntimeError("Cannot convert '{}' to length".format(length))
+ return start, end
+
+
+def putmem(address, length, pages=True, from_tty=True):
+ start, end = eval_range(address, length)
+ return put_bytes(start, end, pages, from_tty)
+
+
+def ghidra_trace_putmem(debugger, command, result, internal_dict):
+ """
+ Record the given block of memory into the Ghidra trace.
+ """
+
+ items = command.split(" ")
+ address = items[0]
+ length = items[1]
+ pages = items[2] if len(items) > 2 else True
+
+ STATE.require_tx()
+ return putmem(address, length, pages, True)
+
+
+def ghidra_trace_putval(debugger, command, result, internal_dict):
+ """
+ Record the given value into the Ghidra trace, if it's in memory.
+ """
+
+ items = command.split(" ")
+ value = items[0]
+ pages = items[1] if len(items) > 1 else True
+
+ STATE.require_tx()
+ try:
+ start = util.parse_and_eval(value)
+ except e:
+ raise RuntimeError("Value '{}' has no address".format(value))
+ end = start + int(start.GetType().GetByteSize())
+ return put_bytes(start, end, pages, True)
+
+
+def ghidra_trace_putmem_state(debugger, command, result, internal_dict):
+ """
+ Set the state of the given range of memory in the Ghidra trace.
+ """
+
+ items = command.split(" ")
+ address = items[0]
+ length = items[1]
+ state = items[2]
+
+ STATE.require_tx()
+ STATE.trace.validate_state(state)
+ start, end = eval_range(address, length)
+ proc = util.get_process()
+ base, addr = STATE.trace.memory_mapper.map(proc, start)
+ if base != addr.space:
+ trace.create_overlay_space(base, addr.space)
+ STATE.trace.set_memory_state(addr.extend(end - start), state)
+
+
+def ghidra_trace_delmem(debugger, command, result, internal_dict):
+ """
+ Delete the given range of memory from the Ghidra trace.
+
+ Why would you do this? Keep in mind putmem quantizes to full pages by
+ default, usually to take advantage of spatial locality. This command does
+ not quantize. You must do that yourself, if necessary.
+ """
+
+ items = command.split(" ")
+ address = items[0]
+ length = items[1]
+
+ STATE.require_tx()
+ start, end = eval_range(address, length)
+ proc = util.get_process()
+ base, addr = STATE.trace.memory_mapper.map(proc, start)
+ # Do not create the space. We're deleting stuff.
+ STATE.trace.delete_bytes(addr.extend(end - start))
+
+
+def putreg(frame, bank):
+ proc = util.get_process()
+ space = REGS_PATTERN.format(procnum=proc.GetProcessID(), tnum=util.selected_thread().GetThreadID(),
+ level=frame.GetFrameID())
+ subspace = BANK_PATTERN.format(procnum=proc.GetProcessID(), tnum=util.selected_thread().GetThreadID(),
+ level=frame.GetFrameID(), bank=bank.name)
+ STATE.trace.create_overlay_space('register', space)
+ robj = STATE.trace.create_object(space)
+ robj.insert()
+ bobj = STATE.trace.create_object(subspace)
+ bobj.insert()
+ mapper = STATE.trace.register_mapper
+ values = []
+ for i in range(0, bank.GetNumChildren()):
+ item = bank.GetChildAtIndex(i, lldb.eDynamicCanRunTarget, True)
+ values.append(mapper.map_value(proc, item.GetName(), item.GetValueAsUnsigned()))
+ bobj.set_value(item.GetName(), hex(item.GetValueAsUnsigned()))
+ # TODO: Memorize registers that failed for this arch, and omit later.
+ return {'missing': STATE.trace.put_registers(space, values)}
+
+
+def ghidra_trace_putreg(debugger, command, result, internal_dict):
+ """
+ Record the given register group for the current frame into the Ghidra trace.
+
+ If no group is specified, 'all' is assumed.
+ """
+
+ group = command if len(command) > 0 else 'all'
+
+ STATE.require_tx()
+ frame = util.selected_frame()
+ regs = frame.GetRegisters()
+ if group is not 'all':
+ bank = regs.GetFirstValueByName(group)
+ return putreg(frame, bank)
+
+ for i in range(0, regs.GetSize()):
+ bank = regs.GetValueAtIndex(i)
+ putreg(frame, bank)
+
+
+def ghidra_trace_delreg(debugger, command, result, internal_dict):
+ """
+ Delete the given register group for the curent frame from the Ghidra trace.
+
+ Why would you do this? If no group is specified, 'all' is assumed.
+ """
+
+ group = command if len(command) > 0 else 'all'
+
+ STATE.require_tx()
+ proc = util.get_process()
+ frame = util.selected_frame()
+ space = 'Processes[{}].Threads[{}].Stack[{}].Registers'.format(
+ proc.GetProcessID(), util.selected_thread().GetThreadID(), frame.GetFrameID()
+ )
+ mapper = STATE.trace.register_mapper
+ names = []
+ for desc in frame.registers:
+ names.append(mapper.map_name(proc, desc.name))
+ return STATE.trace.delete_registers(space, names)
+
+
+def ghidra_trace_create_obj(debugger, command, result, internal_dict):
+ """
+ Create an object in the Ghidra trace.
+
+ The new object is in a detached state, so it may not be immediately
+ recognized by the Debugger GUI. Use 'ghidra_trace_insert-obj' to finish the
+ object, after all its required attributes are set.
+ """
+
+ path = command
+
+ STATE.require_tx()
+ obj = STATE.trace.create_object(path)
+ obj.insert()
+ print("Created object: id={}, path='{}'\n".format(obj.id, obj.path))
+ return {'id': obj.id, 'path': obj.path}
+
+
+def ghidra_trace_insert_obj(debugger, command, result, internal_dict):
+ """
+ Insert an object into the Ghidra trace.
+ """
+
+ path = command
+
+ # NOTE: id parameter is probably not necessary, since this command is for
+ # humans.
+ STATE.require_tx()
+ span = STATE.trace.proxy_object_path(path).insert()
+ print("Inserted object: lifespan={}\n".format(span))
+ return {'lifespan': span}
+
+
+def ghidra_trace_remove_obj(debugger, command, result, internal_dict):
+ """
+ Remove an object from the Ghidra trace.
+
+ This does not delete the object. It just removes it from the tree for the
+ current snap and onwards.
+ """
+
+ path = command
+
+ # NOTE: id parameter is probably not necessary, since this command is for
+ # humans.
+ STATE.require_tx()
+ STATE.trace.proxy_object_path(path).remove()
+
+
+def to_bytes(value, type):
+ n = value.GetNumChildren()
+ return bytes(int(value.GetChildAtIndex(i).GetValueAsUnsigned()) for i in range(0,n))
+
+
+def to_string(value, type, encoding, full):
+ n = value.GetNumChildren()
+ b = bytes(int(value.GetChildAtIndex(i).GetValueAsUnsigned()) for i in range(0,n))
+ return str(b, encoding)
+
+
+def to_bool_list(value, type):
+ n = value.GetNumChildren()
+ return [bool(int(value.GetChildAtIndex(i).GetValueAsUnsigned())) for i in range(0,n)]
+
+
+def to_int_list(value, type):
+ n = value.GetNumChildren()
+ return [int(value.GetChildAtIndex(i).GetValueAsUnsigned()) for i in range(0,n)]
+
+
+def to_short_list(value, type):
+ n = value.GetNumChildren()
+ return [int(value.GetChildAtIndex(i).GetValueAsUnsigned()) for i in range(0,n)]
+
+
+def eval_value(value, schema=None):
+ val = util.get_eval(value)
+ type = val.GetType()
+ while type.IsTypedefType():
+ type = type.GetTypedefedType()
+
+ code = type.GetBasicType()
+ if code == lldb.eBasicTypeVoid:
+ return None, sch.VOID
+ if code == lldb.eBasicTypeChar or code == lldb.eBasicTypeSignedChar or code == lldb.eBasicTypeUnsignedChar:
+ if not "\\x" in val.GetValue():
+ return int(val.GetValueAsUnsigned()), sch.CHAR
+ return int(val.GetValueAsUnsigned()), sch.BYTE
+ if code == lldb.eBasicTypeShort or code == lldb.eBasicTypeUnsignedShort:
+ return int(val.GetValue()), sch.SHORT
+ if code == lldb.eBasicTypeInt or code == lldb.eBasicTypeUnsignedInt:
+ return int(val.GetValue()), sch.INT
+ if code == lldb.eBasicTypeLong or code == lldb.eBasicTypeUnsignedLong:
+ return int(val.GetValue()), sch.LONG
+ if code == lldb.eBasicTypeLongLong or code == lldb.eBasicTypeUnsignedLongLong:
+ return int(val.GetValue()), sch.LONG
+ if code == lldb.eBasicTypeBool:
+ return bool(val.GetValue()), sch.BOOL
+
+ # TODO: This seems like a bit of a hack
+ type_name = type.GetName()
+ if type_name.startswith("const char["):
+ return val.GetSummary(), sch.STRING
+ if type_name.startswith("const wchar_t["):
+ return val.GetSummary(), sch.STRING
+
+ if type.IsArrayType():
+ etype = type.GetArrayElementType()
+ while etype.IsTypedefType():
+ etype = etype.GetTypedefedType()
+ ecode = etype.GetBasicType()
+ if ecode == lldb.eBasicTypeBool:
+ return to_bool_list(val, type), sch.BOOL_ARR
+ elif ecode == lldb.eBasicTypeChar or ecode == lldb.eBasicTypeSignedChar or ecode == lldb.eBasicTypeUnsignedChar:
+ if schema == sch.BYTE_ARR:
+ return to_bytes(val, type), schema
+ elif schema == sch.CHAR_ARR:
+ return to_string(val, type, 'utf-8', full=True), schema
+ return to_string(val, type, 'utf-8', full=False), sch.STRING
+ elif ecode == lldb.eBasicTypeShort or ecode == lldb.eBasicTypeUnsignedShort:
+ if schema is None:
+ if etype.name == 'wchar_t':
+ return to_string(val, type, 'utf-16', full=False), sch.STRING
+ schema = sch.SHORT_ARR
+ elif schema == sch.CHAR_ARR:
+ return to_string(val, type, 'utf-16', full=True), schema
+ return to_int_list(val, type), schema
+ elif ecode == lldb.eBasicTypeSignedWChar or ecode == lldb.eBasicTypeUnsignedWChar:
+ if schema is not None and schema != sch.CHAR_ARR:
+ return to_short_list(val, type), schema
+ else:
+ return to_string(val, type, 'utf-16', full=False), sch.STRING
+ elif ecode == lldb.eBasicTypeInt or ecode == lldb.eBasicTypeUnsignedInt:
+ if schema is None:
+ if etype.name == 'wchar_t':
+ return to_string(val, type, 'utf-32', full=False), sch.STRING
+ schema = sch.INT_ARR
+ elif schema == sch.CHAR_ARR:
+ return to_string(val, type, 'utf-32', full=True), schema
+ return to_int_list(val, type), schema
+ elif ecode == lldb.eBasicTypeLong or ecode == lldb.eBasicTypeUnsignedLong or ecode == lldb.eBasicTypeLongLong or ecode == lldb.eBasicTypeUnsignedLongLong:
+ if schema is not None:
+ return to_int_list(val, type), schema
+ else:
+ return to_int_list(val, type), sch.LONG_ARR
+ elif type.IsPointerType():
+ offset = int(val.GetValue(),16)
+ proc = util.get_process()
+ base, addr = STATE.trace.memory_mapper.map(proc, offset)
+ return (base, addr), sch.ADDRESS
+ raise ValueError(
+ "Cannot convert ({}): '{}', value='{}'".format(schema, value, val))
+
+
+def ghidra_trace_set_value(debugger, command, result, internal_dict):
+ """
+ Set a value (attribute or element) in the Ghidra trace's object tree.
+
+ A void value implies removal. NOTE: The type of an expression may be
+ subject to LLDB's current language. e.g., there is no 'bool' in C. You may
+ have to change to C++ if you need this type. Alternatively, you can use the
+ Python API.
+ """
+
+ # NOTE: id parameter is probably not necessary, since this command is for
+ # humans.
+ # TODO: path and key are two separate parameters.... This is mostly to
+ # spare me from porting path parsing to Python, but it may also be useful
+ # if we ever allow ids here, since the id would be for the object, not the
+ # complete value path.
+
+ items = command.split(" ")
+ path = items[0]
+ key = items[1]
+ value = items[2]
+ if len(items) > 3 and items[3] is not "":
+ schema = items[3]
+ # This is a horrible hack
+ if (value.startswith("\"") or value.startswith("L\"")) and schema.endswith("\""):
+ value = value+" "+schema
+ schema = None
+ else:
+ schema = None
+
+ schema = None if schema is None else sch.Schema(schema)
+ STATE.require_tx()
+ if schema == sch.OBJECT:
+ val = STATE.trace.proxy_object_path(value)
+ else:
+ val, schema = eval_value(value, schema)
+ if schema == sch.ADDRESS:
+ base, addr = val
+ val = addr
+ if base != addr.space:
+ trace.create_overlay_space(base, addr.space)
+ STATE.trace.proxy_object_path(path).set_value(key, val, schema)
+
+
+def ghidra_trace_retain_values(debugger, command, result, internal_dict):
+ """
+ Retain only those keys listed, settings all others to null.
+
+ Takes a list of keys to retain. The first argument may optionally be one of
+ the following:
+
+ --elements To set all other elements to null (default)
+ --attributes To set all other attributes to null
+ --both To set all other values (elements and attributes) to null
+
+ If, for some reason, one of the keys to retain would be mistaken for this
+ switch, then the switch is required. Only the first argument is taken as the
+ switch. All others are taken as keys.
+ """
+
+ items = command.split(" ")
+ path = items[0]
+ keys = items[1:]
+
+ STATE.require_tx()
+ kinds = 'elements'
+ if keys[0] == '--elements':
+ kinds = 'elements'
+ keys = keys[1:]
+ elif keys[0] == '--attributes':
+ kinds = 'attributes'
+ keys = keys[1:]
+ elif keys[0] == '--both':
+ kinds = 'both'
+ keys = keys[1:]
+ elif keys[0].startswith('--'):
+ raise RuntimeError("Invalid argument: " + keys[0])
+ STATE.trace.proxy_object_path(path).retain_values(keys, kinds=kinds)
+
+
+def ghidra_trace_get_obj(debugger, command, result, internal_dict):
+ """
+ Get an object descriptor by its canonical path.
+
+ This isn't the most informative, but it will at least confirm whether an
+ object exists and provide its id.
+ """
+
+ path = command
+
+ trace = STATE.require_trace()
+ object = trace.get_object(path)
+ print("{}\t{}\n".format(object.id, object.path))
+ return object
+
+
+class TableColumn(object):
+ def __init__(self, head):
+ self.head = head
+ self.contents = [head]
+ self.is_last = False
+
+ def add_data(self, data):
+ self.contents.append(str(data))
+
+ def finish(self):
+ self.width = max(len(d) for d in self.contents) + 1
+
+ def print_cell(self, i):
+ print(
+ self.contents[i] if self.is_last else self.contents[i].ljust(self.width))
+
+
+class Tabular(object):
+ def __init__(self, heads):
+ self.columns = [TableColumn(h) for h in heads]
+ self.columns[-1].is_last = True
+ self.num_rows = 1
+
+ def add_row(self, datas):
+ for c, d in zip(self.columns, datas):
+ c.add_data(d)
+ self.num_rows += 1
+
+ def print_table(self):
+ for c in self.columns:
+ c.finish()
+ for rn in range(self.num_rows):
+ for c in self.columns:
+ c.print_cell(rn)
+ print('\n')
+
+
+def val_repr(value):
+ if isinstance(value, TraceObject):
+ return value.path
+ elif isinstance(value, Address):
+ return '{}:{:08x}'.format(value.space, value.offset)
+ return repr(value)
+
+
+def print_values(values):
+ table = Tabular(['Parent', 'Key', 'Span', 'Value', 'Type'])
+ for v in values:
+ table.add_row(
+ [v.parent.path, v.key, v.span, val_repr(v.value), v.schema])
+ table.print_table()
+
+
+def ghidra_trace_get_values(debugger, command, result, internal_dict):
+ """
+ List all values matching a given path pattern.
+ """
+
+ pattern = command
+
+ trace = STATE.require_trace()
+ values = trace.get_values(pattern)
+ print_values(values)
+ return values
+
+
+def ghidra_trace_get_values_rng(debugger, command, result, internal_dict):
+ """
+ List all values intersecting a given address range.
+ """
+
+ items = command.split(" ")
+ address = items[0]
+ length = items[1]
+
+ trace = STATE.require_trace()
+ start, end = eval_range(address, length)
+ proc = util.get_process()
+ base, addr = trace.memory_mapper.map(proc, start)
+ # Do not create the space. We're querying. No tx.
+ values = trace.get_values_intersecting(addr.extend(end - start))
+ print_values(values)
+ return values
+
+
+def activate(path=None):
+ trace = STATE.require_trace()
+ if path is None:
+ proc = util.get_process()
+ t = util.selected_thread()
+ if t is None:
+ path = PROCESS_PATTERN.format(procnum=proc.GetProcessID())
+ else:
+ frame = util.selected_frame()
+ if frame is None:
+ path = THREAD_PATTERN.format(procnum=proc.GetProcessID(), tnum=t.GetThreadID())
+ else:
+ path = FRAME_PATTERN.format(
+ procnum=proc.GetProcessID(), tnum=t.GetThreadID(), level=frame.GetFrameID())
+ trace.proxy_object_path(path).activate()
+
+
+def ghidra_trace_activate(debugger, command, result, internal_dict):
+ """
+ Activate an object in Ghidra's GUI.
+
+ This has no effect if the current trace is not current in Ghidra. If path is
+ omitted, this will activate the current frame.
+ """
+
+ path = command if len(command) > 0 else None
+
+ activate(path)
+
+
+def ghidra_trace_disassemble(debugger, command, result, internal_dict):
+ """
+ Disassemble starting at the given seed.
+
+ Disassembly proceeds linearly and terminates at the first branch or unknown
+ memory encountered.
+ """
+
+ address = command
+
+ STATE.require_tx()
+ start = eval_address(address)
+ proc = util.get_process()
+ base, addr = STATE.trace.memory_mapper.map(proc, start)
+ if base != addr.space:
+ trace.create_overlay_space(base, addr.space)
+
+ length = STATE.trace.disassemble(addr)
+ print("Disassembled {} bytes\n".format(length))
+ return {'length': length}
+
+
+def compute_proc_state(proc = None):
+ if proc.is_running:
+ return 'RUNNING'
+ return 'STOPPED'
+
+
+def put_processes():
+ keys = []
+ proc = util.get_process()
+ ipath = PROCESS_PATTERN.format(procnum=proc.GetProcessID())
+ keys.append(PROCESS_KEY_PATTERN.format(procnum=proc.GetProcessID()))
+ procobj = STATE.trace.create_object(ipath)
+ istate = compute_proc_state(proc)
+ procobj.set_value('_state', istate)
+ procobj.insert()
+ STATE.trace.proxy_object_path(PROCESSES_PATH).retain_values(keys)
+
+def put_state(event_process):
+ STATE.require_no_tx()
+ STATE.tx = STATE.require_trace().start_tx("state", undoable=False)
+ ipath = PROCESS_PATTERN.format(procnum=event_process.GetProcessID())
+ procobj = STATE.trace.create_object(ipath)
+ state = "STOPPED" if event_process.is_stopped else "RUNNING"
+ procobj.set_value('_state', state)
+ procobj.insert()
+ STATE.require_tx().commit()
+ STATE.reset_tx()
+
+
+def ghidra_trace_put_processes(debugger, command, result, internal_dict):
+ """
+ Put the list of processes into the trace's Processes list.
+ """
+
+ STATE.require_tx()
+ with STATE.client.batch() as b:
+ put_processes()
+
+
+def put_available():
+ keys = []
+ for proc in psutil.process_iter():
+ ppath = AVAILABLE_PATTERN.format(pid=proc.pid)
+ procobj = STATE.trace.create_object(ppath)
+ keys.append(AVAILABLE_KEY_PATTERN.format(pid=proc.pid))
+ procobj.set_value('_pid', proc.pid)
+ procobj.set_value('_display', '{} {}'.format(proc.pid, proc.name))
+ procobj.insert()
+ STATE.trace.proxy_object_path(AVAILABLES_PATH).retain_values(keys)
+
+
+def ghidra_trace_put_available(debugger, command, result, internal_dict):
+ """
+ Put the list of available processes into the trace's Available list.
+ """
+
+ STATE.require_tx()
+ with STATE.client.batch() as b:
+ put_available()
+
+
+def put_single_breakpoint(b, ibobj, proc, ikeys):
+ mapper = STATE.trace.memory_mapper
+ bpath = BREAKPOINT_PATTERN.format(breaknum=b.GetID())
+ brkobj = STATE.trace.create_object(bpath)
+ if b.IsHardware():
+ brkobj.set_value('_expression', util.get_description(b))
+ brkobj.set_value('_kinds', 'HW_EXECUTE')
+ else:
+ brkobj.set_value('_expression', util.get_description(b))
+ brkobj.set_value('_kinds', 'SW_EXECUTE')
+ cmdList = lldb.SBStringList()
+ if b.GetCommandLineCommands(cmdList):
+ list = []
+ for i in range(0,cmdList.GetSize()):
+ list.append(cmdList.GetStringAtIndex(i))
+ brkobj.set_value('Commands', list)
+ if b.GetCondition():
+ brkobj.set_value('Condition', b.GetCondition())
+ brkobj.set_value('Hit Count', b.GetHitCount())
+ brkobj.set_value('Ignore Count', b.GetIgnoreCount())
+ brkobj.set_value('Temporary', b.IsOneShot())
+ keys = []
+ locs = util.BREAKPOINT_LOCATION_INFO_READER.get_locations(b)
+ hooks.BRK_STATE.update_brkloc_count(b, len(locs))
+ for i, l in enumerate(locs):
+ # Retain the key, even if not for this process
+ k = BREAK_LOC_KEY_PATTERN.format(locnum=i+1)
+ keys.append(k)
+ locobj = STATE.trace.create_object(bpath + k)
+ ik = PROC_BREAK_KEY_PATTERN.format(breaknum=b.GetID(), locnum=i+1)
+ ikeys.append(ik)
+ if b.location is not None: # Implies execution break
+ base, addr = mapper.map(proc, l.GetLoadAddress())
+ if base != addr.space:
+ STATE.trace.create_overlay_space(base, addr.space)
+ locobj.set_value('_range', addr.extend(1))
+ else: # I guess it's a catchpoint
+ pass
+ locobj.insert()
+ ibobj.set_value(ik, locobj)
+ brkobj.retain_values(keys)
+ brkobj.insert()
+
+def put_single_watchpoint(b, ibobj, proc, ikeys):
+ mapper = STATE.trace.memory_mapper
+ bpath = PROC_WATCH_KEY_PATTERN.format(procnum=proc.GetProcessID(), watchnum=b.GetID())
+ brkobj = STATE.trace.create_object(bpath)
+ desc = util.get_description(b, level=0)
+ brkobj.set_value('_expression', desc)
+ brkobj.set_value('_kinds', 'WRITE')
+ if "type = r" in desc:
+ brkobj.set_value('_kinds', 'READ')
+ if "type = rw" in desc:
+ brkobj.set_value('_kinds', 'READ,WRITE')
+ base, addr = mapper.map(proc, b.GetWatchAddress())
+ if base != addr.space:
+ STATE.trace.create_overlay_space(base, addr.space)
+ brkobj.set_value('_range', addr.extend(b.GetWatchSize()))
+ if b.GetCondition():
+ brkobj.set_value('Condition', b.GetCondition())
+ brkobj.set_value('Hit Count', b.GetHitCount())
+ brkobj.set_value('Ignore Count', b.GetIgnoreCount())
+ brkobj.set_value('Hardware Index', b.GetHardwareIndex())
+ brkobj.set_value('Watch Address', hex(b.GetWatchAddress()))
+ brkobj.set_value('Watch Size', b.GetWatchSize())
+ brkobj.insert()
+
+
+def put_breakpoints():
+ target = util.get_target()
+ proc = util.get_process()
+ ibpath = PROC_BREAKS_PATTERN.format(procnum=proc.GetProcessID())
+ ibobj = STATE.trace.create_object(ibpath)
+ keys = []
+ ikeys = []
+ for i in range(0, target.GetNumBreakpoints()):
+ b = target.GetBreakpointAtIndex(i)
+ keys.append(BREAKPOINT_KEY_PATTERN.format(breaknum=b.GetID()))
+ put_single_breakpoint(b, ibobj, proc, ikeys)
+ ibobj.insert()
+ STATE.trace.proxy_object_path(BREAKPOINTS_PATH).retain_values(keys)
+ ibobj.retain_values(ikeys)
+
+def put_watchpoints():
+ target = util.get_target()
+ proc = util.get_process()
+ ibpath = PROC_WATCHES_PATTERN.format(procnum=proc.GetProcessID())
+ ibobj = STATE.trace.create_object(ibpath)
+ keys = []
+ ikeys = []
+ for i in range(0, target.GetNumWatchpoints()):
+ b = target.GetWatchpointAtIndex(i)
+ keys.append(WATCHPOINT_KEY_PATTERN.format(watchnum=b.GetID()))
+ put_single_watchpoint(b, ibobj, proc, ikeys)
+ ibobj.insert()
+ STATE.trace.proxy_object_path(WATCHPOINTS_PATH).retain_values(keys)
+
+
+def ghidra_trace_put_breakpoints(debugger, command, result, internal_dict):
+ """
+ Put the current process's breakpoints into the trace.
+ """
+
+ STATE.require_tx()
+ with STATE.client.batch() as b:
+ put_breakpoints()
+
+def ghidra_trace_put_watchpoints(debugger, command, result, internal_dict):
+ """
+ Put the current process's watchpoints into the trace.
+ """
+
+ STATE.require_tx()
+ with STATE.client.batch() as b:
+ put_watchpoints()
+
+
+def put_environment():
+ proc = util.get_process()
+ epath = ENV_PATTERN.format(procnum=proc.GetProcessID())
+ envobj = STATE.trace.create_object(epath)
+ envobj.set_value('_debugger', 'lldb')
+ envobj.set_value('_arch', arch.get_arch())
+ envobj.set_value('_os', arch.get_osabi())
+ envobj.set_value('_endian', arch.get_endian())
+ envobj.insert()
+
+
+def ghidra_trace_put_environment(debugger, command, result, internal_dict):
+ """
+ Put some environment indicators into the Ghidra trace
+ """
+
+ STATE.require_tx()
+ with STATE.client.batch() as b:
+ put_environment()
+
+
+def put_regions():
+ proc = util.get_process()
+ try:
+ regions = util.REGION_INFO_READER.get_regions()
+ except Exception:
+ regions = []
+ if len(regions) == 0 and util.selected_thread() is not None:
+ regions = [util.REGION_INFO_READER.full_mem()]
+ mapper = STATE.trace.memory_mapper
+ keys = []
+ for r in regions:
+ rpath = REGION_PATTERN.format(procnum=proc.GetProcessID(), start=r.start)
+ keys.append(REGION_KEY_PATTERN.format(start=r.start))
+ regobj = STATE.trace.create_object(rpath)
+ start_base, start_addr = mapper.map(proc, r.start)
+ if start_base != start_addr.space:
+ STATE.trace.create_overlay_space(start_base, start_addr.space)
+ regobj.set_value('_range', start_addr.extend(r.end - r.start))
+ regobj.set_value('_readable', r.perms == None or 'r' in r.perms)
+ regobj.set_value('_writable', r.perms == None or 'w' in r.perms)
+ regobj.set_value('_executable', r.perms == None or 'x' in r.perms)
+ regobj.set_value('_offset', r.offset)
+ regobj.set_value('_objfile', r.objfile)
+ regobj.insert()
+ STATE.trace.proxy_object_path(
+ MEMORY_PATTERN.format(procnum=proc.GetProcessID())).retain_values(keys)
+
+
+def ghidra_trace_put_regions(debugger, command, result, internal_dict):
+ """
+ Read the memory map, if applicable, and write to the trace's Regions
+ """
+
+ STATE.require_tx()
+ with STATE.client.batch() as b:
+ put_regions()
+
+
+def put_modules():
+ target = util.get_target()
+ proc = util.get_process()
+ modules = util.MODULE_INFO_READER.get_modules()
+ mapper = STATE.trace.memory_mapper
+ mod_keys = []
+ for mk, m in modules.items():
+ mpath = MODULE_PATTERN.format(procnum=proc.GetProcessID(), modpath=mk)
+ modobj = STATE.trace.create_object(mpath)
+ mod_keys.append(MODULE_KEY_PATTERN.format(modpath=mk))
+ modobj.set_value('_module_name', m.name)
+ base_base, base_addr = mapper.map(proc, m.base)
+ if base_base != base_addr.space:
+ STATE.trace.create_overlay_space(base_base, base_addr.space)
+ if m.max > m.base:
+ modobj.set_value('_range', base_addr.extend(m.max - m.base + 1))
+ sec_keys = []
+ for sk, s in m.sections.items():
+ spath = mpath + SECTION_ADD_PATTERN.format(secname=sk)
+ secobj = STATE.trace.create_object(spath)
+ sec_keys.append(SECTION_KEY_PATTERN.format(secname=sk))
+ start_base, start_addr = mapper.map(proc, s.start)
+ if start_base != start_addr.space:
+ STATE.trace.create_overlay_space(
+ start_base, start_addr.space)
+ secobj.set_value('_range', start_addr.extend(s.end - s.start + 1))
+ secobj.set_value('_offset', s.offset)
+ secobj.set_value('_attrs', s.attrs)
+ secobj.insert()
+ # In case there are no sections, we must still insert the module
+ modobj.insert()
+ STATE.trace.proxy_object_path(
+ mpath + SECTIONS_ADD_PATTERN).retain_values(sec_keys)
+ STATE.trace.proxy_object_path(MODULES_PATTERN.format(
+ procnum=proc.GetProcessID())).retain_values(mod_keys)
+
+
+def ghidra_trace_put_modules(debugger, command, result, internal_dict):
+ """
+ Gather object files, if applicable, and write to the trace's Modules
+ """
+
+ STATE.require_tx()
+ with STATE.client.batch() as b:
+ put_modules()
+
+
+def convert_state(t):
+ if t.IsSuspended():
+ return 'SUSPENDED'
+ if t.IsStopped():
+ return 'STOPPED'
+ return 'RUNNING'
+
+
+def convert_tid(t):
+ if t[1] == 0:
+ return t[2]
+ return t[1]
+
+
+@contextmanager
+def restore_frame():
+ f = util.selected_frame()
+ yield
+ f.select()
+
+
+def compute_thread_display(t):
+ return util.get_description(t)
+
+
+def put_threads():
+ radix = util.get_convenience_variable('output-radix')
+ if radix == 'auto':
+ radix = 16
+ proc = util.get_process()
+ keys = []
+ for t in proc.threads:
+ tpath = THREAD_PATTERN.format(procnum=proc.GetProcessID(), tnum=t.GetThreadID())
+ tobj = STATE.trace.create_object(tpath)
+ keys.append(THREAD_KEY_PATTERN.format(tnum=t.GetThreadID()))
+ tobj.set_value('_state', convert_state(t))
+ tobj.set_value('_name', t.GetName())
+ tid = t.GetThreadID()
+ tobj.set_value('_tid', tid)
+ tidstr = ('0x{:x}' if radix ==
+ 16 else '0{:o}' if radix == 8 else '{}').format(tid)
+ tobj.set_value('_short_display', '[{}.{}:{}]'.format(
+ proc.GetProcessID(), t.GetThreadID(), tidstr))
+ tobj.set_value('_display', compute_thread_display(t))
+ tobj.insert()
+ STATE.trace.proxy_object_path(
+ THREADS_PATTERN.format(procnum=proc.GetProcessID())).retain_values(keys)
+
+
+def put_event_thread():
+ proc = util.get_process()
+ # Assumption: Event thread is selected by lldb upon stopping
+ t = util.selected_thread()
+ if t is not None:
+ tpath = THREAD_PATTERN.format(procnum=proc.GetProcessID(), tnum=t.GetThreadID())
+ tobj = STATE.trace.proxy_object_path(tpath)
+ else:
+ tobj = None
+ STATE.trace.proxy_object_path('').set_value('_event_thread', tobj)
+
+
+def ghidra_trace_put_threads(debugger, command, result, internal_dict):
+ """
+ Put the current process's threads into the Ghidra trace
+ """
+
+ STATE.require_tx()
+ with STATE.client.batch() as b:
+ put_threads()
+
+
+def put_frames():
+ proc = util.get_process()
+ mapper = STATE.trace.memory_mapper
+ t = util.selected_thread()
+ if t is None:
+ return
+ keys = []
+ for i in range(0,t.GetNumFrames()):
+ f = t.GetFrameAtIndex(i)
+ fpath = FRAME_PATTERN.format(
+ procnum=proc.GetProcessID(), tnum=t.GetThreadID(), level=f.GetFrameID())
+ fobj = STATE.trace.create_object(fpath)
+ keys.append(FRAME_KEY_PATTERN.format(level=f.GetFrameID()))
+ base, pc = mapper.map(proc, f.GetPC())
+ if base != pc.space:
+ STATE.trace.create_overlay_space(base, pc.space)
+ fobj.set_value('_pc', pc)
+ fobj.set_value('_func', str(f.GetFunctionName()))
+ fobj.set_value('_display', util.get_description(f))
+ fobj.insert()
+ STATE.trace.proxy_object_path(STACK_PATTERN.format(
+ procnum=proc.GetProcessID(), tnum=t.GetThreadID())).retain_values(keys)
+
+
+def ghidra_trace_put_frames(debugger, command, result, internal_dict):
+ """
+ Put the current thread's frames into the Ghidra trace
+ """
+
+ STATE.require_tx()
+ with STATE.client.batch() as b:
+ put_frames()
+
+
+def ghidra_trace_put_all(debugger, command, result, internal_dict):
+ """
+ Put everything currently selected into the Ghidra trace
+ """
+
+ STATE.require_tx()
+ with STATE.client.batch() as b:
+ ghidra_trace_putreg(debugger, DEFAULT_REGISTER_BANK, result, internal_dict)
+ ghidra_trace_putmem(debugger, "$pc 1", result, internal_dict)
+ ghidra_trace_putmem(debugger, "$sp 1", result, internal_dict)
+ put_processes()
+ put_environment()
+ put_regions()
+ put_modules()
+ put_threads()
+ put_frames()
+ put_breakpoints()
+ put_watchpoints()
+ put_available()
+
+
+def ghidra_trace_install_hooks(debugger, command, result, internal_dict):
+ """
+ Install hooks to trace in Ghidra
+ """
+
+ hooks.install_hooks()
+
+
+def ghidra_trace_remove_hooks(debugger, command, result, internal_dict):
+ """
+ Remove hooks to trace in Ghidra
+
+ Using this directly is not recommended, unless it seems the hooks are
+ preventing lldb or other extensions from operating. Removing hooks will break
+ trace synchronization until they are replaced.
+ """
+
+ hooks.remove_hooks()
+
+
+def ghidra_trace_sync_enable(debugger, command, result, internal_dict):
+ """
+ Synchronize the current process with the Ghidra trace
+
+ This will automatically install hooks if necessary. The goal is to record
+ the current frame, thread, and process into the trace immediately, and then
+ to append the trace upon stopping and/or selecting new frames. This action
+ is effective only for the current process. This command must be executed
+ for each individual process you'd like to synchronize. In older versions of
+ lldb, certain events cannot be hooked. In that case, you may need to execute
+ certain "trace put" commands manually, or go without.
+
+ This will have no effect unless or until you start a trace.
+ """
+
+ hooks.install_hooks()
+ hooks.enable_current_process()
+
+
+def ghidra_trace_sync_disable(debugger, command, result, internal_dict):
+ """
+ Cease synchronizing the current process with the Ghidra trace
+
+ This is the opposite of 'ghidra_trace_sync-disable', except it will not
+ automatically remove hooks.
+ """
+
+ hooks.disable_current_process()
+
+
+def ghidra_util_wait_stopped(debugger, command, result, internal_dict):
+ """
+ Spin wait until the selected thread is stopped.
+ """
+
+ timeout = commmand if len(command) > 0 else '1'
+
+ timeout = int(timeout)
+ start = time.time()
+ t = util.selected_thread()
+ if t is None:
+ return
+ while not t.IsStopped() and not t.IsSuspended():
+ t = util.selected_thread() # I suppose it could change
+ time.sleep(0.1)
+ if time.time() - start > timeout:
+ raise RuntimeError('Timed out waiting for thread to stop')
+
+
+def ghidra_util_mark(debugger, command, result, internal_dict):
+ print(command)
diff --git a/Ghidra/Debug/Debugger-agent-lldb/src/main/py/ghidralldb/hooks.py b/Ghidra/Debug/Debugger-agent-lldb/src/main/py/ghidralldb/hooks.py
new file mode 100644
index 0000000000..6f9ec0bb39
--- /dev/null
+++ b/Ghidra/Debug/Debugger-agent-lldb/src/main/py/ghidralldb/hooks.py
@@ -0,0 +1,709 @@
+## ###
+# IP: GHIDRA
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+import time
+import threading
+
+import lldb
+
+from . import commands, util
+
+ALL_EVENTS = 0xFFFF
+
+class HookState(object):
+ __slots__ = ('installed', 'mem_catchpoint')
+
+ def __init__(self):
+ self.installed = False
+ self.mem_catchpoint = None
+
+
+class ProcessState(object):
+ __slots__ = ('first', 'regions', 'modules', 'threads', 'breaks', 'watches', 'visited')
+
+ def __init__(self):
+ self.first = True
+ # For things we can detect changes to between stops
+ self.regions = False
+ self.modules = False
+ self.threads = False
+ self.breaks = False
+ self.watches = False
+ # For frames and threads that have already been synced since last stop
+ self.visited = set()
+
+ def record(self, description=None):
+ first = self.first
+ self.first = False
+ if description is not None:
+ commands.STATE.trace.snapshot(description)
+ if first:
+ commands.put_processes()
+ commands.put_environment()
+ if self.threads:
+ commands.put_threads()
+ self.threads = False
+ thread = util.selected_thread()
+ if thread is not None:
+ if first or thread.GetThreadID() not in self.visited:
+ commands.put_frames()
+ self.visited.add(thread.GetThreadID())
+ frame = util.selected_frame()
+ hashable_frame = (thread.GetThreadID(), frame.GetFrameID())
+ if first or hashable_frame not in self.visited:
+ banks = frame.GetRegisters()
+ commands.putreg(frame, banks.GetFirstValueByName(commands.DEFAULT_REGISTER_BANK))
+ commands.putmem("$pc", "1", from_tty=False)
+ commands.putmem("$sp", "1", from_tty=False)
+ self.visited.add(hashable_frame)
+ if first or self.regions or self.threads or self.modules:
+ # Sections, memory syscalls, or stack allocations
+ commands.put_regions()
+ self.regions = False
+ if first or self.modules:
+ commands.put_modules()
+ self.modules = False
+ if first or self.breaks:
+ commands.put_breakpoints()
+ self.breaks = False
+ if first or self.watches:
+ commands.put_watchpoints()
+ self.watches = False
+
+ def record_continued(self):
+ commands.put_processes()
+ commands.put_threads()
+
+ def record_exited(self, exit_code):
+ proc = util.get_process()
+ ipath = commands.PROCESS_PATTERN.format(procnum=proc.GetProcessID())
+ commands.STATE.trace.proxy_object_path(
+ ipath).set_value('_exit_code', exit_code)
+
+
+class BrkState(object):
+ __slots__ = ('break_loc_counts',)
+
+ def __init__(self):
+ self.break_loc_counts = {}
+
+ def update_brkloc_count(self, b, count):
+ self.break_loc_counts[b.GetID()] = count
+
+ def get_brkloc_count(self, b):
+ return self.break_loc_counts.get(b.GetID(), 0)
+
+ def del_brkloc_count(self, b):
+ if b not in self.break_loc_counts:
+ return 0 # TODO: Print a warning?
+ count = self.break_loc_counts[b.GetID()]
+ del self.break_loc_counts[b.GetID()]
+ return count
+
+
+HOOK_STATE = HookState()
+BRK_STATE = BrkState()
+PROC_STATE = {}
+
+def process_event(self, listener, event):
+ try:
+ desc = util.get_description(event)
+ #event_process = lldb.SBProcess_GetProcessFromEvent(event)
+ event_process = util.get_process()
+ if event_process not in PROC_STATE:
+ PROC_STATE[event_process.GetProcessID()] = ProcessState()
+ rc = event_process.GetBroadcaster().AddListener(listener, ALL_EVENTS)
+ if rc is False:
+ print("add listener for process failed")
+ event_thread = lldb.SBThread_GetThreadFromEvent(event)
+ commands.put_state(event_process)
+ type = event.GetType()
+ if lldb.SBTarget.EventIsTargetEvent(event):
+ print('Event:', desc)
+ if (type & lldb.SBTarget.eBroadcastBitBreakpointChanged) != 0:
+ print("eBroadcastBitBreakpointChanged")
+ return on_breakpoint_modified(event)
+ if (type & lldb.SBTarget.eBroadcastBitWatchpointChanged) != 0:
+ print("eBroadcastBitWatchpointChanged")
+ return on_watchpoint_modified(event)
+ if (type & lldb.SBTarget.eBroadcastBitModulesLoaded) != 0:
+ print("eBroadcastBitModulesLoaded")
+ return on_new_objfile(event)
+ if (type & lldb.SBTarget.eBroadcastBitModulesUnloaded) != 0:
+ print("eBroadcastBitModulesUnloaded")
+ return on_free_objfile(event)
+ if (type & lldb.SBTarget.eBroadcastBitSymbolsLoaded) != 0:
+ print("eBroadcastBitSymbolsLoaded")
+ return True
+ if lldb.SBProcess.EventIsProcessEvent(event):
+ if (type & lldb.SBProcess.eBroadcastBitStateChanged) != 0:
+ print("eBroadcastBitStateChanged")
+ if not event_process.is_alive:
+ return on_exited(event)
+ if event_process.is_stopped:
+ return on_stop(event)
+ return True
+ if (type & lldb.SBProcess.eBroadcastBitInterrupt) != 0:
+ print("eBroadcastBitInterrupt")
+ if event_process.is_stopped:
+ return on_stop(event)
+ if (type & lldb.SBProcess.eBroadcastBitSTDOUT) != 0:
+ return True
+ if (type & lldb.SBProcess.eBroadcastBitSTDERR) != 0:
+ return True
+ if (type & lldb.SBProcess.eBroadcastBitProfileData) != 0:
+ print("eBroadcastBitProfileData")
+ return True
+ if (type & lldb.SBProcess.eBroadcastBitStructuredData) != 0:
+ print("eBroadcastBitStructuredData")
+ return True
+ # NB: Thread events not currently processes
+ if lldb.SBThread.EventIsThreadEvent(event):
+ print('Event:', desc)
+ if (type & lldb.SBThread.eBroadcastBitStackChanged) != 0:
+ print("eBroadcastBitStackChanged")
+ return on_frame_selected()
+ if (type & lldb.SBThread.eBroadcastBitThreadSuspended) != 0:
+ print("eBroadcastBitThreadSuspended")
+ if event_process.is_stopped:
+ return on_stop(event)
+ if (type & lldb.SBThread.eBroadcastBitThreadResumed) != 0:
+ print("eBroadcastBitThreadResumed")
+ return on_cont(event)
+ if (type & lldb.SBThread.eBroadcastBitSelectedFrameChanged) != 0:
+ print("eBroadcastBitSelectedFrameChanged")
+ return on_frame_selected()
+ if (type & lldb.SBThread.eBroadcastBitThreadSelected) != 0:
+ print("eBroadcastBitThreadSelected")
+ return on_thread_selected()
+ if lldb.SBBreakpoint.EventIsBreakpointEvent(event):
+ print('Event:', desc)
+ btype = lldb.SBBreakpoint.GetBreakpointEventTypeFromEvent(event);
+ bpt = lldb.SBBreakpoint.GetBreakpointFromEvent(event);
+ if btype is lldb.eBreakpointEventTypeAdded:
+ print("eBreakpointEventTypeAdded")
+ return on_breakpoint_created(bpt)
+ if btype is lldb.eBreakpointEventTypeAutoContinueChanged:
+ print("elldb.BreakpointEventTypeAutoContinueChanged")
+ return on_breakpoint_modified(bpt)
+ if btype is lldb.eBreakpointEventTypeCommandChanged:
+ print("eBreakpointEventTypeCommandChanged")
+ return on_breakpoint_modified(bpt)
+ if btype is lldb.eBreakpointEventTypeConditionChanged:
+ print("eBreakpointEventTypeConditionChanged")
+ return on_breakpoint_modified(bpt)
+ if btype is lldb.eBreakpointEventTypeDisabled:
+ print("eBreakpointEventTypeDisabled")
+ return on_breakpoint_modified(bpt)
+ if btype is lldb.eBreakpointEventTypeEnabled:
+ print("eBreakpointEventTypeEnabled")
+ return on_breakpoint_modified(bpt)
+ if btype is lldb.eBreakpointEventTypeIgnoreChanged:
+ print("eBreakpointEventTypeIgnoreChanged")
+ return True
+ if btype is lldb.eBreakpointEventTypeInvalidType:
+ print("eBreakpointEventTypeInvalidType")
+ return True
+ if btype is lldb.eBreakpointEventTypeLocationsAdded:
+ print("eBreakpointEventTypeLocationsAdded")
+ return on_breakpoint_modified(bpt)
+ if btype is lldb.eBreakpointEventTypeLocationsRemoved:
+ print("eBreakpointEventTypeLocationsRemoved")
+ return on_breakpoint_modified(bpt)
+ if btype is lldb.eBreakpointEventTypeLocationsResolved:
+ print("eBreakpointEventTypeLocationsResolved")
+ return on_breakpoint_modified(bpt)
+ if btype is lldb.eBreakpointEventTypeRemoved:
+ print("eBreakpointEventTypeRemoved")
+ return on_breakpoint_deleted(bpt)
+ if btype is lldb.eBreakpointEventTypeThreadChanged:
+ print("eBreakpointEventTypeThreadChanged")
+ return on_breakpoint_modified(bpt)
+ print("UNKNOWN BREAKPOINT EVENT")
+ return True
+ if lldb.SBWatchpoint.EventIsWatchpointEvent(event):
+ print('Event:', desc)
+ btype = lldb.SBWatchpoint.GetWatchpointEventTypeFromEvent(event);
+ bpt = lldb.SBWatchpoint.GetWatchpointFromEvent(eventt);
+ if btype is lldb.eWatchpointEventTypeAdded:
+ print("eWatchpointEventTypeAdded")
+ return on_watchpoint_added(bpt)
+ if btype is lldb.eWatchpointEventTypeCommandChanged:
+ print("eWatchpointEventTypeCommandChanged")
+ return on_watchpoint_modified(bpt)
+ if btype is lldb.eWatchpointEventTypeConditionChanged:
+ print("eWatchpointEventTypeConditionChanged")
+ return on_watchpoint_modified(bpt)
+ if btype is lldb.eWatchpointEventTypeDisabled:
+ print("eWatchpointEventTypeDisabled")
+ return on_watchpoint_modified(bpt)
+ if btype is lldb.eWatchpointEventTypeEnabled:
+ print("eWatchpointEventTypeEnabled")
+ return on_watchpoint_modified(bpt)
+ if btype is lldb.eWatchpointEventTypeIgnoreChanged:
+ print("eWatchpointEventTypeIgnoreChanged")
+ return True
+ if btype is lldb.eWatchpointEventTypeInvalidType:
+ print("eWatchpointEventTypeInvalidType")
+ return True
+ if btype is lldb.eWatchpointEventTypeRemoved:
+ print("eWatchpointEventTypeRemoved")
+ return on_watchpoint_deleted(bpt)
+ if btype is lldb.eWatchpointEventTypeThreadChanged:
+ print("eWatchpointEventTypeThreadChanged")
+ return on_watchpoint_modified(bpt)
+ if btype is lldb.eWatchpointEventTypeTypeChanged:
+ print("eWatchpointEventTypeTypeChanged")
+ return on_watchpoint_modified(bpt)
+ print("UNKNOWN WATCHPOINT EVENT")
+ return True
+ if lldb.SBCommandInterpreter.EventIsCommandInterpreterEvent(event):
+ print('Event:', desc)
+ if (type & lldb.SBCommandInterpreter.eBroadcastBitAsynchronousErrorData) != 0:
+ print("eBroadcastBitAsynchronousErrorData")
+ return True
+ if (type & lldb.SBCommandInterpreter.eBroadcastBitAsynchronousOutputData) != 0:
+ print("eBroadcastBitAsynchronousOutputData")
+ return True
+ if (type & lldb.SBCommandInterpreter.eBroadcastBitQuitCommandReceived) != 0:
+ print("eBroadcastBitQuitCommandReceived")
+ return True
+ if (type & lldb.SBCommandInterpreter.eBroadcastBitResetPrompt) != 0:
+ print("eBroadcastBitResetPrompt")
+ return True
+ if (type & lldb.SBCommandInterpreter.eBroadcastBitThreadShouldExit) != 0:
+ print("eBroadcastBitThreadShouldExit")
+ return True
+ print("UNKNOWN EVENT")
+ return True
+ except RuntimeError as e:
+ print(e)
+
+class EventThread(threading.Thread):
+ func = process_event
+ event = lldb.SBEvent()
+
+ def run(self):
+ # Let's only try at most 4 times to retrieve any kind of event.
+ # After that, the thread exits.
+ listener = lldb.SBListener('eventlistener')
+ cli = util.get_debugger().GetCommandInterpreter()
+ target = util.get_target()
+ proc = util.get_process()
+ rc = cli.GetBroadcaster().AddListener(listener, ALL_EVENTS)
+ if rc is False:
+ print("add listener for cli failed")
+ return
+ rc = target.GetBroadcaster().AddListener(listener, ALL_EVENTS)
+ if rc is False:
+ print("add listener for target failed")
+ return
+ rc = proc.GetBroadcaster().AddListener(listener, ALL_EVENTS)
+ if rc is False:
+ print("add listener for process failed")
+ return
+
+ # Not sure what effect this logic has
+ rc = cli.GetBroadcaster().AddInitialEventsToListener(listener, ALL_EVENTS)
+ if rc is False:
+ print("add listener for cli failed")
+ return
+ rc = target.GetBroadcaster().AddInitialEventsToListener(listener, ALL_EVENTS)
+ if rc is False:
+ print("add listener for target failed")
+ return
+ rc = proc.GetBroadcaster().AddInitialEventsToListener(listener, ALL_EVENTS)
+ if rc is False:
+ print("add listener for process failed")
+ return
+
+ rc = listener.StartListeningForEventClass(util.get_debugger(), lldb.SBThread.GetBroadcasterClassName(), ALL_EVENTS)
+ if rc is False:
+ print("add listener for threads failed")
+ return
+ # THIS WILL NOT WORK: listener = util.get_debugger().GetListener()
+
+ while True:
+ event_recvd = False
+ while event_recvd is False:
+ if listener.WaitForEvent(lldb.UINT32_MAX, self.event):
+ try:
+ self.func(listener, self.event)
+ while listener.GetNextEvent(self.event):
+ self.func(listener, self.event)
+ event_recvd = True
+ except Exception as e:
+ print(e)
+ proc = util.get_process()
+ if proc is not None and not proc.is_alive:
+ break
+ return
+
+"""
+ # Not sure if this is possible in LLDB...
+
+ # Respond to user-driven state changes: (Not target-driven)
+ lldb.events.memory_changed.connect(on_memory_changed)
+ lldb.events.register_changed.connect(on_register_changed)
+ # Respond to target-driven memory map changes:
+ # group:memory is actually a bit broad, but will probably port better
+ # One alternative is to name all syscalls that cause a change....
+ # Ones we could probably omit:
+ # msync,
+ # (Deals in syncing file-backed pages to disk.)
+ # mlock, munlock, mlockall, munlockall, mincore, madvise,
+ # (Deal in paging. Doesn't affect valid addresses.)
+ # mbind, get_mempolicy, set_mempolicy, migrate_pages, move_pages
+ # (All NUMA stuff)
+ #
+ if HOOK_STATE.mem_catchpoint is not None:
+ HOOK_STATE.mem_catchpoint.enabled = True
+ else:
+ breaks_before = set(lldb.breakpoints())
+ lldb.execute(
+ catch syscall group:memory
+ commands
+ silent
+ ghidra-hook event-memory
+ cont
+ end
+ )
+ HOOK_STATE.mem_catchpoint = (
+ set(lldb.breakpoints()) - breaks_before).pop()
+"""
+
+
+def on_new_process(event):
+ trace = commands.STATE.trace
+ if trace is None:
+ return
+ with commands.STATE.client.batch():
+ with trace.open_tx("New Process {}".format(event.process.num)):
+ commands.put_processes() # TODO: Could put just the one....
+
+
+def on_process_selected():
+ proc = util.get_process()
+ if proc.GetProcessID() not in PROC_STATE:
+ return
+ trace = commands.STATE.trace
+ if trace is None:
+ return
+ with commands.STATE.client.batch():
+ with trace.open_tx("Process {} selected".format(proc.GetProcessID())):
+ PROC_STATE[proc.GetProcessID()].record()
+ commands.activate()
+
+
+def on_process_deleted(event):
+ trace = commands.STATE.trace
+ if trace is None:
+ return
+ if event.process.num in PROC_STATE:
+ del PROC_STATE[event.process.num]
+ with commands.STATE.client.batch():
+ with trace.open_tx("Process {} deleted".format(event.process.num)):
+ commands.put_processes() # TODO: Could just delete the one....
+
+
+def on_new_thread(event):
+ proc = util.get_process()
+ if proc.GetProcessID() not in PROC_STATE:
+ return
+ PROC_STATE[proc.GetProcessID()].threads = True
+ # TODO: Syscall clone/exit to detect thread destruction?
+
+
+def on_thread_selected():
+ proc = util.get_process()
+ if proc.GetProcessID() not in PROC_STATE:
+ return
+ trace = commands.STATE.trace
+ if trace is None:
+ return
+ t = util.selected_thread()
+ with commands.STATE.client.batch():
+ with trace.open_tx("Thread {}.{} selected".format(proc.GetProcessID(), t.GetThreadID())):
+ PROC_STATE[proc.GetProcessID()].record()
+ commands.put_threads()
+ commands.activate()
+
+
+def on_frame_selected():
+ proc = util.get_process()
+ if proc.GetProcessID() not in PROC_STATE:
+ return
+ trace = commands.STATE.trace
+ if trace is None:
+ return
+ f = util.selected_frame()
+ t = f.GetThread()
+ with commands.STATE.client.batch():
+ with trace.open_tx("Frame {}.{}.{} selected".format(proc.GetProcessID(), t.GetThreadID(), f.GetFrameID())):
+ PROC_STATE[proc.GetProcessID()].record()
+ commands.put_threads()
+ commands.put_frames()
+ commands.activate()
+
+
+def on_syscall_memory():
+ proc = util.get_process()
+ if proc.GetProcessID() not in PROC_STATE:
+ return
+ PROC_STATE[proc.GetProcessID()].regions = True
+
+
+def on_memory_changed(event):
+ proc = util.get_process()
+ if proc.GetProcessID() not in PROC_STATE:
+ return
+ trace = commands.STATE.trace
+ if trace is None:
+ return
+ with commands.STATE.client.batch():
+ with trace.open_tx("Memory *0x{:08x} changed".format(event.address)):
+ commands.put_bytes(event.address, event.address + event.length,
+ pages=False, is_mi=False, from_tty=False)
+
+
+def on_register_changed(event):
+ print("Register changed: {}".format(dir(event)))
+ proc = util.get_process()
+ if proc.GetProcessID() not in PROC_STATE:
+ return
+ trace = commands.STATE.trace
+ if trace is None:
+ return
+ # I'd rather have a descriptor!
+ # TODO: How do I get the descriptor from the number?
+ # For now, just record the lot
+ with commands.STATE.client.batch():
+ with trace.open_tx("Register {} changed".format(event.regnum)):
+ banks = event.frame.GetRegisters()
+ commands.putreg(
+ event.frame, banks.GetFirstValueByName(commands.DEFAULT_REGISTER_BANK))
+
+
+def on_cont(event):
+ proc = util.get_process()
+ if proc.GetProcessID() not in PROC_STATE:
+ return
+ trace = commands.STATE.trace
+ if trace is None:
+ return
+ state = PROC_STATE[proc.GetProcessID()]
+ with commands.STATE.client.batch():
+ with trace.open_tx("Continued"):
+ state.record_continued()
+
+
+def on_stop(event):
+ proc = lldb.SBProcess.GetProcessFromEvent(event)
+ if proc.GetProcessID() not in PROC_STATE:
+ print("not in state")
+ return
+ trace = commands.STATE.trace
+ if trace is None:
+ print("no trace")
+ return
+ state = PROC_STATE[proc.GetProcessID()]
+ state.visited.clear()
+ with commands.STATE.client.batch():
+ with trace.open_tx("Stopped"):
+ state.record("Stopped")
+ commands.put_event_thread()
+ commands.put_threads()
+ commands.put_frames()
+ commands.activate()
+
+
+def on_exited(event):
+ proc = util.get_process()
+ if proc.GetProcessID() not in PROC_STATE:
+ return
+ trace = commands.STATE.trace
+ if trace is None:
+ return
+ state = PROC_STATE[proc.GetProcessID()]
+ state.visited.clear()
+ exit_code = proc.GetExitStatus()
+ description = "Exited with code {}".format(exit_code)
+ with commands.STATE.client.batch():
+ with trace.open_tx(description):
+ state.record(description)
+ state.record_exited(exit_code)
+ commands.put_event_thread()
+ commands.activate()
+
+def notify_others_breaks(proc):
+ for num, state in PROC_STATE.items():
+ if num != proc.GetProcessID():
+ state.breaks = True
+
+def notify_others_watches(proc):
+ for num, state in PROC_STATE.items():
+ if num != proc.GetProcessID():
+ state.watches = True
+
+
+def modules_changed():
+ # Assumption: affects the current process
+ proc = util.get_process()
+ if proc.GetProcessID() not in PROC_STATE:
+ return
+ PROC_STATE[proc.GetProcessID()].modules = True
+
+
+def on_new_objfile(event):
+ modules_changed()
+
+
+def on_free_objfile(event):
+ modules_changed()
+
+
+def on_breakpoint_created(b):
+ proc = util.get_process()
+ notify_others_breaks(proc)
+ if proc.GetProcessID() not in PROC_STATE:
+ return
+ trace = commands.STATE.trace
+ if trace is None:
+ return
+ ibpath = commands.PROC_BREAKS_PATTERN.format(procnum=proc.GetProcessID())
+ with commands.STATE.client.batch():
+ with trace.open_tx("Breakpoint {} created".format(b.GetID())):
+ ibobj = trace.create_object(ibpath)
+ # Do not use retain_values or it'll remove other locs
+ commands.put_single_breakpoint(b, ibobj, proc, [])
+ ibobj.insert()
+
+
+def on_breakpoint_modified(b):
+ proc = util.get_process()
+ notify_others_breaks(proc)
+ if proc.GetProcessID() not in PROC_STATE:
+ return
+ old_count = BRK_STATE.get_brkloc_count(b)
+ trace = commands.STATE.trace
+ if trace is None:
+ return
+ ibpath = commands.PROC_BREAKS_PATTERN.format(procnum=proc.GetProcessID())
+ with commands.STATE.client.batch():
+ with trace.open_tx("Breakpoint {} modified".format(b.GetID())):
+ ibobj = trace.create_object(ibpath)
+ commands.put_single_breakpoint(b, ibobj, proc, [])
+ new_count = BRK_STATE.get_brkloc_count(b)
+ # NOTE: Location may not apply to process, but whatever.
+ for i in range(new_count, old_count):
+ ikey = commands.PROC_BREAK_KEY_PATTERN.format(
+ breaknum=b.GetID(), locnum=i+1)
+ ibobj.set_value(ikey, None)
+
+
+def on_breakpoint_deleted(b):
+ proc = util.get_process()
+ notify_others_breaks(proc)
+ if proc.GetProcessID() not in PROC_STATE:
+ return
+ old_count = BRK_STATE.del_brkloc_count(b.GetID())
+ trace = commands.STATE.trace
+ if trace is None:
+ return
+ bpath = commands.BREAKPOINT_PATTERN.format(breaknum=b.GetID())
+ ibobj = trace.proxy_object_path(
+ commands.PROC_BREAKS_PATTERN.format(procnum=proc.GetProcessID()))
+ with commands.STATE.client.batch():
+ with trace.open_tx("Breakpoint {} deleted".format(b.GetID())):
+ trace.proxy_object_path(bpath).remove(tree=True)
+ for i in range(old_count):
+ ikey = commands.PROC_BREAK_KEY_PATTERN.format(
+ breaknum=b.GetID(), locnum=i+1)
+ ibobj.set_value(ikey, None)
+
+
+def on_watchpoint_created(b):
+ proc = util.get_process()
+ notify_others_watches(proc)
+ if proc.GetProcessID() not in PROC_STATE:
+ return
+ trace = commands.STATE.trace
+ if trace is None:
+ return
+ ibpath = commands.PROC_WATCHES_PATTERN.format(procnum=proc.GetProcessID())
+ with commands.STATE.client.batch():
+ with trace.open_tx("Breakpoint {} created".format(b.GetID())):
+ ibobj = trace.create_object(ibpath)
+ # Do not use retain_values or it'll remove other locs
+ commands.put_single_watchpoint(b, ibobj, proc, [])
+ ibobj.insert()
+
+
+def on_watchpoint_modified(b):
+ proc = util.get_process()
+ notify_others_watches(proc)
+ if proc.GetProcessID() not in PROC_STATE:
+ return
+ old_count = BRK_STATE.get_brkloc_count(b)
+ trace = commands.STATE.trace
+ if trace is None:
+ return
+ ibpath = commands.PROC_WATCHES_PATTERN.format(procnum=proc.GetProcessID())
+ with commands.STATE.client.batch():
+ with trace.open_tx("Watchpoint {} modified".format(b.GetID())):
+ ibobj = trace.create_object(ibpath)
+ commands.put_single_watchpoint(b, ibobj, proc, [])
+
+
+def on_watchpoint_deleted(b):
+ proc = util.get_process()
+ notify_others_watches(proc)
+ if proc.GetProcessID() not in PROC_STATE:
+ return
+ trace = commands.STATE.trace
+ if trace is None:
+ return
+ bpath = commands.WATCHPOINT_PATTERN.format(watchnum=b.GetID())
+ ibobj = trace.proxy_object_path(
+ commands.PROC_WATCHES_PATTERN.format(procnum=proc.GetProcessID()))
+ with commands.STATE.client.batch():
+ with trace.open_tx("Watchpoint {} deleted".format(b.GetID())):
+ trace.proxy_object_path(bpath).remove(tree=True)
+
+
+def install_hooks():
+ if HOOK_STATE.installed:
+ return
+ HOOK_STATE.installed = True
+
+ event_thread = EventThread()
+ event_thread.start()
+
+
+def remove_hooks():
+ if not HOOK_STATE.installed:
+ return
+ HOOK_STATE.installed = False
+
+def enable_current_process():
+ proc = util.get_process()
+ PROC_STATE[proc.GetProcessID()] = ProcessState()
+
+
+def disable_current_process():
+ proc = util.get_process()
+ if proc.GetProcessID() in PROC_STATE:
+ # Silently ignore already disabled
+ del PROC_STATE[proc.GetProcessID()]
diff --git a/Ghidra/Debug/Debugger-agent-lldb/src/main/py/ghidralldb/methods.py b/Ghidra/Debug/Debugger-agent-lldb/src/main/py/ghidralldb/methods.py
new file mode 100644
index 0000000000..359ff94568
--- /dev/null
+++ b/Ghidra/Debug/Debugger-agent-lldb/src/main/py/ghidralldb/methods.py
@@ -0,0 +1,640 @@
+## ###
+# IP: GHIDRA
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+from concurrent.futures import Future, ThreadPoolExecutor
+import re
+
+from ghidratrace import sch
+from ghidratrace.client import MethodRegistry, ParamDesc, Address, AddressRange
+
+import lldb
+
+from . import commands, util
+
+
+REGISTRY = MethodRegistry(ThreadPoolExecutor(max_workers=1))
+
+
+def extre(base, ext):
+ return re.compile(base.pattern + ext)
+
+
+AVAILABLE_PATTERN = re.compile('Available\[(?P\\d*)\]')
+WATCHPOINT_PATTERN = re.compile('Watchpoints\[(?P\\d*)\]')
+BREAKPOINT_PATTERN = re.compile('Breakpoints\[(?P\\d*)\]')
+BREAK_LOC_PATTERN = extre(BREAKPOINT_PATTERN, '\[(?P\\d*)\]')
+PROCESS_PATTERN = re.compile('Processes\[(?P\\d*)\]')
+PROC_BREAKS_PATTERN = extre(PROCESS_PATTERN, '\.Breakpoints')
+PROC_WATCHES_PATTERN = extre(PROCESS_PATTERN, '\.Watchpoints')
+PROC_WATCHLOC_PATTERN = extre(PROC_WATCHES_PATTERN, '\[(?P\\d*)\]')
+ENV_PATTERN = extre(PROCESS_PATTERN, '\.Environment')
+THREADS_PATTERN = extre(PROCESS_PATTERN, '\.Threads')
+THREAD_PATTERN = extre(THREADS_PATTERN, '\[(?P\\d*)\]')
+STACK_PATTERN = extre(THREAD_PATTERN, '\.Stack')
+FRAME_PATTERN = extre(STACK_PATTERN, '\[(?P\\d*)\]')
+REGS_PATTERN = extre(FRAME_PATTERN, '.Registers')
+MEMORY_PATTERN = extre(PROCESS_PATTERN, '\.Memory')
+MODULES_PATTERN = extre(PROCESS_PATTERN, '\.Modules')
+
+
+def find_availpid_by_pattern(pattern, object, err_msg):
+ mat = pattern.fullmatch(object.path)
+ if mat is None:
+ raise TypeError(f"{object} is not {err_msg}")
+ pid = int(mat['pid'])
+ return pid
+
+
+def find_availpid_by_obj(object):
+ return find_availpid_by_pattern(AVAILABLE_PATTERN, object, "an Available")
+
+
+def find_proc_by_num(procnum):
+ return util.get_process()
+
+
+def find_proc_by_pattern(object, pattern, err_msg):
+ print(object.path)
+ mat = pattern.fullmatch(object.path)
+ print(mat)
+ if mat is None:
+ raise TypeError(f"{object} is not {err_msg}")
+ procnum = int(mat['procnum'])
+ return find_proc_by_num(procnum)
+
+
+def find_proc_by_obj(object):
+ return find_proc_by_pattern(object, PROCESS_PATTERN, "an Process")
+
+
+def find_proc_by_procbreak_obj(object):
+ return find_proc_by_pattern(object, PROC_BREAKS_PATTERN,
+ "a BreakpointLocationContainer")
+
+def find_proc_by_procwatch_obj(object):
+ return find_proc_by_pattern(object, PROC_WATCHES_PATTERN,
+ "a WatchpointContainer")
+
+
+def find_proc_by_env_obj(object):
+ return find_proc_by_pattern(object, ENV_PATTERN, "an Environment")
+
+
+def find_proc_by_threads_obj(object):
+ return find_proc_by_pattern(object, THREADS_PATTERN, "a ThreadContainer")
+
+
+def find_proc_by_mem_obj(object):
+ return find_proc_by_pattern(object, MEMORY_PATTERN, "a Memory")
+
+
+def find_proc_by_modules_obj(object):
+ return find_proc_by_pattern(object, MODULES_PATTERN, "a ModuleContainer")
+
+
+def find_thread_by_num(proc, tnum):
+ for t in proc.threads:
+ if t.GetThreadID() == tnum:
+ return t
+ raise KeyError(f"Processes[{proc.GetProcessID()}].Threads[{tnum}] does not exist")
+
+
+def find_thread_by_pattern(pattern, object, err_msg):
+ mat = pattern.fullmatch(object.path)
+ if mat is None:
+ raise TypeError(f"{object} is not {err_msg}")
+ procnum = int(mat['procnum'])
+ tnum = int(mat['tnum'])
+ proc = find_proc_by_num(procnum)
+ return find_thread_by_num(proc, tnum)
+
+
+def find_thread_by_obj(object):
+ return find_thread_by_pattern(THREAD_PATTERN, object, "a Thread")
+
+
+def find_thread_by_stack_obj(object):
+ return find_thread_by_pattern(STACK_PATTERN, object, "a Stack")
+
+
+def find_frame_by_level(thread, level):
+ return thread.GetFrameAtIndex(level)
+
+
+def find_frame_by_pattern(pattern, object, err_msg):
+ mat = pattern.fullmatch(object.path)
+ if mat is None:
+ raise TypeError(f"{object} is not {err_msg}")
+ procnum = int(mat['procnum'])
+ tnum = int(mat['tnum'])
+ level = int(mat['level'])
+ proc = find_proc_by_num(procnum)
+ t = find_thread_by_num(proc, tnum)
+ return find_frame_by_level(t, level)
+
+
+def find_frame_by_obj(object):
+ return find_frame_by_pattern(FRAME_PATTERN, object, "a StackFrame")
+
+
+def find_frame_by_regs_obj(object):
+ return find_frame_by_pattern(REGS_PATTERN, object,
+ "a RegisterValueContainer")
+
+
+# Because there's no method to get a register by name....
+def find_reg_by_name(f, name):
+ for reg in f.architecture().registers():
+ if reg.name == name:
+ return reg
+ raise KeyError(f"No such register: {name}")
+
+
+# Oof. no lldb/Python method to get breakpoint by number
+# I could keep my own cache in a dict, but why?
+def find_bpt_by_number(breaknum):
+ # TODO: If len exceeds some threshold, use binary search?
+ for i in range(0,util.get_target().GetNumBreakpoints()):
+ b = util.get_target().GetBreakpointAtIndex(i)
+ if b.GetID() == breaknum:
+ return b
+ raise KeyError(f"Breakpoints[{breaknum}] does not exist")
+
+
+def find_bpt_by_pattern(pattern, object, err_msg):
+ mat = pattern.fullmatch(object.path)
+ if mat is None:
+ raise TypeError(f"{object} is not {err_msg}")
+ breaknum = int(mat['breaknum'])
+ return find_bpt_by_number(breaknum)
+
+
+def find_bpt_by_obj(object):
+ return find_bpt_by_pattern(BREAKPOINT_PATTERN, object, "a BreakpointSpec")
+
+
+# Oof. no lldb/Python method to get breakpoint by number
+# I could keep my own cache in a dict, but why?
+def find_wpt_by_number(watchnum):
+ # TODO: If len exceeds some threshold, use binary search?
+ for i in range(0,util.get_target().GetNumWatchpoints()):
+ w = util.get_target().GetWatchpointAtIndex(i)
+ if w.GetID() == watchnum:
+ return w
+ raise KeyError(f"Watchpoints[{watchnum}] does not exist")
+
+
+def find_wpt_by_pattern(pattern, object, err_msg):
+ mat = pattern.fullmatch(object.path)
+ if mat is None:
+ raise TypeError(f"{object} is not {err_msg}")
+ watchnum = int(mat['watchnum'])
+ return find_wpt_by_number(watchnum)
+
+def find_wpt_by_obj(object):
+ return find_wpt_by_pattern(PROC_WATCHLOC_PATTERN, object, "a WatchpointSpec")
+
+
+def find_bptlocnum_by_pattern(pattern, object, err_msg):
+ mat = pattern.fullmatch(object.path)
+ if mat is None:
+ raise TypError(f"{object} is not {err_msg}")
+ breaknum = int(mat['breaknum'])
+ locnum = int(mat['locnum'])
+ return breaknum, locnum
+
+
+def find_bptlocnum_by_obj(object):
+ return find_bptlocnum_by_pattern(BREAK_LOC_PATTERN, object,
+ "a BreakpointLocation")
+
+
+def find_bpt_loc_by_obj(object):
+ breaknum, locnum = find_bptlocnum_by_obj(object)
+ bpt = find_bpt_by_number(breaknum)
+ # Requires lldb-13.1 or later
+ return bpt.locations[locnum - 1] # Display is 1-up
+
+
+@REGISTRY.method
+def execute(cmd: str, to_string: bool=False):
+ """Execute a CLI command."""
+ res = lldb.SBCommandReturnObject()
+ util.get_debugger().GetCommandInterpreter().HandleCommand(cmd, res)
+ if to_string:
+ if res.Succeeded():
+ return res.GetOutput()
+ else:
+ return res.GetError()
+
+
+@REGISTRY.method(action='refresh')
+def refresh_available(node: sch.Schema('AvailableContainer')):
+ """List processes on lldb's host system."""
+ with commands.open_tracked_tx('Refresh Available'):
+ util.get_debugger().HandleCommand('ghidra_trace_put_available')
+
+
+@REGISTRY.method(action='refresh')
+def refresh_breakpoints(node: sch.Schema('BreakpointContainer')):
+ """
+ Refresh the list of breakpoints (including locations for the current
+ process).
+ """
+ with commands.open_tracked_tx('Refresh Breakpoints'):
+ util.get_debugger().HandleCommand('ghidra_trace_put_breakpoints')
+
+
+@REGISTRY.method(action='refresh')
+def refresh_processes(node: sch.Schema('ProcessContainer')):
+ """Refresh the list of processes."""
+ with commands.open_tracked_tx('Refresh Processes'):
+ util.get_debugger().HandleCommand('ghidra_trace_put_threads')
+
+
+@REGISTRY.method(action='refresh')
+def refresh_proc_breakpoints(node: sch.Schema('BreakpointLocationContainer')):
+ """
+ Refresh the breakpoint locations for the process.
+
+ In the course of refreshing the locations, the breakpoint list will also be
+ refreshed.
+ """
+ with commands.open_tracked_tx('Refresh Breakpoint Locations'):
+ util.get_debugger().HandleCommand('ghidra_trace_put_breakpoints');
+
+
+@REGISTRY.method(action='refresh')
+def refresh_proc_watchpoints(node: sch.Schema('WatchpointContainer')):
+ """
+ Refresh the watchpoint locations for the process.
+
+ In the course of refreshing the locations, the watchpoint list will also be
+ refreshed.
+ """
+ with commands.open_tracked_tx('Refresh Watchpoint Locations'):
+ util.get_debugger().HandleCommand('ghidra_trace_put_watchpoints');
+
+
+@REGISTRY.method(action='refresh')
+def refresh_environment(node: sch.Schema('Environment')):
+ """Refresh the environment descriptors (arch, os, endian)."""
+ with commands.open_tracked_tx('Refresh Environment'):
+ util.get_debugger().HandleCommand('ghidra_trace_put_environment')
+
+@REGISTRY.method(action='refresh')
+def refresh_threads(node: sch.Schema('ThreadContainer')):
+ """Refresh the list of threads in the process."""
+ with commands.open_tracked_tx('Refresh Threads'):
+ util.get_debugger().HandleCommand('ghidra_trace_put_threads')
+
+
+@REGISTRY.method(action='refresh')
+def refresh_stack(node: sch.Schema('Stack')):
+ """Refresh the backtrace for the thread."""
+ t = find_thread_by_stack_obj(node)
+ t.process.SetSelectedThread(t)
+ with commands.open_tracked_tx('Refresh Stack'):
+ util.get_debugger().HandleCommand('ghidra_trace_put_frames');
+
+
+@REGISTRY.method(action='refresh')
+def refresh_registers(node: sch.Schema('RegisterValueContainer')):
+ """Refresh the register values for the frame."""
+ f = find_frame_by_regs_obj(node)
+ f.thread.SetSelectedFrame(f.GetFrameID())
+ # TODO: Groups?
+ with commands.open_tracked_tx('Refresh Registers'):
+ util.get_debugger().HandleCommand('ghidra_trace_putreg');
+
+
+@REGISTRY.method(action='refresh')
+def refresh_mappings(node: sch.Schema('Memory')):
+ """Refresh the list of memory regions for the process."""
+ with commands.open_tracked_tx('Refresh Memory Regions'):
+ util.get_debugger().HandleCommand('ghidra_trace_put_regions');
+
+
+@REGISTRY.method(action='refresh')
+def refresh_modules(node: sch.Schema('ModuleContainer')):
+ """
+ Refresh the modules and sections list for the process.
+
+ This will refresh the sections for all modules, not just the selected one.
+ """
+ with commands.open_tracked_tx('Refresh Modules'):
+ util.get_debugger().HandleCommand('ghidra_trace_put_modules');
+
+
+@REGISTRY.method(action='activate')
+def activate_process(process: sch.Schema('Process')):
+ """Switch to the process."""
+ return
+
+@REGISTRY.method(action='activate')
+def activate_thread(thread: sch.Schema('Thread')):
+ """Switch to the thread."""
+ t = find_thread_by_obj(thread)
+ t.process.SetSelectedThread(t)
+
+
+@REGISTRY.method(action='activate')
+def activate_frame(frame: sch.Schema('StackFrame')):
+ """Select the frame."""
+ f = find_frame_by_obj(frame)
+ f.thread.SetSelectedFrame(f.GetFrameID())
+
+
+@REGISTRY.method(action='delete')
+def remove_process(process: sch.Schema('Process')):
+ """Remove the process."""
+ proc = find_proc_by_obj(process)
+ util.get_debugger().HandleCommand(f'target delete 0')
+
+
+@REGISTRY.method(action='connect')
+def target(process: sch.Schema('Process'), spec: str):
+ """Connect to a target machine or process."""
+ util.get_debugger().HandleCommand(f'target select {spec}')
+
+
+@REGISTRY.method(action='attach')
+def attach_obj(process: sch.Schema('Process'), target: sch.Schema('Attachable')):
+ """Attach the process to the given target."""
+ pid = find_availpid_by_obj(target)
+ util.get_debugger().HandleCommand(f'process attach -p {pid}')
+
+@REGISTRY.method(action='attach')
+def attach_pid(process: sch.Schema('Process'), pid: int):
+ """Attach the process to the given target."""
+ util.get_debugger().HandleCommand(f'process attach -p {pid}')
+
+@REGISTRY.method(action='attach')
+def attach_name(process: sch.Schema('Process'), name: str):
+ """Attach the process to the given target."""
+ util.get_debugger().HandleCommand(f'process attach -n {name}')
+
+
+@REGISTRY.method
+def detach(process: sch.Schema('Process')):
+ """Detach the process's target."""
+ util.get_debugger().HandleCommand(f'process detach')
+
+
+@REGISTRY.method(action='launch')
+def launch_loader(process: sch.Schema('Process'),
+ file: ParamDesc(str, display='File'),
+ args: ParamDesc(str, display='Arguments')=''):
+ """
+ Start a native process with the given command line, stopping at 'main'.
+
+ If 'main' is not defined in the file, this behaves like 'run'.
+ """
+ util.get_debugger().HandleCommand(f'file {file}')
+ if args is not '':
+ util.get_debugger().HandleCommand(f'settings set target.run-args {args}')
+ util.get_debugger().HandleCommand(f'process launch --stop-at-entry')
+
+
+@REGISTRY.method(action='launch')
+def launch(process: sch.Schema('Process'),
+ file: ParamDesc(str, display='File'),
+ args: ParamDesc(str, display='Arguments')=''):
+ """
+ Run a native process with the given command line.
+
+ The process will not stop until it hits one of your breakpoints, or it is
+ signaled.
+ """
+ util.get_debugger().HandleCommand(f'file {file}')
+ if args is not '':
+ util.get_debugger().HandleCommand(f'settings set target.run-args {args}')
+ util.get_debugger().HandleCommand(f'run')
+
+
+@REGISTRY.method
+def kill(process: sch.Schema('Process')):
+ """Kill execution of the process."""
+ util.get_debugger().HandleCommand('process kill')
+
+
+@REGISTRY.method(name='continue', action='resume')
+def _continue(process: sch.Schema('Process')):
+ """Continue execution of the process."""
+ util.get_debugger().HandleCommand('process continue')
+
+
+@REGISTRY.method
+def interrupt():
+ """Interrupt the execution of the debugged program."""
+ util.get_debugger().HandleCommand('process interrupt')
+ #util.get_process().SendAsyncInterrupt()
+ #util.get_debugger().HandleCommand('^c')
+ #util.get_process().Signal(2)
+
+
+@REGISTRY.method(action='step_into')
+def step_into(thread: sch.Schema('Thread'), n: ParamDesc(int, display='N')=1):
+ """Step on instruction exactly."""
+ t = find_thread_by_obj(thread)
+ t.process.SetSelectedThread(t)
+ util.get_debugger().HandleCommand('thread step-inst')
+
+
+@REGISTRY.method(action='step_over')
+def step_over(thread: sch.Schema('Thread'), n: ParamDesc(int, display='N')=1):
+ """Step one instruction, but proceed through subroutine calls."""
+ t = find_thread_by_obj(thread)
+ t.process.SetSelectedThread(t)
+ util.get_debugger().HandleCommand('thread step-inst-over')
+
+
+@REGISTRY.method(action='step_out')
+def step_out(thread: sch.Schema('Thread')):
+ """Execute until the current stack frame returns."""
+ if thread is not None:
+ t = find_thread_by_obj(thread)
+ t.process.SetSelectedThread(t)
+ util.get_debugger().HandleCommand('thread step-out')
+
+
+@REGISTRY.method(action='step_ext')
+def step_ext(thread: sch.Schema('Thread'), address: Address):
+ """Continue execution up to the given address."""
+ t = find_thread_by_obj(thread)
+ t.process.SetSelectedThread(t)
+ offset = thread.trace.memory_mapper.map_back(t.process, address)
+ util.get_debugger().HandleCommand(f'thread until -a {offset}')
+
+
+@REGISTRY.method(name='return', action='step_ext')
+def _return(thread: sch.Schema('Thread'), value: int=None):
+ """Skip the remainder of the current function."""
+ t = find_thread_by_obj(thread)
+ t.process.SetSelectedThread(t)
+ if value is None:
+ util.get_debugger().HandleCommand('thread return')
+ else:
+ util.get_debugger().HandleCommand(f'thread return {value}')
+
+
+@REGISTRY.method(action='break_sw_execute')
+def break_address(process: sch.Schema('Process'), address: Address):
+ """Set a breakpoint."""
+ proc = find_proc_by_obj(process)
+ offset = process.trace.memory_mapper.map_back(proc, address)
+ util.get_debugger().HandleCommand(f'breakpoint set -a 0x{offset:x}')
+
+
+@REGISTRY.method(action='break_sw_execute')
+def break_expression(expression: str):
+ """Set a breakpoint."""
+ # TODO: Escape?
+ util.get_debugger().HandleCommand(f'breakpoint set -r {expression}')
+
+
+@REGISTRY.method(action='break_hw_execute')
+def break_hw_address(process: sch.Schema('Process'), address: Address):
+ """Set a hardware-assisted breakpoint."""
+ proc = find_proc_by_obj(process)
+ offset = process.trace.memory_mapper.map_back(proc, address)
+ util.get_debugger().HandleCommand(f'breakpoint set -H -a 0x{offset:x}')
+
+
+@REGISTRY.method(action='break_hw_execute')
+def break_hw_expression(expression: str):
+ """Set a hardware-assisted breakpoint."""
+ # TODO: Escape?
+ util.get_debugger().HandleCommand(f'breakpoint set -H -name {expression}')
+
+
+@REGISTRY.method(action='break_read')
+def break_read_range(process: sch.Schema('Process'), range: AddressRange):
+ """Set a read watchpoint."""
+ proc = find_proc_by_obj(process)
+ offset_start = process.trace.memory_mapper.map_back(
+ proc, Address(range.space, range.min))
+ sz = range.length()
+ util.get_debugger().HandleCommand(f'watchpoint set expression -s {sz} -w read -- {offset_start}')
+
+
+@REGISTRY.method(action='break_read')
+def break_read_expression(expression: str):
+ """Set a read watchpoint."""
+ util.get_debugger().HandleCommand(f'watchpoint set expression -w read -- {expression}')
+
+
+@REGISTRY.method(action='break_write')
+def break_write_range(process: sch.Schema('Process'), range: AddressRange):
+ """Set a watchpoint."""
+ proc = find_proc_by_obj(process)
+ offset_start = process.trace.memory_mapper.map_back(
+ proc, Address(range.space, range.min))
+ sz = range.length()
+ util.get_debugger().HandleCommand(f'watchpoint set expression -s {sz} -- {offset_start}')
+
+
+@REGISTRY.method(action='break_write')
+def break_write_expression(expression: str):
+ """Set a watchpoint."""
+ util.get_debugger().HandleCommand(f'watchpoint set expression -- {expression}')
+
+
+@REGISTRY.method(action='break_access')
+def break_access_range(process: sch.Schema('Process'), range: AddressRange):
+ """Set an access watchpoint."""
+ proc = find_proc_by_obj(process)
+ offset_start = process.trace.memory_mapper.map_back(
+ proc, Address(range.space, range.min))
+ sz = range.length()
+ util.get_debugger().HandleCommand(f'watchpoint set expression -s {sz} -w read_write -- {offset_start}')
+
+
+@REGISTRY.method(action='break_access')
+def break_access_expression(expression: str):
+ """Set an access watchpoint."""
+ util.get_debugger().HandleCommand(f'watchpoint set expression -w read_write -- {expression}')
+
+
+@REGISTRY.method(action='break_ext')
+def break_exception(lang: str):
+ """Set a catchpoint."""
+ util.get_debugger().HandleCommand(f'breakpoint set -E {lang}')
+
+
+@REGISTRY.method(action='toggle')
+def toggle_watchpoint(breakpoint: sch.Schema('WatchpointSpec'), enabled: bool):
+ """Toggle a watchpoint."""
+ wpt = find_wpt_by_obj(watchpoint)
+ wpt.enabled = enabled
+
+@REGISTRY.method(action='toggle')
+def toggle_breakpoint(breakpoint: sch.Schema('BreakpointSpec'), enabled: bool):
+ """Toggle a breakpoint."""
+ bpt = find_bpt_by_obj(breakpoint)
+ bpt.enabled = enabled
+
+@REGISTRY.method(action='toggle')
+def toggle_breakpoint_location(location: sch.Schema('BreakpointLocation'), enabled: bool):
+ """Toggle a breakpoint location."""
+ bptnum, locnum = find_bptlocnum_by_obj(location)
+ cmd = 'enable' if enabled else 'disable'
+ util.get_debugger().HandleCommand(f'breakpoint {cmd} {bptnum}.{locnum}')
+
+
+@REGISTRY.method(action='delete')
+def delete_watchpoint(watchpoint: sch.Schema('WatchpointSpec')):
+ """Delete a watchpoint."""
+ wpt = find_wpt_by_obj(watchpoint)
+ wptnum = wpt.GetID()
+ util.get_debugger().HandleCommand(f'watchpoint delete {wptnum}')
+
+@REGISTRY.method(action='delete')
+def delete_breakpoint(breakpoint: sch.Schema('BreakpointSpec')):
+ """Delete a breakpoint."""
+ bpt = find_bpt_by_obj(breakpoint)
+ bptnum = bpt.GetID()
+ util.get_debugger().HandleCommand(f'breakpoint delete {bptnum}')
+
+
+@REGISTRY.method
+def read_mem(process: sch.Schema('Process'), range: AddressRange):
+ """Read memory."""
+ proc = find_proc_by_obj(process)
+ offset_start = process.trace.memory_mapper.map_back(
+ proc, Address(range.space, range.min))
+ with commands.open_tracked_tx('Read Memory'):
+ util.get_debugger().HandleCommand(f'ghidra_trace_putmem 0x{offset_start:x} {range.length()}')
+
+
+@REGISTRY.method
+def write_mem(process: sch.Schema('Process'), address: Address, data: bytes):
+ """Write memory."""
+ proc = find_proc_by_obj(process)
+ offset = process.trace.memory_mapper.map_back(proc, address)
+ proc.write_memory(offset, data)
+
+
+@REGISTRY.method
+def write_reg(frame: sch.Schema('Frame'), name: str, value: bytes):
+ """Write a register."""
+ f = find_frame_by_obj(frame)
+ f.select()
+ proc = lldb.selected_process()
+ mname, mval = frame.trace.register_mapper.map_value_back(proc, name, value)
+ reg = find_reg_by_name(f, mname)
+ size = int(lldb.parse_and_eval(f'sizeof(${mname})'))
+ arr = '{' + ','.join(str(b) for b in mval) + '}'
+ util.get_debugger().HandleCommand(f'expr ((unsigned char[{size}])${mname}) = {arr};')
diff --git a/Ghidra/Debug/Debugger-agent-lldb/src/main/py/ghidralldb/parameters.py b/Ghidra/Debug/Debugger-agent-lldb/src/main/py/ghidralldb/parameters.py
new file mode 100644
index 0000000000..2c7a43a676
--- /dev/null
+++ b/Ghidra/Debug/Debugger-agent-lldb/src/main/py/ghidralldb/parameters.py
@@ -0,0 +1,46 @@
+## ###
+# IP: GHIDRA
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+import lldb
+
+# TODO: I don't know how to register a custom parameter prefix. I would rather
+# these were 'ghidra language' and 'ghidra compiler'
+
+
+class GhidraLanguageParameter(lldb.Parameter):
+ """
+ The language id for Ghidra traces. Set this to 'auto' to try to derive it
+ from 'show arch' and 'show endian'. Otherwise, set it to a Ghidra
+ LanguageID.
+ """
+
+ def __init__(self):
+ super().__init__('ghidra-language', lldb.COMMAND_DATA, lldb.PARAM_STRING)
+ self.value = 'auto'
+GhidraLanguageParameter()
+
+
+class GhidraCompilerParameter(lldb.Parameter):
+ """
+ The compiler spec id for Ghidra traces. Set this to 'auto' to try to derive
+ it from 'show osabi'. Otherwise, set it to a Ghidra CompilerSpecID. Note
+ that valid compiler spec ids depend on the language id.
+ """
+
+ def __init__(self):
+ super().__init__('ghidra-compiler', lldb.COMMAND_DATA, lldb.PARAM_STRING)
+ self.value = 'auto'
+GhidraCompilerParameter()
+
diff --git a/Ghidra/Debug/Debugger-agent-lldb/src/main/py/ghidralldb/schema.xml b/Ghidra/Debug/Debugger-agent-lldb/src/main/py/ghidralldb/schema.xml
new file mode 100644
index 0000000000..15d22213ef
--- /dev/null
+++ b/Ghidra/Debug/Debugger-agent-lldb/src/main/py/ghidralldb/schema.xml
@@ -0,0 +1,465 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Ghidra/Debug/Debugger-agent-lldb/src/main/py/ghidralldb/util.py b/Ghidra/Debug/Debugger-agent-lldb/src/main/py/ghidralldb/util.py
new file mode 100644
index 0000000000..2eb1d50442
--- /dev/null
+++ b/Ghidra/Debug/Debugger-agent-lldb/src/main/py/ghidralldb/util.py
@@ -0,0 +1,236 @@
+## ###
+# IP: GHIDRA
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+from collections import namedtuple
+import os
+import re
+import sys
+
+import lldb
+
+
+LldbVersion = namedtuple('LldbVersion', ['full', 'major', 'minor'])
+
+
+def _compute_lldb_ver():
+ blurb = lldb.debugger.GetVersionString()
+ top = blurb.split('\n')[0]
+ full = top.split(' ')[2]
+ major, minor = full.split('.')[:2]
+ return LldbVersion(full, int(major), int(minor))
+
+
+LLDB_VERSION = _compute_lldb_ver()
+
+GNU_DEBUGDATA_PREFIX = ".gnu_debugdata for "
+
+class Module(namedtuple('BaseModule', ['name', 'base', 'max', 'sections'])):
+ pass
+
+
+class Section(namedtuple('BaseSection', ['name', 'start', 'end', 'offset', 'attrs'])):
+ def better(self, other):
+ start = self.start if self.start != 0 else other.start
+ end = self.end if self.end != 0 else other.end
+ offset = self.offset if self.offset != 0 else other.offset
+ attrs = dict.fromkeys(self.attrs)
+ attrs.update(dict.fromkeys(other.attrs))
+ return Section(self.name, start, end, offset, list(attrs))
+
+
+# AFAICT, Objfile does not give info about load addresses :(
+class ModuleInfoReader(object):
+ def name_from_line(self, line):
+ mat = self.objfile_pattern.fullmatch(line)
+ if mat is None:
+ return None
+ n = mat['name']
+ if n.startswith(GNU_DEBUGDATA_PREFIX):
+ return None
+ return None if mat is None else mat['name']
+
+ def section_from_sbsection(self, s):
+ start = s.GetLoadAddress(get_target())
+ if start >= sys.maxsize*2:
+ start = 0
+ end = start + s.GetFileByteSize()
+ offset = s.GetFileOffset()
+ name = s.GetName()
+ attrs = s.GetPermissions()
+ return Section(name, start, end, offset, attrs)
+
+ def finish_module(self, name, sections):
+ alloc = {k: s for k, s in sections.items()}
+ if len(alloc) == 0:
+ return Module(name, 0, 0, alloc)
+ # TODO: This may not be the module base, depending on headers
+ all_zero = True
+ for s in alloc.values():
+ if s.start != 0:
+ all_zero = False
+ if all_zero:
+ base_addr = 0
+ else:
+ base_addr = min(s.start for s in alloc.values() if s.start != 0)
+ max_addr = max(s.end for s in alloc.values())
+ return Module(name, base_addr, max_addr, alloc)
+
+ def get_modules(self):
+ modules = {}
+ name = None
+ sections = {}
+ for i in range(0, get_target().GetNumModules()):
+ module = get_target().GetModuleAtIndex(i)
+ fspec = module.GetFileSpec()
+ name = debracket(fspec.GetFilename())
+ sections = {}
+ for i in range(0, module.GetNumSections()):
+ s = self.section_from_sbsection(module.GetSectionAtIndex(i))
+ sname = debracket(s.name)
+ sections[sname] = s
+ modules[name] = self.finish_module(name, sections)
+ return modules
+
+
+def _choose_module_info_reader():
+ return ModuleInfoReader()
+
+MODULE_INFO_READER = _choose_module_info_reader()
+
+
+
+class Region(namedtuple('BaseRegion', ['start', 'end', 'offset', 'perms', 'objfile'])):
+ pass
+
+
+class RegionInfoReader(object):
+ def region_from_sbmemreg(self, info):
+ start = info.GetRegionBase()
+ end = info.GetRegionEnd()
+ offset = info.GetRegionBase()
+ if offset >= sys.maxsize:
+ offset = 0
+ perms = ""
+ if info.IsReadable():
+ perms += 'r'
+ if info.IsWritable():
+ perms += 'w'
+ if info.IsExecutable():
+ perms += 'x'
+ objfile = info.GetName()
+ return Region(start, end, offset, perms, objfile)
+
+ def get_regions(self):
+ regions = []
+ reglist = get_process().GetMemoryRegions()
+ for i in range(0, reglist.GetSize()):
+ module = get_target().GetModuleAtIndex(i)
+ info = lldb.SBMemoryRegionInfo();
+ success = reglist.GetMemoryRegionAtIndex(i, info);
+ if success:
+ r = self.region_from_sbmemreg(info)
+ regions.append(r)
+ return regions
+
+ def full_mem(self):
+ # TODO: This may not work for Harvard architectures
+ sizeptr = int(parse_and_eval('sizeof(void*)')) * 8
+ return Region(0, 1 << sizeptr, 0, None, 'full memory')
+
+
+def _choose_region_info_reader():
+ return RegionInfoReader()
+
+
+REGION_INFO_READER = _choose_region_info_reader()
+
+
+BREAK_LOCS_CMD = 'breakpoint list {}'
+BREAK_PATTERN = re.compile('')
+BREAK_LOC_PATTERN = re.compile('')
+
+
+class BreakpointLocation(namedtuple('BaseBreakpointLocation', ['address', 'enabled', 'thread_groups'])):
+ pass
+
+
+class BreakpointLocationInfoReader(object):
+ def get_locations(self, breakpoint):
+ return breakpoint.locations
+
+
+def _choose_breakpoint_location_info_reader():
+ return BreakpointLocationInfoReader()
+
+
+BREAKPOINT_LOCATION_INFO_READER = _choose_breakpoint_location_info_reader()
+
+def get_debugger():
+ return lldb.SBDebugger.FindDebuggerWithID(1)
+
+def get_target():
+ return get_debugger().GetTargetAtIndex(0)
+
+def get_process():
+ return get_target().GetProcess()
+
+def selected_thread():
+ return get_process().GetSelectedThread()
+
+def selected_frame():
+ return selected_thread().GetSelectedFrame()
+
+def parse_and_eval(expr, signed=False):
+ if signed is True:
+ return get_target().EvaluateExpression(expr).GetValueAsSigned()
+ return get_target().EvaluateExpression(expr).GetValueAsUnsigned()
+
+def get_eval(expr):
+ return get_target().EvaluateExpression(expr)
+
+def get_description(object, level=None):
+ stream = lldb.SBStream()
+ if level is None:
+ object.GetDescription(stream)
+ else:
+ object.GetDescription(stream, level)
+ return escape_ansi(stream.GetData())
+
+conv_map = {}
+
+def get_convenience_variable(id):
+ #val = get_target().GetEnvironment().Get(id)
+ if id not in conv_map:
+ return "auto"
+ val = conv_map[id]
+ if val is None:
+ return "auto"
+ return val
+
+def set_convenience_variable(id, value):
+ #env = get_target().GetEnvironment()
+ #return env.Set(id, value, True)
+ conv_map[id] = value
+
+
+def escape_ansi(line):
+ ansi_escape =re.compile(r'(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]')
+ return ansi_escape.sub('', line)
+
+def debracket(init):
+ val = init
+ val = val.replace("[","(")
+ val = val.replace("]",")")
+ return val
diff --git a/Ghidra/Debug/Debugger-agent-lldb/src/main/py/pyproject.toml b/Ghidra/Debug/Debugger-agent-lldb/src/main/py/pyproject.toml
new file mode 100644
index 0000000000..216e155f45
--- /dev/null
+++ b/Ghidra/Debug/Debugger-agent-lldb/src/main/py/pyproject.toml
@@ -0,0 +1,25 @@
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "ghidralldb"
+version = "10.4"
+authors = [
+ { name="Ghidra Development Team" },
+]
+description = "Ghidra's Plugin for lldb"
+readme = "README.md"
+requires-python = ">=3.7"
+classifiers = [
+ "Programming Language :: Python :: 3",
+ "License :: OSI Approved :: Apache Software License",
+ "Operating System :: OS Independent",
+]
+dependencies = [
+ "ghidratrace==10.4",
+]
+
+[project.urls]
+"Homepage" = "https://github.com/NationalSecurityAgency/ghidra"
+"Bug Tracker" = "https://github.com/NationalSecurityAgency/ghidra/issues"
diff --git a/Ghidra/Debug/Debugger-agent-lldb/src/test/java/agent/lldb/model/MacOSSpecimen.java b/Ghidra/Debug/Debugger-agent-lldb/src/test/java/agent/lldb/model/MacOSSpecimen.java
index efffc2ddbf..2b7fa43ee1 100644
--- a/Ghidra/Debug/Debugger-agent-lldb/src/test/java/agent/lldb/model/MacOSSpecimen.java
+++ b/Ghidra/Debug/Debugger-agent-lldb/src/test/java/agent/lldb/model/MacOSSpecimen.java
@@ -31,54 +31,54 @@ import ghidra.dbg.testutil.DummyProc;
public enum MacOSSpecimen implements DebuggerTestSpecimen, DebuggerModelTestUtils {
SPIN {
@Override
- String getCommandLine() {
+ public String getCommandLine() {
return DummyProc.which("expSpin");
}
},
FORK_EXIT {
@Override
- String getCommandLine() {
+ public String getCommandLine() {
return DummyProc.which("expFork");
}
},
CLONE_EXIT {
@Override
- String getCommandLine() {
+ public String getCommandLine() {
return DummyProc.which("expCloneExit");
}
},
PRINT {
@Override
- String getCommandLine() {
+ public String getCommandLine() {
return DummyProc.which("expPrint");
}
},
REGISTERS {
@Override
- String getCommandLine() {
+ public String getCommandLine() {
return DummyProc.which("expRegisters");
}
},
STACK {
@Override
- String getCommandLine() {
+ public String getCommandLine() {
return DummyProc.which("expStack");
}
},
CREATE_PROCESS {
@Override
- String getCommandLine() {
+ public String getCommandLine() {
return DummyProc.which("expCreateProcess");
}
},
CREATE_THREAD_EXIT {
@Override
- String getCommandLine() {
+ public String getCommandLine() {
return DummyProc.which("expCreateThreadExit");
}
};
- abstract String getCommandLine();
+ public abstract String getCommandLine();
@Override
public DummyProc runDummy() throws Throwable {
@@ -117,24 +117,19 @@ public enum MacOSSpecimen implements DebuggerTestSpecimen, DebuggerModelTestUtil
}
@Override
- public boolean isRunningIn(TargetProcess process, AbstractDebuggerModelTest test)
- throws Throwable {
+ public boolean isRunningIn(TargetProcess process, AbstractDebuggerModelTest test) throws Throwable {
// NB. ShellUtils.parseArgs removes the \s. Not good.
String expected = getBinModuleName();
TargetObject session = process.getParent().getParent();
- Collection modules =
- test.m.findAll(TargetModule.class, session.getPath(), true).values();
- return modules.stream()
- .anyMatch(m -> expected.equalsIgnoreCase(getShortName(m.getModuleName())));
+ Collection modules = test.m.findAll(TargetModule.class, session.getPath(), true).values();
+ return modules.stream().anyMatch(m -> expected.equalsIgnoreCase(getShortName(m.getModuleName())));
}
@Override
- public boolean isAttachable(DummyProc dummy, TargetAttachable attachable,
- AbstractDebuggerModelTest test) throws Throwable {
+ public boolean isAttachable(DummyProc dummy, TargetAttachable attachable, AbstractDebuggerModelTest test)
+ throws Throwable {
waitOn(attachable.fetchAttributes());
- long pid =
- attachable.getTypedAttributeNowByName(LldbModelTargetAvailable.PID_ATTRIBUTE_NAME,
- Long.class, -1L);
+ long pid = attachable.getTypedAttributeNowByName(LldbModelTargetAvailable.PID_ATTRIBUTE_NAME, Long.class, -1L);
return pid == dummy.pid;
}
}
diff --git a/Ghidra/Debug/Debugger-gadp/build.gradle b/Ghidra/Debug/Debugger-gadp/build.gradle
index f4dbd3cd0c..9e1c57fafe 100644
--- a/Ghidra/Debug/Debugger-gadp/build.gradle
+++ b/Ghidra/Debug/Debugger-gadp/build.gradle
@@ -13,97 +13,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-/*plugins {
- id 'com.google.protobuf' version '0.8.10'
-}*/
apply from: "${rootProject.projectDir}/gradle/javaProject.gradle"
apply from: "${rootProject.projectDir}/gradle/jacocoProject.gradle"
apply from: "${rootProject.projectDir}/gradle/javaTestProject.gradle"
apply from: "${rootProject.projectDir}/gradle/distributableGhidraModule.gradle"
+apply from: "${rootProject.projectDir}/gradle/debugger/hasProtobuf.gradle"
apply plugin: 'eclipse'
eclipse.project.name = 'Debug Debugger-gadp'
-configurations {
- allProtocArtifacts
- protocArtifact
-}
-
-def platform = getCurrentPlatformName()
-
dependencies {
- allProtocArtifacts 'com.google.protobuf:protoc:3.21.8:windows-x86_64@exe'
- allProtocArtifacts 'com.google.protobuf:protoc:3.21.8:linux-x86_64@exe'
- allProtocArtifacts 'com.google.protobuf:protoc:3.21.8:linux-aarch_64@exe'
- allProtocArtifacts 'com.google.protobuf:protoc:3.21.8:osx-x86_64@exe'
- allProtocArtifacts 'com.google.protobuf:protoc:3.21.8:osx-aarch_64@exe'
-
- if (isCurrentWindows()) {
- protocArtifact 'com.google.protobuf:protoc:3.21.8:windows-x86_64@exe'
- }
- if (isCurrentLinux()) {
- if (platform.endsWith("x86_64")) {
- protocArtifact 'com.google.protobuf:protoc:3.21.8:linux-x86_64@exe'
- }
- else {
- protocArtifact 'com.google.protobuf:protoc:3.21.8:linux-aarch_64@exe'
- }
- }
- if (isCurrentMac()) {
- if (platform.endsWith("x86_64")) {
- protocArtifact 'com.google.protobuf:protoc:3.21.8:osx-x86_64@exe'
- }
- else {
- protocArtifact 'com.google.protobuf:protoc:3.21.8:osx-aarch_64@exe'
- }
- }
-
api project(':Framework-AsyncComm')
api project(':Framework-Debugging')
api project(':ProposedUtils')
-
+
testImplementation project(path: ':Framework-AsyncComm', configuration: 'testArtifacts')
testImplementation project(path: ':Framework-Debugging', configuration: 'testArtifacts')
}
-
-/*protobuf {
- protoc {
- artifact = 'com.google.protobuf:protoc:3.21.8'
- }
-}*/
-
-task generateProto {
- ext.srcdir = file("src/main/proto")
- ext.src = fileTree(srcdir) {
- include "**/*.proto"
- }
- ext.outdir = file("build/generated/source/proto/main/java")
- outputs.dir(outdir)
- inputs.files(src)
- dependsOn(configurations.protocArtifact)
- doLast {
- def exe = configurations.protocArtifact.first()
- if (!isCurrentWindows()) {
- exe.setExecutable(true)
- }
- exec {
- commandLine exe, "--java_out=$outdir", "-I$srcdir"
- args src
- }
- }
-}
-
-tasks.compileJava.dependsOn(tasks.generateProto)
-tasks.eclipse.dependsOn(tasks.generateProto)
-rootProject.tasks.prepDev.dependsOn(tasks.generateProto)
-
-sourceSets {
- main {
- java {
- srcDir tasks.generateProto.outdir
- }
- }
-}
-zipSourceSubproject.dependsOn generateProto
-
diff --git a/Ghidra/Debug/Debugger-isf/build.gradle b/Ghidra/Debug/Debugger-isf/build.gradle
index 6bf945c2e6..d135294a0a 100644
--- a/Ghidra/Debug/Debugger-isf/build.gradle
+++ b/Ghidra/Debug/Debugger-isf/build.gradle
@@ -18,91 +18,24 @@ apply from: "${rootProject.projectDir}/gradle/javaProject.gradle"
apply from: "${rootProject.projectDir}/gradle/jacocoProject.gradle"
apply from: "${rootProject.projectDir}/gradle/javaTestProject.gradle"
apply from: "${rootProject.projectDir}/gradle/distributableGhidraModule.gradle"
+apply from: "${rootProject.projectDir}/gradle/debugger/hasProtobuf.gradle"
apply plugin: 'eclipse'
eclipse.project.name = 'Debug Debugger-isf'
-configurations {
- allProtocArtifacts
- protocArtifact
-}
-
-def platform = getCurrentPlatformName()
-
dependencies {
- allProtocArtifacts 'com.google.protobuf:protoc:3.21.8:windows-x86_64@exe'
- allProtocArtifacts 'com.google.protobuf:protoc:3.21.8:linux-x86_64@exe'
- allProtocArtifacts 'com.google.protobuf:protoc:3.21.8:linux-aarch_64@exe'
- allProtocArtifacts 'com.google.protobuf:protoc:3.21.8:osx-x86_64@exe'
- allProtocArtifacts 'com.google.protobuf:protoc:3.21.8:osx-aarch_64@exe'
-
- if (isCurrentWindows()) {
- protocArtifact 'com.google.protobuf:protoc:3.21.8:windows-x86_64@exe'
- }
- if (isCurrentLinux()) {
- if (platform.endsWith("x86_64")) {
- protocArtifact 'com.google.protobuf:protoc:3.21.8:linux-x86_64@exe'
- }
- else {
- protocArtifact 'com.google.protobuf:protoc:3.21.8:linux-aarch_64@exe'
- }
- }
- if (isCurrentMac()) {
- if (platform.endsWith("x86_64")) {
- protocArtifact 'com.google.protobuf:protoc:3.21.8:osx-x86_64@exe'
- }
- else {
- protocArtifact 'com.google.protobuf:protoc:3.21.8:osx-aarch_64@exe'
- }
- }
-
api project(':Framework-AsyncComm')
api project(':Framework-Debugging')
api project(':ProposedUtils')
-
+
testImplementation project(path: ':Framework-AsyncComm', configuration: 'testArtifacts')
testImplementation project(path: ':Framework-Debugging', configuration: 'testArtifacts')
}
-task generateProto {
- ext.srcdir = file("src/main/proto")
- ext.src = fileTree(srcdir) {
- include "**/*.proto"
- }
- ext.outdir = file("build/generated/source/proto/main/java")
- outputs.dir(outdir)
- inputs.files(src)
- dependsOn(configurations.protocArtifact)
- doLast {
- def exe = configurations.protocArtifact.first()
- if (!isCurrentWindows()) {
- exe.setExecutable(true)
- }
- exec {
- commandLine exe, "--java_out=$outdir", "-I$srcdir"
- args src
- }
- }
-}
-
-tasks.compileJava.dependsOn(tasks.generateProto)
-tasks.eclipse.dependsOn(tasks.generateProto)
-rootProject.tasks.prepDev.dependsOn(tasks.generateProto)
-
-sourceSets {
- main {
- java {
- srcDir tasks.generateProto.outdir
- }
- }
-}
-zipSourceSubproject.dependsOn generateProto
-
// Include buildable native source in distribution
rootProject.assembleDistribution {
- from (this.project.projectDir.toString()) {
+ from (this.project.projectDir.toString()) {
include "runISFServer"
into { getZipPath(this.project) }
- }
+ }
}
-
diff --git a/Ghidra/Debug/Debugger-rmi-trace/DEVNOTES.txt b/Ghidra/Debug/Debugger-rmi-trace/DEVNOTES.txt
new file mode 100644
index 0000000000..a96a04bdf8
--- /dev/null
+++ b/Ghidra/Debug/Debugger-rmi-trace/DEVNOTES.txt
@@ -0,0 +1,280 @@
+This is just a scratchpad of notes for development.
+After developer documentation is authored, this file should be deleted.
+
+Terminology can be a bit weird regarding client vs server.
+Instead, I prefer to use "front end" and "back end".
+Ghidra is always the front end, as it provides the UI.
+The actual debugger is always the "back end" is it provides the actual instrumentation and access to the target.
+wrt/ TCP, the connection can go either way, but once established, Ghidra still plays the front end role.
+
+Client/Server otherwise depends on context.
+For the trace-recording channel, the back-end is the client, and the front-end (Ghidra) is the server.
+The back-end invokes remote methods on the DBTrace, and those cause DomainObjectChange events, updating the UI.
+The front-end replies with minimal information.
+ (More on this and sync/async/batching later)
+
+For the command channel, the front-end (Ghidra) is the client, and the back-end is the server.
+The user presses a button, which invokes a remote method on the back-end.
+Often, that method and/or its effects on the target and back-end result in it updating the trace, and the loop is complete.
+Again, the back-end replies with minimal information.
+ One notable exception is the `execute` method, which can optionally return captured console output.
+ In general, methods should only respond with actual information that doesn't belong in the trace.
+ While I've not yet needed this, I suppose another exception could be for methods that want to return the path to an object, to clarify association of cause and effect.
+
+Regarding sync/async and batching:
+One of the goals of TraceRmi was to simplify the trace-recording process.
+It does this in three ways:
+
+1. Providing direct control to write the Trace database.
+ The ObjectModel approach was more descriptive.
+ It would announce the existence of things, and a recorder at the front end would decide (applying some arcane rules) what to record and display.
+ Almost every new model required some adjustment to the recorder.
+
+2. Changing to a synchronous RMI scheme.
+ The decision to use an asynchronous scheme was to avoid accidental lock-ups of the Swing thread.
+ In practice, it just poisoned every API that depended on it, and we still got Swing lock-ups.
+ And worse, they were harder to diagnose, because the stack traces were obscured.
+ And still worse, execution order and threading was difficult to predict.
+
+ We've only been somewhat successful in changing to a fully synchronous scheme, but even then, we've (attempted to) mitigate each of the above complaints.
+ On the front-end, the internals still use CompletableFuture, but we're more apt to use .get(), which keeps the stack together on the thread waiting for the result.
+ In essence, there's little difference in blocking on .get() vs blocking on .recv().
+ The reason we need a dedicated background thread to receive is to sort out the two channels.
+ The recommended public API method is RemoteMethod.invoke(), which uses .get() internally, so this is mostly transparent, except when debugging the front end.
+ There is still an .invokeAsync(), if desired, giving better control of timeouts, which is actually a feature we would not have using a purely synchronous .recv() (at least not without implementing non-blocking IO)
+ To mitigate Swing lock-ups the .get() methods are overridden to explicitly check for the Swing thread.
+
+ On the back end, the internals work similarly to the front end.
+ We use a Future to handle waiting for the result, and the implementation of each trace modification method will immediately invoke .result().
+ Unfortunately, this does slow things down far too much, since every miniscule operation requires a round trip.
+ We mitigate this by implementing a `batch` context manager.
+ Inside this context, most of the trace modification methods will now return the Future.
+ However, a reference to each such future is stored off in the context.
+ When the context is exited, all the Futures' results are waited on.
+ This maintains a mostly synchronous behavior, while alleviating the repeated round-trip costs.
+
+3. Simplifying the back end implementation, and providing it in Python.
+ It turns out no debugger we've encountered up to this point provides Java language bindings out of the box.
+ The closest we've seen is LLDB, which has specified their interfaces using SWIG, which lent itself to exporting Java bindings.
+ And that was lucky, too, because accessing C++ virtual functions from JNA is fraught with peril.
+ For gdb, we've been using a pseudo-terminal or ssh connection to its Machine Interface, which aside from the piping delays, has been pretty nice.
+ It's not been great on Windows, though -- their ConPTY stuff has some ANSI oddities, the handling of which has slowed our performance.
+ For dbgeng/dbgmodel, we've been fortunate that they follow COM+, which is fairly well understood by JNA.
+ Nevertheless, all of these have required us to hack some kind of native bindings in Java.
+ This introduces risks of crashing the JVM, and in some cases can cause interesting conflicts, e.g., the JVM and dbgeng may try to handle the same signals differently.
+ dbgeng also only allows a single session.
+ If the user connects twice to it using IN-VM (this is easy to do by accident), then the two connections are aliases of the same dbgeng session.
+
+ Both gdb and lldb offer Python bindings, so it is an obvious choice for back end implementations.
+ We are already using protobuf, so we keep it, but developed a new protocol specification.
+ The trace modification methods are prescribed by Ghidra, so each is implemented specifically in the trace client.
+ The back end remote methods are described entirely by the back end.
+ They are enumerated during connection negotiation; otherwise, there is only one generic "Invoke" message.
+
+
+Because we're more tightly integrated with the debugger, there may be some interesting caveats.
+
+Pay careful attention to synchronization and session tear down.
+At one point, I was using gdb's post_event as a Python Executor.
+A separate thread handled the method invocation requests, scheduled it on the executor, waited for the result, and then responded.
+This worked until the front end invoked `execute("quit")`.
+I was expecting gdb to just quit, and the front end would expect the connection to die.
+However, this never happened.
+Instead, during execution of the `quit`, gdb wanted to clean up the Python interpreter.
+Part of that was gracefully cleaning up all the Python threads, one of which was blocking indefinitely on execution of the `quit`.
+Thus, the two threads were waiting on each other, and gdb locked up.
+
+Depending on the debugger, the Python API may be more or less mature, and there could be much variation among versions we'd like to support.
+For retrieving information, we at least have console capture as a fallback; however, there's not always a reliable way to detect certain events without a direct callback.
+At worst, we can always hook something like `prompt`, but if we do, we must be quick in our checks.
+Dealing with multiple versions, there's at least two ways:
+1. Probe for the feature.
+ This is one case where Python's dynamic nature helps out.
+ Use `hasattr` to check for the existence of various features and choose accordingly.
+2. Check the version string.
+ Assuming version information can be consistently and reliably retrieved across all the supported versions, parse it first thing.
+ If the implementation of a feature various across versions, the appropriate one can be selected.
+ This may not work well for users of development branches, or are otherwise off the standard releases of their debuggers.
+
+This is probably well understood by the Python community, but I'll overstate it here:
+If you've written something, but you haven't unit tested it yet, then you haven't really written it.
+This may be mitigated by some static analysis tools and type annotations, but I didn't use them.
+In fact, you might even say I abused type annotations for remote method specifications.
+
+For gdb, I did all of my unit testing using JUnit as the front end in Java.
+This is perhaps not ideal, since this is inherently an integration test; nevertheless, it does allow me to test each intended feature of the back end separately.
+
+
+# Package installation
+
+I don't know what the community preference will be here, but now that we're playing in the Python ecosystem, we have to figure out how to play nicely.
+Granted, some of this depends on how nicely the debugger plays in the Python ecosystem.
+My current thought is distribute our stuff as Python packages, and let the user figure it out.
+We'll still want to figure out the best way, if possible, to make things work out of the box.
+Nevertheless, a `pip install` command may not be *that* offensive for a set-up step.
+
+That said, for unit testing, I've had to incorporate package installation as a @BeforeClass method.
+There's probably a better way, and that way may also help with out-of-the-box support.
+Something like setting PYTHON_PATH before invoking the debugger?
+There's still the issue of installing protobuf, though.
+And the version we use is not the latest, which may put users who already have protobuf in dependency hell.
+We use version 3.20, while the latest is 4.something.
+According to protobuf docs, major versions are not guaranteed backward compatible.
+To upgrade, we'd also have to upgrade the Java side.
+
+# Protobuf headaches
+
+Protobufs in Java have these nifty `writeDelimitedTo` and `parseDelimitedFrom` methods.
+There's no equivalent for Python :(
+That said, according to a stackoverflow post (which I've lost track of, but it's easily confirmed by examining protobufs Java source), you can hand-spin this by prepending a varint giving each message's length.
+If only the varint codec were part of protobuf's public Python API....
+They're pretty easily accessed in Python by importing the `internal` package, but that's probably not a good idea.
+Also, (as I had been doing that), it's easy to goof up receiving just variable-length int and keeping the encoded message in tact for parsing.
+I instead just use a fixed 32-bit int now.
+
+# How-To?
+
+For now, I'd say just the the gdb implementation as a template / guide.
+Just beware, the whole thing is a bit unstable, so the code may change, but still, I don't expect it to change so drastically that integration work would be scrapped.
+
+If you're writing Python, create a Python package following the template for gdb's.
+I'd like the version numbers to match Ghidra's, though this may need discussion.
+Currently, only Python 3 is supported.
+I expect older versions of gdb may not support Py3, so we may need some backporting.
+That said, if your distro's package for whatever debugger is compiled for Py2, you may need to build from source, assuming it supports Py3 at all.
+I recommend mirroring the file layout:
+
+__init__.py:
+ Python package marker, but also initialization.
+ For gdb, this file gets executed when the user types `python import ghidragdb`.
+ Thus, that's how they load the extension.
+arch.py:
+ Utilities for mapping architecture-specific things between back and front ends.
+ Technically, you should just be able to use the "DATA" processor for your trace, things will generally work better if you can map.
+commands.py:
+ These are commands we add to the debugger's CLI.
+ For gdb, we use classes that extend `gdb.Command`, which allows the user to access them whether or not connected to Ghidra.
+ For now, this is the recommendation, as I expect it'll allow users to "hack" on it more easily, either to customize or to retrieve diagnostics, etc.
+ Notice that I use gdb's expression evaluator wherever that can enhance the command's usability, e.g., `ghidra trace putval`
+hooks.py:
+ These are event callbacks from the debugger as well as whatever plumbing in necessary to actually install them.
+ That "plumbing" may vary, since the debugger may not directly support the callback you're hoping for.
+ In gdb, there are at least 3 flavors:
+ 1. A directly-supported callback, i.e., in `gdb.events`
+ 2. A breakpoint callback, which also breaks down into two sub-flavors:
+ * Internal breakpoint called back via `gdb.Breakpoint.stop`
+ * Normal breakpoint whose commands invoke a CLI command
+ 3. A hooked command to invoke a CLI command, e.g., `define hook-inferior`
+method.py:
+ These are remote methods available to the front end.
+ See the `MethodRegistry` object in the Python implementation, or the `RemoteMethod` interface in the Java implementation.
+parameters.py:
+ These are for gdb parameters, which may not map to anything in your debugger, so adjust as necessary.
+ They're preferred to custom commands whose only purpose is to access a variable.
+schema.xml:
+ This is exactly what you think it is.
+ It is recommended you copy this directly from the ObjectModel-based implementation and make adjustments as needed.
+ See `commands.start_trace` to see how to load this file from your Python package.
+util.py:
+ Just utilities and such.
+ For the gdb connector, this is where I put my version-specific implementations, e.g., to retrieve the memory map and module list.
+
+For testing, similarly copy the JUnit tests (they're in the IntegrationTests project) into a separate properly named package.
+I don't intend to factor out test cases, except for a few utilities.
+The only real service that did in the past was to remind you what cases you ought to test.
+Prescribing exactly *how* to test those and the scenarios, I think, was a mistake.
+If I provide a base test class, it might just be to name some methods that all fail by default.
+Then, as a tester, the failures would remind you to override each method with the actual test code.
+
+
+For manual testing, I've used two methods
+1. See `GdbCommandsTest#testManual`.
+ Uncomment it to have JUnit start a trace-rmi front-end listener.
+ You can then manually connect from inside your debugger and send/diagnose commands one at a time.
+ Typically, I'd use the script from another test that was giving me trouble.
+2. Start the full Ghidra Debugger and use a script to connect.
+ At the moment, there's little UI integration beyond what is already offered by viewing a populated trace.
+ Use either ConnectTraceRmiScript or ListenTraceRmiScript and follow the prompts / console.
+ The handler will activate the trace when commanded, and it will follow the latest snapshot.
+
+
+# User installation instructions:
+
+The intent is to provide .whl or whatever Python packages as part of the Ghidra distribution.
+A user should be able to install them using `pip3 install ...`, however:
+We've recently encountered issues where the version of Python that gdb is linked to may not be the same version of Python the user gets when the type `python`, `python3` or `pip3`.
+To manually check for this version, a user must type, starting in their shell:
+
+```bash
+gdb
+python-interactive
+import sys
+print(sys.version)
+```
+
+Suppose they get `3.8.10`.
+They'd then take the major and minor numbers to invoke `python3.8` directly:
+
+```bash
+python3.8 -m pip install ...
+```
+
+A fancy way to just have gdb print the python command for you is:
+
+```bash
+gdb --batch -ex 'python import sys' -ex 'python print(f"python{sys.version_info.major}.{sys.version_info.minor}")'
+```
+
+Regarding method registry, the executor has to be truly asynchronous.
+You cannot just invoke the method synchronously and return a completed future.
+If you do, you'll hang the message receiver thread, which may need to be free if the invoked method interacts with the trace.
+
+We've currently adopted a method-naming convention that aims for a somewhat consistent API across back-end plugins.
+In general, the method name should match the action name exactly, e.g., the method corresponding the Ghidra's `resume` action should be defined as:
+
+ @REGISTRY.method
+ def resume(...):
+ ...
+
+Not:
+
+ @REGISTRY.method(name='continue', action='resume')
+ def _continue(...):
+ ...
+
+Even though the back-end's command set and/or API may call it "continue."
+If you would like to provide a hint to the user regarding the actual back-end command, do so in the method's docstring:
+
+ @REGISTRY.method
+ def resume(...):
+ """Continue execution of the current target (continue)."""
+ ...
+
+There are exceptions:
+
+1. When there is not a one-to-one mapping from the method to an action.
+ This is usually the case for delete, toggle, refresh, etc.
+ For these, use the action as the prefix, and then some suffix, usually describing the type of object affected, e.g., delete_breakpoint.
+2. When using an "_ext" class of action, e.g., step_ext or break_ext.
+ There is almost certainly not a one-to-one method for such an action.
+ The naming convention is the same as 1, but omitting the "_ext", e.g., step_advance or break_event
+ Even if you only have one method that maps to step_ext, the method should *never* be called step_ext.
+3. There is no corresponding action at all.
+ In this case, call it what you want, but strive for consistency among related methods in this category for your back-end.
+ Act as though there could one day be a Ghidra action that you'd like to map them to.
+
+There may be some naming you find annoying, e.g., "resume" (not "continue") or "launch" (not "start")
+We also do not use the term "watchpoint." We instead say "write breakpoint."
+Thus, the method for placing one is named `break_write_whatever`, not `watch_whatever`.
+
+
+# Regarding transactions:
+
+At the moment, I've defined two modes for transaction management on the client side.
+The server side couldn't care less. A transactions is a transaction.
+For hooks, i.e., things driven by events on the back end, use the client's transaction manager directly.
+For commands, i.e., things driven by the user via the CLI, things are a little dicey.
+I wouldn't expect the user to manage multiple transaction objects.
+The recommendation is that the CLI can have at most one active transaction.
+For the user to open a second transaction may be considered an error.
+Take care as you're coding (and likely re-using command logic) that you don't accidentally take or otherwise conflict with the CLI's transaction manager when processing an event.
diff --git a/Ghidra/Debug/Debugger-rmi-trace/Module.manifest b/Ghidra/Debug/Debugger-rmi-trace/Module.manifest
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/Ghidra/Debug/Debugger-rmi-trace/build.gradle b/Ghidra/Debug/Debugger-rmi-trace/build.gradle
new file mode 100644
index 0000000000..eec00786ef
--- /dev/null
+++ b/Ghidra/Debug/Debugger-rmi-trace/build.gradle
@@ -0,0 +1,56 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+apply from: "${rootProject.projectDir}/gradle/javaProject.gradle"
+apply from: "${rootProject.projectDir}/gradle/jacocoProject.gradle"
+apply from: "${rootProject.projectDir}/gradle/javaTestProject.gradle"
+apply from: "${rootProject.projectDir}/gradle/distributableGhidraModule.gradle"
+apply from: "${rootProject.projectDir}/gradle/debugger/hasProtobuf.gradle"
+apply from: "${rootProject.projectDir}/gradle/debugger/hasPythonPackage.gradle"
+
+apply plugin: 'eclipse'
+eclipse.project.name = 'Debug Debugger-rmi-trace'
+
+dependencies {
+ api project(':Debugger')
+}
+
+task generateProtoPy {
+ ext.srcdir = file("src/main/proto")
+ ext.src = fileTree(srcdir) {
+ include "**/*.proto"
+ }
+ ext.outdir = file("build/generated/source/proto/main/py")
+ outputs.dir(outdir)
+ inputs.files(src)
+ dependsOn(configurations.protocArtifact)
+ doLast {
+ def exe = configurations.protocArtifact.first()
+ if (!isCurrentWindows()) {
+ exe.setExecutable(true)
+ }
+ exec {
+ commandLine exe, "--python_out=$outdir", "-I$srcdir"
+ args src
+ }
+ }
+}
+
+tasks.assemblePyPackage {
+ from(generateProtoPy) {
+ into "src/ghidratrace"
+ }
+}
diff --git a/Ghidra/Debug/Debugger-rmi-trace/certification.manifest b/Ghidra/Debug/Debugger-rmi-trace/certification.manifest
new file mode 100644
index 0000000000..9a07dbeaad
--- /dev/null
+++ b/Ghidra/Debug/Debugger-rmi-trace/certification.manifest
@@ -0,0 +1,7 @@
+##VERSION: 2.0
+DEVNOTES.txt||GHIDRA||||END|
+Module.manifest||GHIDRA||||END|
+src/main/py/LICENSE||GHIDRA||||END|
+src/main/py/README.md||GHIDRA||||END|
+src/main/py/pyproject.toml||GHIDRA||||END|
+src/main/py/tests/EMPTY||GHIDRA||||END|
diff --git a/Ghidra/Debug/Debugger-rmi-trace/ghidra_scripts/ConnectTraceRmiScript.java b/Ghidra/Debug/Debugger-rmi-trace/ghidra_scripts/ConnectTraceRmiScript.java
new file mode 100644
index 0000000000..7434cf48a9
--- /dev/null
+++ b/Ghidra/Debug/Debugger-rmi-trace/ghidra_scripts/ConnectTraceRmiScript.java
@@ -0,0 +1,48 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import java.net.InetSocketAddress;
+import java.util.Map;
+import java.util.Objects;
+
+import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiHandler;
+import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiPlugin;
+import ghidra.app.script.GhidraScript;
+import ghidra.app.services.TraceRmiService;
+
+public class ConnectTraceRmiScript extends GhidraScript {
+
+ TraceRmiService getService() throws Exception {
+ TraceRmiService service = state.getTool().getService(TraceRmiService.class);
+ if (service != null) {
+ return service;
+ }
+ state.getTool().addPlugin(TraceRmiPlugin.class.getName());
+ return Objects.requireNonNull(state.getTool().getService(TraceRmiService.class));
+ }
+
+ @Override
+ protected void run() throws Exception {
+ TraceRmiService service = getService();
+ TraceRmiHandler handler = service.connect(
+ new InetSocketAddress(askString("Trace RMI", "hostname", "localhost"), askInt("Trace RMI", "port")));
+ println("Connected");
+ handler.start();
+
+// if (askYesNo("Execute?", "Execute 'echo test'?")) {
+// handler.getMethods().get("execute").invoke(Map.of("cmd", "script print('test')"));
+// }
+ }
+}
diff --git a/Ghidra/Debug/Debugger-rmi-trace/ghidra_scripts/ListenTraceRmiScript.java b/Ghidra/Debug/Debugger-rmi-trace/ghidra_scripts/ListenTraceRmiScript.java
new file mode 100644
index 0000000000..af21cd9c7e
--- /dev/null
+++ b/Ghidra/Debug/Debugger-rmi-trace/ghidra_scripts/ListenTraceRmiScript.java
@@ -0,0 +1,48 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import java.util.Map;
+import java.util.Objects;
+
+import ghidra.app.plugin.core.debug.service.rmi.trace.*;
+import ghidra.app.script.GhidraScript;
+import ghidra.app.services.TraceRmiService;
+
+public class ListenTraceRmiScript extends GhidraScript {
+
+ TraceRmiService getService() throws Exception {
+ TraceRmiService service = state.getTool().getService(TraceRmiService.class);
+ if (service != null) {
+ return service;
+ }
+ state.getTool().addPlugin(TraceRmiPlugin.class.getName());
+ return Objects.requireNonNull(state.getTool().getService(TraceRmiService.class));
+ }
+
+ @Override
+ protected void run() throws Exception {
+ TraceRmiService service = getService();
+
+ TraceRmiAcceptor acceptor = service.acceptOne(null);
+ println("Listening at " + acceptor.getAddress());
+ TraceRmiHandler handler = acceptor.accept();
+ println("Connection from " + handler.getRemoteAddress());
+ handler.start();
+
+ while (askYesNo("Execute?", "Execute 'echo test'?")) {
+ handler.getMethods().get("execute").invoke(Map.of("cmd", "echo test"));
+ }
+ }
+}
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/OpenTrace.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/OpenTrace.java
new file mode 100644
index 0000000000..7ab709aa93
--- /dev/null
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/OpenTrace.java
@@ -0,0 +1,107 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.plugin.core.debug.service.rmi.trace;
+
+import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiHandler.*;
+import ghidra.program.model.address.*;
+import ghidra.program.model.lang.Register;
+import ghidra.rmi.trace.TraceRmi.*;
+import ghidra.trace.model.Trace;
+import ghidra.trace.model.target.TraceObject;
+import ghidra.trace.model.time.TraceSnapshot;
+
+class OpenTrace implements ValueDecoder {
+ final DoId doId;
+ final Trace trace;
+ TraceSnapshot lastSnapshot;
+
+ OpenTrace(DoId doId, Trace trace) {
+ this.doId = doId;
+ this.trace = trace;
+ }
+
+ public TraceSnapshot createSnapshot(Snap snap, String description) {
+ TraceSnapshot snapshot = trace.getTimeManager().getSnapshot(snap.getSnap(), true);
+ snapshot.setDescription(description);
+ return this.lastSnapshot = snapshot;
+ }
+
+ public TraceObject getObject(long id, boolean required) {
+ TraceObject object = trace.getObjectManager().getObjectById(id);
+ if (object == null) {
+ throw new InvalidObjIdError();
+ }
+ return object;
+ }
+
+ public TraceObject getObject(ObjPath path, boolean required) {
+ TraceObject object =
+ trace.getObjectManager().getObjectByCanonicalPath(TraceRmiHandler.toKeyPath(path));
+ if (required && object == null) {
+ throw new InvalidObjPathError();
+ }
+ return object;
+ }
+
+ @Override
+ public TraceObject getObject(ObjDesc desc, boolean required) {
+ return getObject(desc.getId(), required);
+ }
+
+ @Override
+ public TraceObject getObject(ObjSpec object, boolean required) {
+ return switch (object.getKeyCase()) {
+ case KEY_NOT_SET -> throw new TraceRmiError("Must set id or path");
+ case ID -> getObject(object.getId(), required);
+ case PATH -> getObject(object.getPath(), required);
+ default -> throw new AssertionError();
+ };
+ }
+
+ public AddressSpace getSpace(String name, boolean required) {
+ AddressSpace space = trace.getBaseAddressFactory().getAddressSpace(name);
+ if (required && space == null) {
+ throw new NoSuchAddressSpaceError();
+ }
+ return space;
+ }
+
+ @Override
+ public Address toAddress(Addr addr, boolean required) {
+ AddressSpace space = getSpace(addr.getSpace(), required);
+ return space.getAddress(addr.getOffset());
+ }
+
+ @Override
+ public AddressRange toRange(AddrRange range, boolean required)
+ throws AddressOverflowException {
+ AddressSpace space = getSpace(range.getSpace(), required);
+ if (space == null) {
+ return null;
+ }
+ Address min = space.getAddress(range.getOffset());
+ Address max = space.getAddress(range.getOffset() + range.getExtend());
+ return new AddressRangeImpl(min, max);
+ }
+
+ public Register getRegister(String name, boolean required) {
+ Register register = trace.getBaseLanguage().getRegister(name);
+ if (required && register == null) {
+ throw new InvalidRegisterError(name);
+ }
+ return register;
+ }
+}
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/RemoteAsyncResult.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/RemoteAsyncResult.java
new file mode 100644
index 0000000000..ff8fd132c5
--- /dev/null
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/RemoteAsyncResult.java
@@ -0,0 +1,70 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.plugin.core.debug.service.rmi.trace;
+
+import java.util.concurrent.*;
+
+import ghidra.trace.model.target.TraceObject;
+import ghidra.util.Swing;
+
+/**
+ * The future result of invoking a {@link RemoteMethod}.
+ *
+ *
+ * While this can technically result in an object, returning values from remote methods is highly
+ * discouraged. This has led to several issues in the past, including duplication of information
+ * (and a lot of it) over the connection. Instead, most methods should just update the trace
+ * database, and the client can retrieve the relevant information from it. One exception might be
+ * the {@code execute} method. This is typically for executing a CLI command with captured output.
+ * There is generally no place for such output to go into the trace, and the use cases for such a
+ * method to return the output are compelling. For other cases, perhaps the most you can do is
+ * return a {@link TraceObject}, so that a client can quickly associate the trace changes with the
+ * method. Otherwise, please return null/void/None for all methods.
+ *
+ * NOTE: To avoid the mistake of blocking the Swing thread on an asynchronous result, the
+ * {@link #get()} methods have been overridden to check for the Swing thread. If invoked on the
+ * Swing thread with a timeout greater than 1 second, an assertion error will be thrown. Please use
+ * a non-swing thread, e.g., a task thread or script thread, to wait for results, or chain
+ * callbacks.
+ */
+public class RemoteAsyncResult extends CompletableFuture