From 1de4dfc9c76a8602fd022b79183cc72aa2cd67fb Mon Sep 17 00:00:00 2001 From: Dan <46821332+nsadeveloper789@users.noreply.github.com> Date: Fri, 21 Apr 2023 16:17:59 -0400 Subject: [PATCH] GP-2677: Introduce TraceRmi (API only, experimental) --- .gitignore | 1 + Ghidra/Debug/Debugger-agent-gdb/build.gradle | 3 + .../Debugger-agent-gdb/certification.manifest | 6 + .../breakpoint/GdbBreakpointInsertions.java | 2 + .../Debugger-agent-gdb/src/main/py/LICENSE | 11 + .../Debugger-agent-gdb/src/main/py/README.md | 3 + .../src/main/py/ghidragdb/__init__.py | 16 + .../src/main/py/ghidragdb/arch.py | 287 ++++ .../src/main/py/ghidragdb/commands.py | 1456 ++++++++++++++++ .../src/main/py/ghidragdb/hooks.py | 540 ++++++ .../src/main/py/ghidragdb/methods.py | 653 ++++++++ .../src/main/py/ghidragdb/parameters.py | 46 + .../src/main/py/ghidragdb/schema.xml | 413 +++++ .../src/main/py/ghidragdb/util.py | 286 ++++ .../src/main/py/pyproject.toml | 25 + .../src/main/py/tests/EMPTY | 0 .../agent/gdb/model/GdbLinuxSpecimen.java | 16 +- Ghidra/Debug/Debugger-agent-lldb/build.gradle | 3 + .../certification.manifest | 6 +- .../Debugger-agent-lldb/src/main/py/LICENSE | 11 + .../Debugger-agent-lldb/src/main/py/README.md | 3 + .../src/main/py/ghidralldb/__init__.py | 16 + .../src/main/py/ghidralldb/arch.py | 261 +++ .../src/main/py/ghidralldb/commands.py | 1487 +++++++++++++++++ .../src/main/py/ghidralldb/hooks.py | 709 ++++++++ .../src/main/py/ghidralldb/methods.py | 640 +++++++ .../src/main/py/ghidralldb/parameters.py | 46 + .../src/main/py/ghidralldb/schema.xml | 465 ++++++ .../src/main/py/ghidralldb/util.py | 236 +++ .../src/main/py/pyproject.toml | 25 + .../java/agent/lldb/model/MacOSSpecimen.java | 35 +- Ghidra/Debug/Debugger-gadp/build.gradle | 80 +- Ghidra/Debug/Debugger-isf/build.gradle | 75 +- Ghidra/Debug/Debugger-rmi-trace/DEVNOTES.txt | 280 ++++ .../Debug/Debugger-rmi-trace/Module.manifest | 0 Ghidra/Debug/Debugger-rmi-trace/build.gradle | 56 + .../Debugger-rmi-trace/certification.manifest | 7 + .../ghidra_scripts/ConnectTraceRmiScript.java | 48 + .../ghidra_scripts/ListenTraceRmiScript.java | 48 + .../debug/service/rmi/trace/OpenTrace.java | 107 ++ .../service/rmi/trace/RemoteAsyncResult.java | 70 + .../debug/service/rmi/trace/RemoteMethod.java | 330 ++++ .../rmi/trace/RemoteMethodRegistry.java | 50 + .../service/rmi/trace/RemoteParameter.java | 22 + .../service/rmi/trace/TraceRmiAcceptor.java | 45 + .../service/rmi/trace/TraceRmiError.java | 33 + .../service/rmi/trace/TraceRmiHandler.java | 1162 +++++++++++++ .../service/rmi/trace/TraceRmiPlugin.java | 119 ++ .../service/rmi/trace/TraceRmiServer.java | 99 ++ .../debug/service/rmi/trace/ValueDecoder.java | 95 ++ .../service/rmi/trace/ValueSupplier.java | 22 + .../ghidra/app/services/TraceRmiService.java | 51 + .../src/main/proto/trace-rmi.proto | 525 ++++++ .../Debugger-rmi-trace/src/main/py/LICENSE | 11 + .../Debugger-rmi-trace/src/main/py/README.md | 3 + .../src/main/py/pyproject.toml | 25 + .../src/main/py/src/ghidratrace/__init__.py | 15 + .../src/main/py/src/ghidratrace/client.py | 1107 ++++++++++++ .../src/main/py/src/ghidratrace/sch.py | 47 + .../src/main/py/src/ghidratrace/util.py | 63 + .../src/main/py/tests/EMPTY | 0 .../core/debug/DebuggerCoordinates.java | 5 +- ...CurrentPlatformTraceDisassembleAction.java | 3 + .../DebuggerDisassemblerPlugin.java | 2 +- .../action/DebuggerTrackLocationTrait.java | 3 + .../debug/gui/model/ObjectsTreePanel.java | 4 +- .../register/DebuggerRegistersProvider.java | 22 +- .../core/debug/utils/ManagedDomainObject.java | 64 + .../app/services/DebuggerListingService.java | 7 +- .../src/expCloneExit/c/expCloneExit.c | 1 + .../ghidra/dbg/target/TargetMemoryRegion.java | 2 - .../schema/EnumerableTargetObjectSchema.java | 11 +- .../AbstractDebuggerModelFactoryTest.java | 4 +- .../data/AbstractPcodeTraceDataAccess.java | 14 +- .../memory/DBTraceMemoryBufferEntry.java | 13 +- .../database/memory/DBTraceMemorySpace.java | 5 +- .../AbstractDBTraceSpaceBasedManager.java | 6 +- .../trace/database/target/DBTraceObject.java | 32 + .../database/target/DBTraceObjectManager.java | 14 +- .../target/InternalTraceObjectValue.java | 6 +- .../model/listing/TraceDefinedDataView.java | 5 +- .../trace/model/target/TraceObject.java | 13 +- .../model/target/TraceObjectInterface.java | 12 +- .../trace/model/target/TraceObjectValue.java | 11 + .../trace/database/ToyDBTraceBuilder.java | 59 +- .../database/DBCachedObjectStoreFactory.java | 1 + .../gdb/rmi/AbstractGdbTraceRmiTest.java | 501 ++++++ .../java/agent/gdb/rmi/GdbCommandsTest.java | 1213 ++++++++++++++ .../java/agent/gdb/rmi/GdbHooksTest.java | 425 +++++ .../java/agent/gdb/rmi/GdbMethodsTest.java | 1254 ++++++++++++++ .../lldb/rmi/AbstractLldbTraceRmiTest.java | 509 ++++++ .../java/agent/lldb/rmi/LldbCommandsTest.java | 1279 ++++++++++++++ .../java/agent/lldb/rmi/LldbHooksTest.java | 407 +++++ .../java/agent/lldb/rmi/LldbMethodsTest.java | 1139 +++++++++++++ gradle/debugger/hasProtobuf.gradle | 94 ++ gradle/debugger/hasPythonPackage.gradle | 86 + 96 files changed, 19314 insertions(+), 214 deletions(-) create mode 100644 Ghidra/Debug/Debugger-agent-gdb/src/main/py/LICENSE create mode 100644 Ghidra/Debug/Debugger-agent-gdb/src/main/py/README.md create mode 100644 Ghidra/Debug/Debugger-agent-gdb/src/main/py/ghidragdb/__init__.py create mode 100644 Ghidra/Debug/Debugger-agent-gdb/src/main/py/ghidragdb/arch.py create mode 100644 Ghidra/Debug/Debugger-agent-gdb/src/main/py/ghidragdb/commands.py create mode 100644 Ghidra/Debug/Debugger-agent-gdb/src/main/py/ghidragdb/hooks.py create mode 100644 Ghidra/Debug/Debugger-agent-gdb/src/main/py/ghidragdb/methods.py create mode 100644 Ghidra/Debug/Debugger-agent-gdb/src/main/py/ghidragdb/parameters.py create mode 100644 Ghidra/Debug/Debugger-agent-gdb/src/main/py/ghidragdb/schema.xml create mode 100644 Ghidra/Debug/Debugger-agent-gdb/src/main/py/ghidragdb/util.py create mode 100644 Ghidra/Debug/Debugger-agent-gdb/src/main/py/pyproject.toml create mode 100644 Ghidra/Debug/Debugger-agent-gdb/src/main/py/tests/EMPTY create mode 100644 Ghidra/Debug/Debugger-agent-lldb/src/main/py/LICENSE create mode 100644 Ghidra/Debug/Debugger-agent-lldb/src/main/py/README.md create mode 100644 Ghidra/Debug/Debugger-agent-lldb/src/main/py/ghidralldb/__init__.py create mode 100644 Ghidra/Debug/Debugger-agent-lldb/src/main/py/ghidralldb/arch.py create mode 100644 Ghidra/Debug/Debugger-agent-lldb/src/main/py/ghidralldb/commands.py create mode 100644 Ghidra/Debug/Debugger-agent-lldb/src/main/py/ghidralldb/hooks.py create mode 100644 Ghidra/Debug/Debugger-agent-lldb/src/main/py/ghidralldb/methods.py create mode 100644 Ghidra/Debug/Debugger-agent-lldb/src/main/py/ghidralldb/parameters.py create mode 100644 Ghidra/Debug/Debugger-agent-lldb/src/main/py/ghidralldb/schema.xml create mode 100644 Ghidra/Debug/Debugger-agent-lldb/src/main/py/ghidralldb/util.py create mode 100644 Ghidra/Debug/Debugger-agent-lldb/src/main/py/pyproject.toml create mode 100644 Ghidra/Debug/Debugger-rmi-trace/DEVNOTES.txt create mode 100644 Ghidra/Debug/Debugger-rmi-trace/Module.manifest create mode 100644 Ghidra/Debug/Debugger-rmi-trace/build.gradle create mode 100644 Ghidra/Debug/Debugger-rmi-trace/certification.manifest create mode 100644 Ghidra/Debug/Debugger-rmi-trace/ghidra_scripts/ConnectTraceRmiScript.java create mode 100644 Ghidra/Debug/Debugger-rmi-trace/ghidra_scripts/ListenTraceRmiScript.java create mode 100644 Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/OpenTrace.java create mode 100644 Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/RemoteAsyncResult.java create mode 100644 Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/RemoteMethod.java create mode 100644 Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/RemoteMethodRegistry.java create mode 100644 Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/RemoteParameter.java create mode 100644 Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/TraceRmiAcceptor.java create mode 100644 Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/TraceRmiError.java create mode 100644 Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/TraceRmiHandler.java create mode 100644 Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/TraceRmiPlugin.java create mode 100644 Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/TraceRmiServer.java create mode 100644 Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/ValueDecoder.java create mode 100644 Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/ValueSupplier.java create mode 100644 Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/services/TraceRmiService.java create mode 100644 Ghidra/Debug/Debugger-rmi-trace/src/main/proto/trace-rmi.proto create mode 100644 Ghidra/Debug/Debugger-rmi-trace/src/main/py/LICENSE create mode 100644 Ghidra/Debug/Debugger-rmi-trace/src/main/py/README.md create mode 100644 Ghidra/Debug/Debugger-rmi-trace/src/main/py/pyproject.toml create mode 100644 Ghidra/Debug/Debugger-rmi-trace/src/main/py/src/ghidratrace/__init__.py create mode 100644 Ghidra/Debug/Debugger-rmi-trace/src/main/py/src/ghidratrace/client.py create mode 100644 Ghidra/Debug/Debugger-rmi-trace/src/main/py/src/ghidratrace/sch.py create mode 100644 Ghidra/Debug/Debugger-rmi-trace/src/main/py/src/ghidratrace/util.py create mode 100644 Ghidra/Debug/Debugger-rmi-trace/src/main/py/tests/EMPTY create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/utils/ManagedDomainObject.java create mode 100644 Ghidra/Test/IntegrationTest/src/test.slow/java/agent/gdb/rmi/AbstractGdbTraceRmiTest.java create mode 100644 Ghidra/Test/IntegrationTest/src/test.slow/java/agent/gdb/rmi/GdbCommandsTest.java create mode 100644 Ghidra/Test/IntegrationTest/src/test.slow/java/agent/gdb/rmi/GdbHooksTest.java create mode 100644 Ghidra/Test/IntegrationTest/src/test.slow/java/agent/gdb/rmi/GdbMethodsTest.java create mode 100644 Ghidra/Test/IntegrationTest/src/test.slow/java/agent/lldb/rmi/AbstractLldbTraceRmiTest.java create mode 100644 Ghidra/Test/IntegrationTest/src/test.slow/java/agent/lldb/rmi/LldbCommandsTest.java create mode 100644 Ghidra/Test/IntegrationTest/src/test.slow/java/agent/lldb/rmi/LldbHooksTest.java create mode 100644 Ghidra/Test/IntegrationTest/src/test.slow/java/agent/lldb/rmi/LldbMethodsTest.java create mode 100644 gradle/debugger/hasProtobuf.gradle create mode 100644 gradle/debugger/hasPythonPackage.gradle diff --git a/.gitignore b/.gitignore index b690996f06..a33e0d0d65 100644 --- a/.gitignore +++ b/.gitignore @@ -68,6 +68,7 @@ Release .classpath .settings/ .prefs +.pydevproject # Ignore XTEXT generated dirs/files */*/*/*/xtend-gen diff --git a/Ghidra/Debug/Debugger-agent-gdb/build.gradle b/Ghidra/Debug/Debugger-agent-gdb/build.gradle index 9f143b25f9..cd6d0a542d 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/build.gradle +++ b/Ghidra/Debug/Debugger-agent-gdb/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-gdb' @@ -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-gdb/certification.manifest b/Ghidra/Debug/Debugger-agent-gdb/certification.manifest index aaa7ad0026..2daab586ea 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/certification.manifest +++ b/Ghidra/Debug/Debugger-agent-gdb/certification.manifest @@ -1,7 +1,13 @@ ##VERSION: 2.0 ##MODULE IP: JSch License +DEVNOTES.txt||GHIDRA||||END| Module.manifest||GHIDRA||||END| data/scripts/fallback_info_proc_mappings.gdb||GHIDRA||||END| data/scripts/fallback_maintenance_info_sections.gdb||GHIDRA||||END| data/scripts/getpid-linux-i386.gdb||GHIDRA||||END| data/scripts/wine32_info_proc_mappings.gdb||GHIDRA||||END| +src/main/py/LICENSE||GHIDRA||||END| +src/main/py/README.md||GHIDRA||||END| +src/main/py/ghidragdb/schema.xml||GHIDRA||||END| +src/main/py/pyproject.toml||GHIDRA||||END| +src/main/py/tests/EMPTY||GHIDRA||||END| diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/breakpoint/GdbBreakpointInsertions.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/breakpoint/GdbBreakpointInsertions.java index 551e83fd4a..7712cd070d 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/breakpoint/GdbBreakpointInsertions.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/breakpoint/GdbBreakpointInsertions.java @@ -21,8 +21,10 @@ public interface GdbBreakpointInsertions { /** * Insert a breakpoint * + *

* 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 { + final ValueDecoder decoder; + + public RemoteAsyncResult() { + this.decoder = ValueDecoder.DEFAULT; + } + + public RemoteAsyncResult(OpenTrace open) { + this.decoder = open; + } + + @Override + public Object get() throws InterruptedException, ExecutionException { + if (Swing.isSwingThread()) { + throw new AssertionError("Refusing indefinite wait on Swing thread"); + } + return super.get(); + } + + @Override + public Object get(long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + if (Swing.isSwingThread() && unit.toSeconds(timeout) > 1) { + throw new AssertionError("Refusing a timeout > 1 second on Swing thread"); + } + return super.get(timeout, unit); + } +} diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/RemoteMethod.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/RemoteMethod.java new file mode 100644 index 0000000000..221db1e075 --- /dev/null +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/RemoteMethod.java @@ -0,0 +1,330 @@ +/* ### + * 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.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.function.Function; + +import ghidra.async.AsyncUtils; +import ghidra.dbg.target.TargetObject; +import ghidra.dbg.target.schema.*; +import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName; +import ghidra.trace.model.Trace; +import ghidra.trace.model.target.TraceObject; + +/** + * A remote method registered by the back-end debugger. + * + *

+ * Remote methods must describe the parameters names and types at a minimum. They should also + * provide a display name and description for the method itself and each of its parameters. These + * methods should not return a result. Instead, any "result" should be recorded into a trace. The + * invocation can result in an error, which is communicated by an exception that can carry only a + * message string. Choice few methods should return a result, for example, the {@code execute} + * method with output capture. That output generally does not belong in a trace, so the only way to + * communicate it back to the front end is to return it. + */ +public interface RemoteMethod { + + /** + * A "hint" for how to map the method to a common action. + * + *

+ * Many common commands/actions have varying names across different back-end debuggers. We'd + * like to present common idioms for these common actions, but allow them to keep the names used + * by the back-end, because those names are probably better known to users of that back-end than + * Ghidra's action names are known. The action hints will affect the icon and placement of the + * action in the UI, but the display name will still reflect the name given by the back-end. + * Note that the "stock" action names are not a fixed enumeration. These are just the ones that + * might get special treatment from Ghidra. All methods should appear somewhere (at least, e.g., + * in context menus for applicable objects), even if the action name is unspecified or does not + * match a stock name. This list may change over time, but that shouldn't matter much. Each + * back-end should make its best effort to match its methods to these stock actions where + * applicable, but ultimately, it is up to the UI to decide what is presented where. + */ + public record Action(String name) { + public static final Action REFRESH = new Action("refresh"); + public static final Action ACTIVATE = new Action("activate"); + /** + * A weaker form of activate. + * + *

+ * The user has expressed interest in an object, but has not activated it yet. This is often + * used to communicate selection (i.e., highlight) of the object. Whereas, double-clicking + * or pressing enter would more likely invoke 'activate.' + */ + public static final Action FOCUS = new Action("focus"); + public static final Action TOGGLE = new Action("toggle"); + public static final Action DELETE = new Action("delete"); + + /** + * Forms: (cmd:STRING):STRING + * + * Optional arguments: capture:BOOL + */ + public static final Action EXECUTE = new Action("execute"); + + /** + * Forms: (spec:STRING) + */ + public static final Action CONNECT = new Action("connect"); + + /** + * Forms: (target:Attachable), (pid:INT), (spec:STRING) + */ + public static final Action ATTACH = new Action("attach"); + public static final Action DETACH = new Action("detach"); + + /** + * Forms: (command_line:STRING), (file:STRING,args:STRING), (file:STRING,args:STRING_ARRAY), + * (ANY*) + */ + public static final Action LAUNCH = new Action("launch"); + public static final Action KILL = new Action("kill"); + + public static final Action RESUME = new Action("resume"); + public static final Action INTERRUPT = new Action("interrupt"); + + /** + * All of these will show in the "step" portion of the control toolbar, if present. The + * difference in each "step_x" is minor. The icon will indicate which form, and the + * positions will be shifted so they appear in a consistent order. The display name is + * determined by the method name, not the action name. For stepping actions that don't fit + * the standards, use {@link #STEP_EXT}. There should be at most one of each standard + * applicable for any given context. (Multiple will appear, but may confuse the user.) You + * can have as many extended step actions as you like. They will be ordered + * lexicographically by name. + */ + public static final Action STEP_INTO = new Action("step_into"); + public static final Action STEP_OVER = new Action("step_over"); + public static final Action STEP_OUT = new Action("step_out"); + /** + * Skip is not typically available, except in emulators. If the back-end debugger does not + * have a command for this action out-of-the-box, we do not recommend trying to implement it + * yourself. The purpose of these actions just to expose/map each command to the UI, not to + * invent new features for the back-end debugger. + */ + public static final Action STEP_SKIP = new Action("step_skip"); + /** + * Step back is not typically available, except in emulators and timeless (or time-travel) + * debuggers. + */ + public static final Action STEP_BACK = new Action("step_back"); + /** + * The action for steps that don't fit one of the common stepping actions. + */ + public static final Action STEP_EXT = new Action("step_ext"); + + /** + * Forms: (addr:ADDRESS), R/W(rng:RANGE), set(expr:STRING) + * + * Optional arguments: condition:STRING, commands:STRING + */ + public static final Action BREAK_SW_EXECUTE = new Action("break_sw_execute"); + public static final Action BREAK_HW_EXECUTE = new Action("break_hw_execute"); + public static final Action BREAK_READ = new Action("break_read"); + public static final Action BREAK_WRITE = new Action("break_write"); + public static final Action BREAK_ACCESS = new Action("break_access"); + public static final Action BREAK_EXT = new Action("break_ext"); + + /** + * Forms: (rng:RANGE) + */ + public static final Action READ_MEM = new Action("read_mem"); + /** + * Forms: (addr:ADDRESS,data:BYTES) + */ + public static final Action WRITE_MEM = new Action("write_mem"); + + // NOTE: no read_reg. Use refresh(RegContainer), refresh(RegGroup), refresh(Register) + /** + * Forms: (frame:Frame,name:STRING,value:BYTES), (register:Register,value:BYTES) + */ + public static final Action WRITE_REG = new Action("write_reg"); + } + + /** + * The name of the method. + * + * @return the name + */ + String name(); + + /** + * A string that hints at the UI action this method achieves. + * + * @return the action + */ + Action action(); + + /** + * A description of the method. + * + *

+ * This is the text for tooltips or other information presented by actions whose purpose is to + * invoke this method. If the back-end command name is well known to its users, this text should + * include that name. + * + * @return the description + */ + String description(); + + /** + * The methods parameters. + * + *

+ * Parameters are all keyword-style parameters. This returns a map of names to parameter + * descriptions. + * + * @return the parameter map + */ + Map parameters(); + + /** + * Get the schema for the return type. + * + * NOTE: Most methods should return void, i.e., either they succeed, or they throw/raise + * an error message. One notable exception is "execute," which may return the console output + * from executing a command. In most cases, the method should only cause an update to the trace + * database. That effect is its result. + * + * @return the schema name for the method's return type. + */ + SchemaName retType(); + + /** + * Check the type of an argument. + * + *

+ * This is a hack, because {@link TargetObjectSchema} expects {@link TargetObject}, or a + * primitive. We instead need {@link TraceObject}. I'd add the method to the schema, except that + * trace stuff is not in its dependencies. + * + * @param name the name of the parameter + * @param sch the type of the parameter + * @param arg the argument + */ + static void checkType(String name, TargetObjectSchema sch, Object arg) { + if (sch.getType() != TargetObject.class) { + if (sch.getType().isInstance(arg)) { + return; + } + } + else if (arg instanceof TraceObject obj) { + if (sch.equals(obj.getTargetSchema())) { + return; + } + } + throw new IllegalArgumentException( + "For parameter %s: argument %s is not a %s".formatted(name, arg, sch)); + } + + /** + * Validate the given argument. + * + *

+ * This method is for checking parameter sanity before they are marshalled to the back-end. This + * is called automatically during invocation. Clients can use this method to pre-test or + * validate in the UI, when invocation is not yet desired. + * + * @param arguments the arguments + * @return the trace if any object arguments were given, or null + * @throws IllegalArgumentException if the arguments are not valid + */ + default Trace validate(Map arguments) { + Trace trace = null; + SchemaContext ctx = EnumerableTargetObjectSchema.MinimalSchemaContext.INSTANCE; + for (Map.Entry ent : parameters().entrySet()) { + if (!arguments.containsKey(ent.getKey())) { + if (ent.getValue().required()) { + throw new IllegalArgumentException( + "Missing required parameter '" + ent.getKey() + "'"); + } + continue; // Should not need to check the default value + } + Object arg = arguments.get(ent.getKey()); + if (arg instanceof TraceObject obj) { + if (trace == null) { + trace = obj.getTrace(); + ctx = trace.getObjectManager().getRootSchema().getContext(); + } + else if (trace != obj.getTrace()) { + throw new IllegalArgumentException( + "All TraceObject parameters must come from the same trace"); + } + } + TargetObjectSchema sch = ctx.getSchema(ent.getValue().type()); + checkType(ent.getKey(), sch, arg); + } + for (Map.Entry ent : arguments.entrySet()) { + if (!parameters().containsKey(ent.getKey())) { + throw new IllegalArgumentException("Extra argument '" + ent.getKey() + "'"); + } + } + return trace; + } + + /** + * Invoke the remote method, getting a future result. + * + *

+ * This invokes the method asynchronously. The returned objects is a {@link CompletableFuture}, + * whose getters are overridden to prevent blocking the Swing thread for more than 1 second. Use + * of this method is not recommended, if it can be avoided; however, you should not create a + * thread whose sole purpose is to invoke this method. UI actions that need to invoke a remote + * method should do so using this method, but they must be sure to handle errors using, e.g., + * using {@link CompletableFuture#exceptionally(Function)}, lest the actions fail silently. + * + * @param arguments the keyword arguments to the remote method + * @return the future result + * @throws IllegalArgumentException if the arguments are not valid + */ + RemoteAsyncResult invokeAsync(Map arguments); + + /** + * Invoke the remote method and wait for its completion. + * + *

+ * This method cannot be invoked from the Swing thread. This is to avoid locking up the user + * interface. If you are on the Swing thread, consider {@link #invokeAsync(Map)} instead. You + * can chain the follow-up actions and then schedule any UI updates on the Swing thread using + * {@link AsyncUtils#SWING_EXECUTOR}. + * + * @param arguments the keyword arguments to the remote method + * @throws IllegalArgumentException if the arguments are not valid + */ + default Object invoke(Map arguments) { + try { + return invokeAsync(arguments).get(); + } + catch (InterruptedException | ExecutionException e) { + throw new TraceRmiError(e); + } + } + + record RecordRemoteMethod(TraceRmiHandler handler, String name, Action action, + String description, Map parameters, SchemaName retType) + implements RemoteMethod { + @Override + public RemoteAsyncResult invokeAsync(Map arguments) { + Trace trace = validate(arguments); + OpenTrace open = handler.getOpenTrace(trace); + return handler.invoke(open, name, arguments); + } + } +} diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/RemoteMethodRegistry.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/RemoteMethodRegistry.java new file mode 100644 index 0000000000..04521f6360 --- /dev/null +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/RemoteMethodRegistry.java @@ -0,0 +1,50 @@ +/* ### + * 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.*; + +import ghidra.app.plugin.core.debug.service.rmi.trace.RemoteMethod.Action; + +public class RemoteMethodRegistry { + private final Map map = new HashMap<>(); + private final Map> byAction = new HashMap<>(); + + protected void add(RemoteMethod method) { + synchronized (map) { + map.put(method.name(), method); + byAction.computeIfAbsent(method.action(), k -> new HashSet<>()).add(method); + } + } + + public Map all() { + synchronized (map) { + return Map.copyOf(map); + } + } + + public RemoteMethod get(String name) { + synchronized (map) { + return map.get(name); + } + } + + public Set getByAction(Action action) { + synchronized (map) { + return byAction.getOrDefault(action, Set.of()); + } + } +} diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/RemoteParameter.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/RemoteParameter.java new file mode 100644 index 0000000000..b7caaef916 --- /dev/null +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/RemoteParameter.java @@ -0,0 +1,22 @@ +/* ### + * 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.dbg.target.schema.TargetObjectSchema.SchemaName; + +public record RemoteParameter(String name, SchemaName type, boolean required, + ValueSupplier defaultValue, String display, String description) { +} diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/TraceRmiAcceptor.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/TraceRmiAcceptor.java new file mode 100644 index 0000000000..31530db7fe --- /dev/null +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/TraceRmiAcceptor.java @@ -0,0 +1,45 @@ +/* ### + * 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.io.IOException; +import java.net.ServerSocket; +import java.net.SocketAddress; + +public class TraceRmiAcceptor extends TraceRmiServer { + + public TraceRmiAcceptor(TraceRmiPlugin plugin, SocketAddress address) { + super(plugin, address); + } + + @Override + public void start() throws IOException { + socket = new ServerSocket(); + bind(); + } + + @Override + protected void bind() throws IOException { + socket.bind(address, 1); + } + + @Override + public TraceRmiHandler accept() throws IOException { + TraceRmiHandler handler = super.accept(); + close(); + return handler; + } +} diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/TraceRmiError.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/TraceRmiError.java new file mode 100644 index 0000000000..240bc6eff6 --- /dev/null +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/TraceRmiError.java @@ -0,0 +1,33 @@ +/* ### + * 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; + +public class TraceRmiError extends RuntimeException { + public TraceRmiError() { + } + + public TraceRmiError(Throwable cause) { + super(cause); + } + + public TraceRmiError(String message) { + super(message); + } + + public TraceRmiError(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/TraceRmiHandler.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/TraceRmiHandler.java new file mode 100644 index 0000000000..e6d1363a15 --- /dev/null +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/TraceRmiHandler.java @@ -0,0 +1,1162 @@ +/* ### + * 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.io.*; +import java.math.BigInteger; +import java.net.Socket; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.stream.*; + +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; + +import com.google.protobuf.ByteString; + +import db.Transaction; +import ghidra.app.plugin.core.debug.DebuggerCoordinates; +import ghidra.app.plugin.core.debug.disassemble.DebuggerDisassemblerPlugin; +import ghidra.app.plugin.core.debug.disassemble.TraceDisassembleCommand; +import ghidra.app.plugin.core.debug.service.rmi.trace.RemoteMethod.Action; +import ghidra.app.plugin.core.debug.service.rmi.trace.RemoteMethod.RecordRemoteMethod; +import ghidra.app.services.DebuggerTraceManagerService; +import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName; +import ghidra.dbg.target.schema.XmlSchemaContext; +import ghidra.dbg.util.PathPattern; +import ghidra.dbg.util.PathUtils; +import ghidra.framework.model.*; +import ghidra.framework.plugintool.AutoService; +import ghidra.framework.plugintool.AutoService.Wiring; +import ghidra.framework.plugintool.annotation.AutoServiceConsumed; +import ghidra.lifecycle.Internal; +import ghidra.program.model.address.*; +import ghidra.program.model.lang.*; +import ghidra.program.util.DefaultLanguageService; +import ghidra.rmi.trace.TraceRmi.*; +import ghidra.rmi.trace.TraceRmi.Compiler; +import ghidra.rmi.trace.TraceRmi.Language; +import ghidra.trace.database.DBTrace; +import ghidra.trace.model.Lifespan; +import ghidra.trace.model.Trace; +import ghidra.trace.model.guest.TracePlatform; +import ghidra.trace.model.memory.*; +import ghidra.trace.model.target.*; +import ghidra.trace.model.target.TraceObject.ConflictResolution; +import ghidra.trace.model.time.TraceSnapshot; +import ghidra.util.*; +import ghidra.util.exception.CancelledException; +import ghidra.util.exception.DuplicateFileException; +import ghidra.util.task.TaskMonitor; + +public class TraceRmiHandler { + public static final String VERSION = "10.4"; + + protected static class VersionMismatchError extends TraceRmiError { + public VersionMismatchError(String remote) { + super("Mismatched versions: Front-end: %s, back-end: %s.".formatted(VERSION, remote)); + } + } + + protected static class InvalidRequestError extends TraceRmiError { + public InvalidRequestError(RootMessage req) { + super("Unrecognized or out-of-sequence request: " + req); + } + } + + protected static class InvalidDomObjIdError extends TraceRmiError { + } + + protected static class DomObjIdInUseError extends TraceRmiError { + } + + protected static class InvalidObjIdError extends TraceRmiError { + } + + protected static class InvalidObjPathError extends TraceRmiError { + } + + protected static class NoSuchAddressSpaceError extends TraceRmiError { + } + + protected static class InvalidSchemaError extends TraceRmiError { + public InvalidSchemaError(Throwable cause) { + super(cause); + } + } + + protected static class InvalidRegisterError extends TraceRmiError { + public InvalidRegisterError(String name) { + super("Invalid register: " + name); + } + } + + protected static class InvalidTxIdError extends TraceRmiError { + public InvalidTxIdError(int id) { + super("txid=" + id); + } + } + + protected static class TxIdInUseError extends TraceRmiError { + } + + protected record DoId(int domObjId) { + public DoId(DomObjId oid) { + this(oid.getId()); + } + + public DomObjId toDomObjId() { + return DomObjId.newBuilder().setId(domObjId).build(); + } + } + + protected record Tid(DoId doId, int txId) { + } + + protected record OpenTx(Tid txId, Transaction tx, boolean undoable) { + } + + protected static class OpenTraceMap { + private final Map byId = new HashMap<>(); + private final Map byTrace = new HashMap<>(); + + public synchronized boolean isEmpty() { + return byId.isEmpty(); + } + + public synchronized Set idSet() { + return Set.copyOf(byId.keySet()); + } + + public synchronized OpenTrace removeById(DoId id) { + OpenTrace removed = byId.remove(id); + if (removed == null) { + return null; + } + byTrace.remove(removed.trace); + return removed; + } + + public synchronized OpenTrace getById(DoId doId) { + return byId.get(doId); + } + + public synchronized OpenTrace getByTrace(Trace trace) { + return byTrace.get(trace); + } + + public synchronized void put(OpenTrace openTrace) { + byId.put(openTrace.doId, openTrace); + byTrace.put(openTrace.trace, openTrace); + } + } + + private final TraceRmiPlugin plugin; + private final Socket socket; + private final InputStream in; + private final OutputStream out; + private final CompletableFuture negotiate = new CompletableFuture<>(); + private final CompletableFuture closed = new CompletableFuture<>(); + + private final OpenTraceMap openTraces = new OpenTraceMap(); + private final Map openTxes = new HashMap<>(); + + private final RemoteMethodRegistry methodRegistry = new RemoteMethodRegistry(); + // The remote must service requests and reply in the order received. + private final Deque xReqQueue = new ArrayDeque<>(); + + @AutoServiceConsumed + private DebuggerTraceManagerService traceManager; + @SuppressWarnings("unused") + private final Wiring autoServiceWiring; + + /** + * Create a handler + * + *

+ * Note it is common for this to be constructed by a TCP client. + * + * @param socket the socket to the back-end debugger + * @throws IOException if there is an issue with the I/O streams + */ + public TraceRmiHandler(TraceRmiPlugin plugin, Socket socket) throws IOException { + this.plugin = plugin; + this.socket = socket; + this.in = socket.getInputStream(); + this.out = socket.getOutputStream(); + + this.autoServiceWiring = AutoService.wireServicesConsumed(plugin, this); + + this.negotiate(); + } + + protected void flushXReqQueue(Throwable exc) { + List copy; + synchronized (xReqQueue) { + copy = List.copyOf(xReqQueue); + xReqQueue.clear(); + } + for (RemoteAsyncResult result : copy) { + result.completeExceptionally(exc); + } + } + + public void dispose() throws IOException { + flushXReqQueue(new TraceRmiError("Socket closed")); + socket.close(); + while (!openTxes.isEmpty()) { + Tid nextKey = openTxes.keySet().iterator().next(); + OpenTx open = openTxes.remove(nextKey); + open.tx.close(); + } + while (!openTraces.isEmpty()) { + DoId nextKey = openTraces.idSet().iterator().next(); + OpenTrace open = openTraces.removeById(nextKey); + if (traceManager == null || traceManager.isSaveTracesByDefault()) { + try { + open.trace.save("Save on Disconnect", plugin.getTaskMonitor()); + } + catch (IOException e) { + Msg.error(this, "Could not save " + open.trace); + } + catch (CancelledException e) { + // OK. Move on + } + } + open.trace.release(this); + } + closed.complete(null); + } + + public boolean isClosed() { + return socket.isClosed(); + } + + public void waitClosed() throws InterruptedException, ExecutionException { + closed.get(); + } + + protected DomainFolder getOrCreateNewTracesFolder() + throws InvalidNameException, IOException { + return getOrCreateFolder(plugin.getTool().getProject().getProjectData().getRootFolder(), + "New Traces"); + } + + protected DomainFolder getOrCreateFolder(DomainFolder parent, String name) + throws InvalidNameException, IOException { + try { + return parent.createFolder(name); + } + catch (DuplicateFileException e) { + return parent.getFolder(name); + } + } + + protected DomainFolder createFolders(DomainFolder parent, Path path) + throws InvalidNameException, IOException { + return createFolders(parent, path, 0); + } + + protected DomainFolder createFolders(DomainFolder parent, Path path, int index) + throws InvalidNameException, IOException { + if (path == null && index == 0 || index == path.getNameCount()) { + return parent; + } + String name = path.getName(index).toString(); + return createFolders(getOrCreateFolder(parent, name), path, index + 1); + } + + protected DomainFile createDeconflictedFile(DomainFolder parent, DomainObject object) + throws InvalidNameException, CancelledException, IOException { + String name = object.getName(); + TaskMonitor monitor = plugin.getTaskMonitor(); + for (int nextId = 1; nextId < 100; nextId++) { + try { + return parent.createFile(name, object, monitor); + } + catch (DuplicateFileException e) { + name = object.getName() + "." + nextId; + } + } + // Don't catch it this last time + return parent.createFile(name, object, monitor); + } + + public void start() { + new Thread(this::receiveLoop, "trace-rmi handler " + socket.getRemoteSocketAddress()) + .start(); + } + + protected RootMessage receive() { + try { + // May return null when the socket is closed normally + return recvDelimited(in); + } + catch (IOException e) { + // Also return null for abnormal closure + Msg.error(this, "Cannot read packet: " + e); + flushXReqQueue(e); + return null; + } + } + + protected static void sendDelimited(OutputStream out, RootMessage msg, long dbgSeq) + throws IOException { + ByteBuffer buf = ByteBuffer.allocate(4); + buf.putInt(msg.getSerializedSize()); + out.write(buf.array()); + msg.writeTo(out); + out.flush(); + } + + protected static byte[] recvAll(InputStream in, int len) throws IOException { + byte[] buf = new byte[len]; + int total = 0; + while (total < len) { + int l = in.read(buf, total, len - total); + if (l <= 0) { + return null; + } + total += l; + } + return buf; + } + + protected static RootMessage recvDelimited(InputStream in) throws IOException { + byte[] lenBuf = recvAll(in, Integer.BYTES); + if (lenBuf == null) { + return null; + } + int len = ByteBuffer.wrap(lenBuf).getInt(); + byte[] datBuf = recvAll(in, len); + if (datBuf == null) { + return null; + } + RootMessage msg = RootMessage.parseFrom(datBuf); + return msg; + } + + long dbgSeq = 0; + + protected boolean send(RootMessage rep) { + try { + synchronized (out) { + sendDelimited(out, rep, dbgSeq++); + } + return true; + } + catch (IOException e) { + Msg.error(this, "Cannot send reply", e); + return false; + } + } + + public void receiveLoop() { + try { + while (true) { + RootMessage req = receive(); + if (req == null) { + // Either normal or abnormal closure + return; + } + + RootMessage rep = dispatchNominal.handle(req); + if (rep == null) { + // Handler did not generate a response + continue; + } + + if (!send(rep)) { + return; + } + } + } + finally { + try { + dispose(); + } + catch (IOException e) { + Msg.error(this, "Could not close socket after error", e); + } + } + } + + protected void negotiate() { + RootMessage req = receive(); + RootMessage rep = dispatchNegotiate.handle(req); + if (req == null) { + throw new TraceRmiError("Could not receive negotiation request"); + } + if (!send(rep)) { + throw new TraceRmiError("Could not respond during negotiation"); + } + } + + private interface Dispatcher { + RootMessage.Builder dispatch(RootMessage req, RootMessage.Builder rep) throws Exception; + + default RootMessage handle(RootMessage req) { + String desc = toString(req); + if (desc != null) { + TimedMsg.debug(this, "HANDLING: " + desc); + } + RootMessage.Builder rep = RootMessage.newBuilder(); + try { + rep = dispatch(req, rep); + return rep == null ? null : rep.build(); + } + catch (Throwable e) { + return rep + .setError(ReplyError.newBuilder() + .setMessage( + e.getMessage() + "\n" + ExceptionUtils.getStackTrace(e))) + .build(); + } + } + + default String toString(RootMessage req) { + return switch (req.getMsgCase()) { + case REQUEST_ACTIVATE -> "activate(%d, %d, %s)".formatted( + req.getRequestActivate().getOid().getId(), + req.getRequestActivate().getObject().getId(), + req.getRequestActivate().getObject().getPath().getPath()); + case REQUEST_END_TX -> "endTx(%d)".formatted( + req.getRequestEndTx().getTxid().getId()); + case REQUEST_START_TX -> "startTx(%d)".formatted( + req.getRequestStartTx().getTxid().getId()); + default -> null; + }; + } + } + + final Dispatcher dispatchNegotiate = (req, rep) -> switch (req.getMsgCase()) { + case REQUEST_NEGOTIATE -> rep + .setReplyNegotiate(handleNegotiate(req.getRequestNegotiate())); + default -> throw new InvalidRequestError(req); + }; + + final Dispatcher dispatchNominal = (req, rep) -> switch (req.getMsgCase()) { + case REQUEST_ACTIVATE -> rep + .setReplyActivate(handleActivate(req.getRequestActivate())); + case REQUEST_CLOSE_TRACE -> rep + .setReplyCloseTrace(handleCloseTrace(req.getRequestCloseTrace())); + case REQUEST_CREATE_OBJECT -> rep + .setReplyCreateObject(handleCreateObject(req.getRequestCreateObject())); + case REQUEST_CREATE_OVERLAY -> rep + .setReplyCreateOverlay( + handleCreateOverlay(req.getRequestCreateOverlay())); + case REQUEST_CREATE_ROOT_OBJECT -> rep + .setReplyCreateObject( + handleCreateRootObject(req.getRequestCreateRootObject())); + case REQUEST_CREATE_TRACE -> rep + .setReplyCreateTrace(handleCreateTrace(req.getRequestCreateTrace())); + case REQUEST_DELETE_BYTES -> rep + .setReplyDeleteBytes(handleDeleteBytes(req.getRequestDeleteBytes())); + case REQUEST_DELETE_REGISTER_VALUE -> rep + .setReplyDeleteRegisterValue( + handleDeleteRegisterValue(req.getRequestDeleteRegisterValue())); + case REQUEST_DISASSEMBLE -> rep + .setReplyDisassemble(handleDisassemble(req.getRequestDisassemble())); + case REQUEST_END_TX -> rep + .setReplyEndTx(handleEndTx(req.getRequestEndTx())); + case REQUEST_GET_OBJECT -> rep + .setReplyGetObject(handleGetObject(req.getRequestGetObject())); + case REQUEST_GET_VALUES -> rep + .setReplyGetValues(handleGetValues(req.getRequestGetValues())); + case REQUEST_GET_VALUES_INTERSECTING -> rep + .setReplyGetValues( + handleGetValuesIntersecting(req.getRequestGetValuesIntersecting())); + case REQUEST_INSERT_OBJECT -> rep + .setReplyInsertObject(handleInsertObject(req.getRequestInsertObject())); + case REQUEST_PUT_BYTES -> rep + .setReplyPutBytes(handlePutBytes(req.getRequestPutBytes())); + case REQUEST_PUT_REGISTER_VALUE -> rep + .setReplyPutRegisterValue( + handlePutRegisterValue(req.getRequestPutRegisterValue())); + case REQUEST_REMOVE_OBJECT -> rep + .setReplyRemoveObject(handleRemoveObject(req.getRequestRemoveObject())); + case REQUEST_RETAIN_VALUES -> rep + .setReplyRetainValues(handleRetainValues(req.getRequestRetainValues())); + case REQUEST_SAVE_TRACE -> rep + .setReplySaveTrace(handleSaveTrace(req.getRequestSaveTrace())); + case REQUEST_SET_MEMORY_STATE -> rep + .setReplySetMemoryState( + handleSetMemoryState(req.getRequestSetMemoryState())); + case REQUEST_SET_VALUE -> rep + .setReplySetValue(handleSetValue(req.getRequestSetValue())); + case REQUEST_SNAPSHOT -> rep + .setReplySnapshot(handleSnapshot(req.getRequestSnapshot())); + case REQUEST_START_TX -> rep + .setReplyStartTx(handleStartTx(req.getRequestStartTx())); + case XREPLY_INVOKE_METHOD -> handleXInvokeMethod(req.getXreplyInvokeMethod()); + default -> throw new InvalidRequestError(req); + }; + + protected OpenTrace requireOpenTrace(DomObjId domObjId) { + return requireOpenTrace(new DoId(domObjId)); + } + + protected OpenTrace requireOpenTrace(DoId doId) { + OpenTrace open = openTraces.getById(doId); + if (open == null) { + throw new InvalidDomObjIdError(); + } + return open; + } + + protected DoId requireAvailableDoId(DomObjId domObjId) { + return requireAvailableDoId(new DoId(domObjId)); + } + + protected DoId requireAvailableDoId(DoId doId) { + OpenTrace open = openTraces.getById(doId); + if (open != null) { + throw new DomObjIdInUseError(); + } + return doId; + } + + protected Tid requireAvailableTid(OpenTrace open, TxId txid) { + return requireAvailableTid(new Tid(open.doId, txid.getId())); + } + + protected Tid requireAvailableTid(Tid tid) { + OpenTx tx = openTxes.get(tid); + if (tx != null) { + throw new TxIdInUseError(); + } + return tid; + } + + protected CompilerSpec requireCompilerSpec(Language language, Compiler compiler) + throws LanguageNotFoundException, CompilerSpecNotFoundException { + return DefaultLanguageService.getLanguageService() + .getLanguage(new LanguageID(language.getId())) + .getCompilerSpecByID(new CompilerSpecID(compiler.getId())); + } + + protected static TraceObjectKeyPath toKeyPath(ObjPath path) { + return TraceObjectKeyPath.parse(path.getPath()); + } + + protected static PathPattern toPathPattern(ObjPath path) { + return new PathPattern(PathUtils.parse(path.getPath())); + } + + protected static Lifespan toLifespan(Span span) { + return Lifespan.span(span.getMin(), span.getMax()); + } + + protected static TraceMemoryState toMemoryState(MemoryState state) { + return switch (state) { + case MS_UNKNOWN -> TraceMemoryState.UNKNOWN; + case MS_KNOWN -> TraceMemoryState.KNOWN; + case MS_ERROR -> TraceMemoryState.ERROR; + default -> throw new AssertionError(); + }; + } + + protected static ConflictResolution toResolution(Resolution resolution) { + return switch (resolution) { + case CR_DENY -> ConflictResolution.DENY; + case CR_TRUNCATE -> ConflictResolution.TRUNCATE; + case CR_ADJUST -> ConflictResolution.ADJUST; + default -> throw new AssertionError(); + }; + } + + protected static ObjSpec makeObjSpec(TraceObject object) { + return ObjSpec.newBuilder() + .setId(object.getKey()) + .build(); + } + + protected static ObjPath makeObjPath(TraceObjectKeyPath path) { + return ObjPath.newBuilder() + .setPath(path.toString()) + .build(); + } + + protected static ObjDesc makeObjDesc(TraceObject object) { + return ObjDesc.newBuilder() + .setId(object.getKey()) + .setPath(makeObjPath(object.getCanonicalPath())) + .build(); + } + + protected static ValDesc makeValDesc(TraceObjectValue value) { + return ValDesc.newBuilder() + .setParent(makeObjDesc(value.getParent())) + .setSpan(makeSpan(value.getLifespan())) + .setKey(value.getEntryKey()) + .setValue(makeValue(value.getValue())) + .build(); + } + + protected static ValDesc makeValDesc(TraceObjectValPath valPath) { + // TODO: If links are involved, explain them? + return makeValDesc(valPath.getLastEntry()); + } + + protected static Span makeSpan(Lifespan lifespan) { + if (lifespan.isEmpty()) { + return Span.newBuilder().setMin(0).setMax(-1).build(); + } + return Span.newBuilder().setMin(lifespan.lmin()).setMax(lifespan.lmax()).build(); + } + + protected static Addr makeAddr(Address address) { + return Addr.newBuilder() + .setSpace(address.getAddressSpace().getName()) + .setOffset(address.getOffset()) + .build(); + } + + protected static AddrRange makeAddrRange(AddressRange range) { + return AddrRange.newBuilder() + .setSpace(range.getAddressSpace().getName()) + .setOffset(range.getMinAddress().getOffset()) + .setExtend(range.getLength() - 1) + .build(); + } + + protected static Value makeValue(Object value) { + if (value instanceof Void) { + return Value.newBuilder().setNullValue(Null.getDefaultInstance()).build(); + } + if (value instanceof Boolean b) { + return Value.newBuilder().setBoolValue(b).build(); + } + if (value instanceof Byte b) { + return Value.newBuilder().setByteValue(b).build(); + } + if (value instanceof Character c) { + return Value.newBuilder().setCharValue(c).build(); + } + if (value instanceof Short s) { + return Value.newBuilder().setShortValue(s).build(); + } + if (value instanceof Integer i) { + return Value.newBuilder().setIntValue(i).build(); + } + if (value instanceof Long l) { + return Value.newBuilder().setLongValue(l).build(); + } + if (value instanceof String s) { + return Value.newBuilder().setStringValue(s).build(); + } + if (value instanceof boolean[] ba) { + return Value.newBuilder() + .setBoolArrValue( + BoolArr.newBuilder().addAllArr(Arrays.asList(ArrayUtils.toObject(ba)))) + .build(); + } + if (value instanceof byte[] ba) { + return Value.newBuilder().setBytesValue(ByteString.copyFrom(ba)).build(); + } + if (value instanceof char[] ca) { + return Value.newBuilder().setCharArrValue(new String(ca)).build(); + } + if (value instanceof short[] sa) { + return Value.newBuilder() + .setShortArrValue(ShortArr.newBuilder() + .addAllArr( + Stream.of(ArrayUtils.toObject(sa)).map(s -> (int) s).toList())) + .build(); + } + if (value instanceof int[] ia) { + return Value.newBuilder() + .setIntArrValue( + IntArr.newBuilder() + .addAllArr(IntStream.of(ia).mapToObj(i -> i).toList())) + .build(); + } + if (value instanceof long[] la) { + return Value.newBuilder() + .setLongArrValue( + LongArr.newBuilder() + .addAllArr(LongStream.of(la).mapToObj(l -> l).toList())) + .build(); + } + if (value instanceof String[] sa) { + return Value.newBuilder() + .setStringArrValue(StringArr.newBuilder().addAllArr(List.of(sa))) + .build(); + } + if (value instanceof Address a) { + return Value.newBuilder().setAddressValue(makeAddr(a)).build(); + } + if (value instanceof AddressRange r) { + return Value.newBuilder().setRangeValue(makeAddrRange(r)).build(); + } + if (value instanceof TraceObject o) { + return Value.newBuilder().setChildDesc(makeObjDesc(o)).build(); + } + throw new AssertionError( + "Cannot encode value: " + value + "(type=" + value.getClass() + ")"); + } + + protected static MethodArgument makeArgument(String name, Object value) { + return MethodArgument.newBuilder().setName(name).setValue(makeValue(value)).build(); + } + + protected static MethodArgument makeArgument(Map.Entry ent) { + return makeArgument(ent.getKey(), ent.getValue()); + } + + protected ReplyActivate handleActivate(RequestActivate req) { + OpenTrace open = requireOpenTrace(req.getOid()); + TraceObject object = open.getObject(req.getObject(), true); + DebuggerCoordinates coords = traceManager.getCurrent(); + coords = coords.object(object); + if (open.lastSnapshot != null) { + coords = coords.snap(open.lastSnapshot.getKey()); + } + if (!traceManager.getOpenTraces().contains(open.trace)) { + traceManager.openTrace(open.trace); + traceManager.activate(coords); + } + else { + Trace currentTrace = traceManager.getCurrentTrace(); + if (currentTrace == null || currentTrace == open.trace) { + traceManager.activate(coords); + } + } + return ReplyActivate.getDefaultInstance(); + } + + protected ReplyCloseTrace handleCloseTrace(RequestCloseTrace req) { + OpenTrace open = requireOpenTrace(req.getOid()); + openTraces.removeById(open.doId); + open.trace.release(this); + return ReplyCloseTrace.getDefaultInstance(); + } + + protected ReplyCreateObject handleCreateObject(RequestCreateObject req) { + OpenTrace open = requireOpenTrace(req.getOid()); + TraceObject object = + open.trace.getObjectManager().createObject(toKeyPath(req.getPath())); + return ReplyCreateObject.newBuilder().setObject(makeObjSpec(object)).build(); + } + + protected ReplyCreateOverlaySpace handleCreateOverlay(RequestCreateOverlaySpace req) { + OpenTrace open = requireOpenTrace(req.getOid()); + AddressSpace base = open.getSpace(req.getBaseSpace(), true); + open.trace.getMemoryManager().getOrCreateOverlayAddressSpace(req.getName(), base); + return ReplyCreateOverlaySpace.getDefaultInstance(); + } + + protected ReplyCreateObject handleCreateRootObject(RequestCreateRootObject req) { + OpenTrace open = requireOpenTrace(req.getOid()); + XmlSchemaContext ctx; + try { + ctx = XmlSchemaContext.deserialize(req.getSchemaContext()); + } + catch (Exception e) { + throw new InvalidSchemaError(e); + } + TraceObjectValue value = open.trace.getObjectManager() + .createRootObject(ctx.getSchema(new SchemaName(req.getRootSchema()))); + return ReplyCreateObject.newBuilder().setObject(makeObjSpec(value.getChild())).build(); + } + + protected ReplyCreateTrace handleCreateTrace(RequestCreateTrace req) + throws InvalidNameException, IOException, CancelledException { + DomainFolder traces = getOrCreateNewTracesFolder(); + Path path = Paths.get(req.getPath().getPath()); + DomainFolder folder = createFolders(traces, path.getParent()); + CompilerSpec cs = requireCompilerSpec(req.getLanguage(), req.getCompiler()); + DBTrace trace = new DBTrace(path.getFileName().toString(), cs, this); + + DoId doId = requireAvailableDoId(req.getOid()); + openTraces.put(new OpenTrace(doId, trace)); + createDeconflictedFile(folder, trace); + return ReplyCreateTrace.getDefaultInstance(); + } + + protected ReplyDeleteBytes handleDeleteBytes(RequestDeleteBytes req) + throws AddressOverflowException { + OpenTrace open = requireOpenTrace(req.getOid()); + long snap = req.getSnap().getSnap(); + AddressRange range = open.toRange(req.getRange(), false); + if (range == null) { + return ReplyDeleteBytes.getDefaultInstance(); + } + open.trace.getMemoryManager() + .removeBytes(snap, range.getMinAddress(), (int) range.getLength()); + return ReplyDeleteBytes.getDefaultInstance(); + } + + protected ReplyDeleteRegisterValue handleDeleteRegisterValue( + RequestDeleteRegisterValue req) { + OpenTrace open = requireOpenTrace(req.getOid()); + long snap = req.getSnap().getSnap(); + AddressSpace space = open.trace.getBaseAddressFactory().getAddressSpace(req.getSpace()); + if (space == null) { + return ReplyDeleteRegisterValue.getDefaultInstance(); + } + TraceMemorySpace ms = open.trace.getMemoryManager().getMemorySpace(space, false); + if (ms == null) { + return ReplyDeleteRegisterValue.getDefaultInstance(); + } + for (String name : req.getNamesList()) { + Register register = open.getRegister(name, false); + if (register == null) { + continue; + } + ms.removeValue(snap, register); + } + return ReplyDeleteRegisterValue.getDefaultInstance(); + } + + protected ReplyDisassemble handleDisassemble(RequestDisassemble req) { + OpenTrace open = requireOpenTrace(req.getOid()); + long snap = req.getSnap().getSnap(); + + /** + * TODO: Is this composition of laziness upon laziness efficient enough? + * + *

+ * Can experiment with ordering of address-set-view "expression" to optimize early + * termination. + * + *

+ * Want addresses satisfying {@code known | (readOnly & everKnown)} + */ + TraceMemoryManager memoryManager = open.trace.getMemoryManager(); + AddressSetView readOnly = + memoryManager.getRegionsAddressSetWith(snap, r -> !r.isWrite()); + AddressSetView everKnown = memoryManager.getAddressesWithState(Lifespan.since(snap), + s -> s == TraceMemoryState.KNOWN); + AddressSetView roEverKnown = new IntersectionAddressSetView(readOnly, everKnown); + AddressSetView known = + memoryManager.getAddressesWithState(snap, s -> s == TraceMemoryState.KNOWN); + AddressSetView disassemblable = + new AddressSet(new UnionAddressSetView(known, roEverKnown)); + + Address start = open.toAddress(req.getStart(), true); + TracePlatform host = open.trace.getPlatformManager().getHostPlatform(); + + TraceDisassembleCommand dis = new TraceDisassembleCommand(host, start, disassemblable); + dis.setInitialContext(DebuggerDisassemblerPlugin.deriveAlternativeDefaultContext( + host.getLanguage(), host.getLanguage().getLanguageID(), start)); + + TaskMonitor monitor = plugin.getTaskMonitor(); + dis.applyToTyped(open.trace.getFixedProgramView(snap), monitor); + + return ReplyDisassemble.newBuilder() + .setLength(dis.getDisassembledAddressSet().getNumAddresses()) + .build(); + } + + protected ReplyEndTx handleEndTx(RequestEndTx req) { + OpenTx tx = openTxes.remove(new Tid(new DoId(req.getOid()), req.getTxid().getId())); + if (tx == null) { + throw new InvalidTxIdError(req.getTxid().getId()); + } + if (req.getAbort()) { + Msg.error(this, "Back-end debugger aborted a transaction!"); + tx.tx.abortOnClose(); + } + tx.tx.close(); + if (!tx.undoable) { + requireOpenTrace(tx.txId.doId).trace.clearUndo(); + } + return ReplyEndTx.getDefaultInstance(); + } + + protected ReplyGetObject handleGetObject(RequestGetObject req) { + OpenTrace open = requireOpenTrace(req.getOid()); + // If the client is checking for existence, and error indicates absence. + TraceObject object = open.getObject(req.getObject(), true); + return ReplyGetObject.newBuilder().setObject(makeObjDesc(object)).build(); + } + + protected ReplyGetValues handleGetValues(RequestGetValues req) { + OpenTrace open = requireOpenTrace(req.getOid()); + return ReplyGetValues.newBuilder() + .addAllValues(open.trace.getObjectManager() + .getValuePaths(toLifespan(req.getSpan()), + toPathPattern(req.getPattern())) + .map(TraceRmiHandler::makeValDesc) + .toList()) + .build(); + } + + protected ReplyGetValues handleGetValuesIntersecting( + RequestGetValuesIntersecting req) throws AddressOverflowException { + OpenTrace open = requireOpenTrace(req.getOid()); + AddressRange range = open.toRange(req.getBox().getRange(), false); + Collection col = range == null + ? List.of() + : open.trace.getObjectManager() + .getValuesIntersecting(toLifespan(req.getBox().getSpan()), range); + return ReplyGetValues.newBuilder() + .addAllValues(col.stream().map(TraceRmiHandler::makeValDesc).toList()) + .build(); + } + + protected ReplyInsertObject handleInsertObject(RequestInsertObject req) { + TraceObject obj = requireOpenTrace(req.getOid()).getObject(req.getObject(), true); + TraceObjectValPath val = + obj.insert(toLifespan(req.getSpan()), toResolution(req.getResolution())); + Lifespan span = val.getEntryList() + .stream() + .map(TraceObjectValue::getLifespan) + .reduce(Lifespan.ALL, Lifespan::intersect); + return ReplyInsertObject.newBuilder() + .setSpan(makeSpan(span)) + .build(); + } + + protected ReplyNegotiate handleNegotiate(RequestNegotiate req) { + if (!VERSION.equals(req.getVersion())) { + VersionMismatchError error = new VersionMismatchError(req.getVersion()); + negotiate.completeExceptionally(error); + throw error; + } + for (Method m : req.getMethodsList()) { + RemoteMethod rm = new RecordRemoteMethod(this, m.getName(), new Action(m.getAction()), + m.getDescription(), m.getParametersList() + .stream() + .collect(Collectors.toMap(MethodParameter::getName, + TraceRmiHandler::makeParameter)), + new SchemaName(m.getReturnType().getName())); + methodRegistry.add(rm); + } + negotiate.complete(null); + return ReplyNegotiate.getDefaultInstance(); + } + + protected ReplyPutBytes handlePutBytes(RequestPutBytes req) { + OpenTrace open = requireOpenTrace(req.getOid()); + long snap = req.getSnap().getSnap(); + Address start = open.toAddress(req.getStart(), true); + + int written = open.trace.getMemoryManager() + .putBytes(snap, start, req.getData().asReadOnlyByteBuffer()); + + return ReplyPutBytes.newBuilder().setWritten(written).build(); + } + + protected ReplyPutRegisterValue handlePutRegisterValue(RequestPutRegisterValue req) { + OpenTrace open = requireOpenTrace(req.getOid()); + long snap = req.getSnap().getSnap(); + AddressSpace space = open.getSpace(req.getSpace(), true); + TraceMemorySpace ms = open.trace.getMemoryManager().getMemorySpace(space, true); + ReplyPutRegisterValue.Builder rep = ReplyPutRegisterValue.newBuilder(); + for (RegVal rv : req.getValuesList()) { + Register register = open.getRegister(rv.getName(), false); + if (register == null) { + Msg.warn(this, "Ignoring unrecognized register: " + rv.getName()); + rep.addSkippedNames(rv.getName()); + continue; + } + BigInteger value = new BigInteger(1, rv.getValue().toByteArray()); + ms.setValue(snap, new RegisterValue(register, value)); + } + return ReplyPutRegisterValue.getDefaultInstance(); + } + + protected static RemoteParameter makeParameter(MethodParameter mp) { + return new RemoteParameter(mp.getName(), new SchemaName(mp.getType().getName()), + mp.getRequired(), ot -> ot.toValue(mp.getDefaultValue()), mp.getDisplay(), + mp.getDescription()); + } + + protected ReplyRemoveObject handleRemoveObject(RequestRemoveObject req) { + TraceObject object = requireOpenTrace(req.getOid()).getObject(req.getObject(), false); + if (object == null) { + return ReplyRemoveObject.getDefaultInstance(); + } + Lifespan lifespan = toLifespan(req.getSpan()); + if (req.getTree()) { + object.removeTree(lifespan); + } + else { + object.remove(lifespan); + } + return ReplyRemoveObject.getDefaultInstance(); + } + + protected ReplyRetainValues handleRetainValues(RequestRetainValues req) { + // This is not a primitive DB operation, but should be more efficient server side. + TraceObject object = requireOpenTrace(req.getOid()).getObject(req.getObject(), false); + if (object == null) { + return ReplyRetainValues.getDefaultInstance(); + } + Collection values = switch (req.getKinds()) { + case VK_ELEMENTS -> object.getElements(); + case VK_ATTRIBUTES -> object.getAttributes(); + case VK_BOTH -> object.getValues(); + default -> throw new TraceRmiError("Protocol error: Invalid value kinds"); + }; + Lifespan span = toLifespan(req.getSpan()); + Set keysToKeep = Set.copyOf(req.getKeysList()); + List keysToDelete = values.stream() + .filter(v -> v.getLifespan().intersects(span)) + .map(v -> v.getEntryKey()) + .filter(k -> !keysToKeep.contains(k)) + .distinct() + .toList(); + for (String key : keysToDelete) { + object.setValue(span, key, null, ConflictResolution.TRUNCATE); + } + return ReplyRetainValues.getDefaultInstance(); + } + + protected ReplySaveTrace handleSaveTrace(RequestSaveTrace req) + throws CancelledException, IOException { + OpenTrace open = requireOpenTrace(req.getOid()); + open.trace.save("TraceRMI", plugin.getTaskMonitor()); + return ReplySaveTrace.getDefaultInstance(); + } + + protected ReplySetMemoryState handleSetMemoryState(RequestSetMemoryState req) + throws AddressOverflowException { + OpenTrace open = requireOpenTrace(req.getOid()); + long snap = req.getSnap().getSnap(); + AddressRange range = open.toRange(req.getRange(), true); + TraceMemoryState state = toMemoryState(req.getState()); + + open.trace.getMemoryManager().setState(snap, range, state); + + return ReplySetMemoryState.getDefaultInstance(); + } + + protected ReplySetValue handleSetValue(RequestSetValue req) + throws AddressOverflowException { + ValSpec value = req.getValue(); + OpenTrace open = requireOpenTrace(req.getOid()); + Object objVal = open.toValue(value.getValue()); + TraceObject object = open.getObject(value.getParent(), objVal != null); + if (object == null) { + // Implies request was to set value to null + return ReplySetValue.newBuilder().setSpan(makeSpan(Lifespan.EMPTY)).build(); + } + TraceObjectValue val = object.setValue(toLifespan(value.getSpan()), value.getKey(), + objVal, toResolution(req.getResolution())); + return ReplySetValue.newBuilder() + .setSpan(makeSpan(val == null ? Lifespan.EMPTY : val.getLifespan())) + .build(); + } + + protected ReplySnapshot handleSnapshot(RequestSnapshot req) { + OpenTrace open = requireOpenTrace(req.getOid()); + TraceSnapshot snapshot = open.createSnapshot(req.getSnap(), req.getDescription()); + if (!"".equals(req.getDatetime())) { + Instant instant = + DateTimeFormatter.ISO_INSTANT.parse(req.getDatetime()).query(Instant::from); + snapshot.setRealTime(instant.toEpochMilli()); + } + return ReplySnapshot.getDefaultInstance(); + } + + protected ReplyStartTx handleStartTx(RequestStartTx req) { + OpenTrace open = requireOpenTrace(req.getOid()); + Tid tid = requireAvailableTid(open, req.getTxid()); + @SuppressWarnings("resource") + OpenTx tx = + new OpenTx(tid, open.trace.openTransaction(req.getDescription()), req.getUndoable()); + openTxes.put(tx.txId, tx); + return ReplyStartTx.getDefaultInstance(); + } + + protected RootMessage.Builder handleXInvokeMethod(XReplyInvokeMethod xrep) { + String error = xrep.getError(); + RemoteAsyncResult result; + synchronized (xReqQueue) { + result = xReqQueue.poll(); + } + if (error.isEmpty()) { + try { + result.complete(result.decoder.toValue(xrep.getReturnValue())); + } + catch (Throwable e) { + result.completeExceptionally(e); + } + } + else { + result.completeExceptionally(new TraceRmiError(error)); + } + return null; + } + + public SocketAddress getRemoteAddress() { + return socket.getRemoteSocketAddress(); + } + + public RemoteMethodRegistry getMethods() { + return methodRegistry; + } + + protected OpenTrace getOpenTrace(Trace trace) { + if (trace == null) { + return null; + } + OpenTrace open = openTraces.getByTrace(trace); + if (open == null) { + throw new NoSuchElementException(); + } + return open; + } + + protected RemoteAsyncResult invoke(OpenTrace open, String methodName, + Map arguments) { + RootMessage.Builder req = RootMessage.newBuilder(); + XRequestInvokeMethod.Builder invoke = XRequestInvokeMethod.newBuilder() + .setName(methodName) + .addAllArguments( + arguments.entrySet().stream().map(TraceRmiHandler::makeArgument).toList()); + RemoteAsyncResult result; + if (open != null) { + result = new RemoteAsyncResult(open); + invoke.setOid(open.doId.toDomObjId()); + } + else { + result = new RemoteAsyncResult(); + } + req.setXrequestInvokeMethod(invoke); + synchronized (xReqQueue) { + xReqQueue.offer(result); + synchronized (out) { + try { + sendDelimited(out, req.build(), dbgSeq++); + } + catch (IOException e) { + throw new TraceRmiError("Could not send request", e); + } + } + return result; + } + } + + @Internal + public long getLastSnapshot(Trace trace) { + TraceSnapshot lastSnapshot = openTraces.getByTrace(trace).lastSnapshot; + if (lastSnapshot == null) { + return 0; + } + return lastSnapshot.getKey(); + } +} diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/TraceRmiPlugin.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/TraceRmiPlugin.java new file mode 100644 index 0000000000..01f83e1c6a --- /dev/null +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/TraceRmiPlugin.java @@ -0,0 +1,119 @@ +/* ### + * 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.io.IOException; +import java.net.*; + +import ghidra.app.plugin.PluginCategoryNames; +import ghidra.app.plugin.core.debug.DebuggerPluginPackage; +import ghidra.app.plugin.core.debug.event.TraceActivatedPluginEvent; +import ghidra.app.plugin.core.debug.event.TraceClosedPluginEvent; +import ghidra.app.services.TraceRmiService; +import ghidra.framework.plugintool.*; +import ghidra.framework.plugintool.util.PluginStatus; +import ghidra.util.task.ConsoleTaskMonitor; +import ghidra.util.task.TaskMonitor; + +@PluginInfo( + shortDescription = "Connect to back-end debuggers via Trace RMI", + description = """ + Provides an alternative for connecting to back-end debuggers. The DebuggerModel has + become a bit onerous to implement. Despite its apparent flexibility, the recorder at + the front-end imposes many restrictions, and getting it to work turns into a lot of + guess work and frustration. Trace RMI should offer a more direct means of recording a + trace from a back-end. + """, + category = PluginCategoryNames.DEBUGGER, + packageName = DebuggerPluginPackage.NAME, + status = PluginStatus.RELEASED, + eventsConsumed = { + TraceActivatedPluginEvent.class, + TraceClosedPluginEvent.class, + }, + servicesProvided = { + TraceRmiService.class, + }) +public class TraceRmiPlugin extends Plugin implements TraceRmiService { + private static final int DEFAULT_PORT = 15432; + private final TaskMonitor monitor = new ConsoleTaskMonitor(); + + private SocketAddress serverAddress = new InetSocketAddress("0.0.0.0", DEFAULT_PORT); + private TraceRmiServer server; + + public TraceRmiPlugin(PluginTool tool) { + super(tool); + } + + public TaskMonitor getTaskMonitor() { + // TODO: Create one in the Debug Console? + return monitor; + } + + @Override + public SocketAddress getServerAddress() { + if (server != null) { + // In case serverAddress is ephemeral, get its actual address + return server.getAddress(); + } + return serverAddress; + } + + @Override + public void setServerAddress(SocketAddress serverAddress) { + if (server != null) { + throw new IllegalStateException("Cannot change server address while it is started"); + } + this.serverAddress = serverAddress; + } + + @Override + public void startServer() throws IOException { + if (server != null) { + throw new IllegalStateException("Server is already started"); + } + server = new TraceRmiServer(this, serverAddress); + server.start(); + } + + @Override + public void stopServer() { + if (server != null) { + server.close(); + } + server = null; + } + + @Override + public boolean isServerStarted() { + return server != null; + } + + @Override + @SuppressWarnings("resource") + public TraceRmiHandler connect(SocketAddress address) throws IOException { + Socket socket = new Socket(); + socket.connect(address); + return new TraceRmiHandler(this, socket); + } + + @Override + public TraceRmiAcceptor acceptOne(SocketAddress address) throws IOException { + TraceRmiAcceptor acceptor = new TraceRmiAcceptor(this, address); + acceptor.start(); + return acceptor; + } +} diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/TraceRmiServer.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/TraceRmiServer.java new file mode 100644 index 0000000000..0cb0ca34a0 --- /dev/null +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/TraceRmiServer.java @@ -0,0 +1,99 @@ +/* ### + * 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.io.IOException; +import java.net.*; + +import ghidra.util.Msg; + +public class TraceRmiServer { + protected final TraceRmiPlugin plugin; + protected final SocketAddress address; + + protected ServerSocket socket; + + public TraceRmiServer(TraceRmiPlugin plugin, SocketAddress address) { + this.plugin = plugin; + this.address = address; + } + + protected void bind() throws IOException { + socket.bind(address); + } + + public void start() throws IOException { + socket = new ServerSocket(); + bind(); + new Thread(this::serviceLoop, "trace-rmi server " + socket.getLocalSocketAddress()).start(); + } + + public void setTimeout(int millis) throws SocketException { + socket.setSoTimeout(millis); + } + + /** + * Accept a connection and handle its requests. + * + *

+ * This launches a new thread to handle the requests. The thread remains alive until the socket + * is closed by either side. + * + * @return the handler + * @throws IOException on error + */ + @SuppressWarnings("resource") + protected TraceRmiHandler accept() throws IOException { + Socket client = socket.accept(); + TraceRmiHandler handler = new TraceRmiHandler(plugin, client); + handler.start(); + return handler; + } + + protected void serviceLoop() { + try { + accept(); + } + catch (IOException e) { + if (socket.isClosed()) { + return; + } + Msg.error("Error accepting TraceRmi client", e); + return; + } + finally { + try { + socket.close(); + } + catch (IOException e) { + Msg.error("Error closing TraceRmi service", e); + } + } + } + + public void close() { + try { + socket.close(); + } + catch (IOException e) { + Msg.error("Error closing TraceRmi service", e); + } + } + + public SocketAddress getAddress() { + return socket.getLocalSocketAddress(); + } +} diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/ValueDecoder.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/ValueDecoder.java new file mode 100644 index 0000000000..4d45549116 --- /dev/null +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/ValueDecoder.java @@ -0,0 +1,95 @@ +/* ### + * 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 org.apache.commons.lang3.ArrayUtils; + +import ghidra.program.model.address.*; +import ghidra.rmi.trace.TraceRmi.*; + +public interface ValueDecoder { + ValueDecoder DEFAULT = new ValueDecoder() {}; + + default Address toAddress(Addr addr, boolean required) { + if (required) { + throw new IllegalStateException("Address requires a trace for context"); + } + return null; + } + + default AddressRange toRange(AddrRange range, boolean required) + throws AddressOverflowException { + if (required) { + throw new IllegalStateException("AddressRange requires a trace for context"); + } + return null; + } + + default Object getObject(ObjSpec spec, boolean required) { + if (required) { + throw new IllegalStateException("TraceObject requires a trace for context"); + } + return null; + } + + default Object getObject(ObjDesc desc, boolean required) { + if (required) { + throw new IllegalStateException("TraceObject requires a trace for context"); + } + return null; + } + + default Object toValue(Value value) throws AddressOverflowException { + return switch (value.getValueCase()) { + case NULL_VALUE -> null; + case BOOL_VALUE -> value.getBoolValue(); + case BYTE_VALUE -> (byte) value.getByteValue(); + case CHAR_VALUE -> (char) value.getCharValue(); + case SHORT_VALUE -> (short) value.getShortValue(); + case INT_VALUE -> value.getIntValue(); + case LONG_VALUE -> value.getLongValue(); + case STRING_VALUE -> value.getStringValue(); + case BOOL_ARR_VALUE -> ArrayUtils.toPrimitive( + value.getBoolArrValue().getArrList().stream().toArray(Boolean[]::new)); + case BYTES_VALUE -> value.getBytesValue().toByteArray(); + case CHAR_ARR_VALUE -> value.getCharArrValue().toCharArray(); + case SHORT_ARR_VALUE -> ArrayUtils.toPrimitive( + value.getShortArrValue() + .getArrList() + .stream() + .map(Integer::shortValue) + .toArray(Short[]::new)); + case INT_ARR_VALUE -> value.getIntArrValue() + .getArrList() + .stream() + .mapToInt(Integer::intValue) + .toArray(); + case LONG_ARR_VALUE -> value.getLongArrValue() + .getArrList() + .stream() + .mapToLong(Long::longValue) + .toArray(); + case STRING_ARR_VALUE -> value.getStringArrValue() + .getArrList() + .toArray(String[]::new); + case ADDRESS_VALUE -> toAddress(value.getAddressValue(), true); + case RANGE_VALUE -> toRange(value.getRangeValue(), true); + case CHILD_SPEC -> getObject(value.getChildSpec(), true); + case CHILD_DESC -> getObject(value.getChildDesc(), true); + default -> throw new AssertionError("Unrecognized value: " + value); + }; + } +} diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/ValueSupplier.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/ValueSupplier.java new file mode 100644 index 0000000000..e4e9d7cbf8 --- /dev/null +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/ValueSupplier.java @@ -0,0 +1,22 @@ +/* ### + * 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.program.model.address.AddressOverflowException; + +public interface ValueSupplier { + Object get(ValueDecoder decoder) throws AddressOverflowException; +} diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/services/TraceRmiService.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/services/TraceRmiService.java new file mode 100644 index 0000000000..8a9526da74 --- /dev/null +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/services/TraceRmiService.java @@ -0,0 +1,51 @@ +/* ### + * 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.services; + +import java.io.IOException; +import java.net.SocketAddress; + +import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiAcceptor; +import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiHandler; + +public interface TraceRmiService { + SocketAddress getServerAddress(); + + /** + * Set the server address and port + * + * @param serverAddress may be null to bind to ephemeral port + */ + void setServerAddress(SocketAddress serverAddress); + + void startServer() throws IOException; + + void stopServer(); + + boolean isServerStarted(); + + TraceRmiHandler connect(SocketAddress address) throws IOException; + + /** + * Accept a single connection by listening on the given address + * + * @param address the socket address to bind, or null for ephemeral + * @return the acceptor, which can be used to retrieve the ephemeral address and accept the + * actual connection + * @throws IOException on error + */ + TraceRmiAcceptor acceptOne(SocketAddress address) throws IOException; +} diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/proto/trace-rmi.proto b/Ghidra/Debug/Debugger-rmi-trace/src/main/proto/trace-rmi.proto new file mode 100644 index 0000000000..10326ab452 --- /dev/null +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/proto/trace-rmi.proto @@ -0,0 +1,525 @@ +/* ### + * 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. + */ +syntax = "proto3"; +package ghidra.rmi.trace; + + +message FilePath { + string path = 1; +} + +message DomObjId { + uint32 id = 1; +} + +message TxId { + int32 id = 1; +} + +message ObjPath { + string path = 1; +} + +message Language { + string id = 1; +} + +message Compiler { + string id = 1; +} + +message Addr { + string space = 1; + uint64 offset = 2; +} + +message AddrRange { + string space = 1; + uint64 offset = 2; + uint64 extend = 3; +} + +message Snap { + int64 snap = 1; +} + +message Span { + int64 min = 1; + int64 max = 2; +} + +message Box { + Span span = 1; + AddrRange range = 2; +} + +message ReplyError { + string message = 1; +} + +// Trace operations + +message RequestCreateTrace { + FilePath path = 1; + Language language = 2; + Compiler compiler = 3; + DomObjId oid = 4; +} + +message ReplyCreateTrace { +} + +message RequestSaveTrace { + DomObjId oid = 1; +} + +message ReplySaveTrace { +} + +message RequestCloseTrace { + DomObjId oid = 1; +} + +message ReplyCloseTrace { +} + +message RequestStartTx { + DomObjId oid = 1; + bool undoable = 2; + string description = 3; + TxId txid = 4; +} + +message ReplyStartTx { +} + +message RequestEndTx { + DomObjId oid = 1; + TxId txid = 2; + bool abort = 3; +} + +message ReplyEndTx { +} + +// Memory operations + +message RequestCreateOverlaySpace { + DomObjId oid = 1; + string baseSpace = 2; + string name = 3; +} + +message ReplyCreateOverlaySpace { +} + +enum MemoryState { + MS_UNKNOWN = 0; + MS_KNOWN = 1; + MS_ERROR = 2; +} + +message RequestSetMemoryState { + DomObjId oid = 1; + Snap snap = 2; + AddrRange range = 3; + MemoryState state = 4; +} + +message ReplySetMemoryState { +} + +message RequestPutBytes { + DomObjId oid = 1; + Snap snap = 2; + Addr start = 3; + bytes data = 4; +} + +message ReplyPutBytes { + int32 written = 1; +} + +message RequestDeleteBytes { + DomObjId oid = 1; + Snap snap = 2; + AddrRange range = 3; +} + +message ReplyDeleteBytes { +} + +message RegVal { + string name = 1; + bytes value = 2; +} + +message RequestPutRegisterValue { + DomObjId oid = 1; + Snap snap = 2; + string space = 3; + repeated RegVal values = 4; +} + +message ReplyPutRegisterValue { + repeated string skipped_names = 1; +} + +message RequestDeleteRegisterValue { + DomObjId oid = 1; + Snap snap = 2; + string space = 3; + repeated string names = 4; +} + +message ReplyDeleteRegisterValue { +} + +// Object operations + +message ObjSpec { + oneof key { + int64 id = 1; + ObjPath path = 2; + } +} + +message ObjDesc { + int64 id = 1; + ObjPath path = 2; +} + +message ValSpec { + ObjSpec parent = 1; + Span span = 2; + string key = 3; + Value value = 4; +} + +message ValDesc { + ObjDesc parent = 1; + Span span = 2; + string key = 3; + Value value = 4; +} + +message Null { +} + +message BoolArr { + repeated bool arr = 1; +} + +message ShortArr { + repeated int32 arr = 1; +} + +message IntArr { + repeated int32 arr = 1; +} + +message LongArr { + repeated int64 arr = 1; +} + +message StringArr { + repeated string arr = 1; +} + +message ValueType { + // Names from schema context + string name = 1; +} + +message Value { + oneof value { + Null null_value = 1; + bool bool_value = 2; + int32 byte_value = 3; + uint32 char_value = 4; + int32 short_value = 5; + int32 int_value = 6; + int64 long_value = 7; + string string_value = 8; + BoolArr bool_arr_value = 9; + bytes bytes_value = 10; + string char_arr_value = 11; + ShortArr short_arr_value = 12; + IntArr int_arr_value = 13; + LongArr long_arr_value = 14; + StringArr string_arr_value = 15; + + Addr address_value = 16; + AddrRange range_value = 17; + + ObjSpec child_spec = 18; + ObjDesc child_desc = 19; + } +} + +message RequestCreateRootObject { + DomObjId oid = 1; + string schema_context = 2; + string root_schema = 3; +} + +message RequestCreateObject { + DomObjId oid = 1; + ObjPath path = 2; +} + +message ReplyCreateObject { + ObjSpec object = 1; +} + +enum Resolution { + CR_TRUNCATE = 0; + CR_DENY = 1; + CR_ADJUST = 2; +} + +message RequestInsertObject { + DomObjId oid = 1; + ObjSpec object = 2; + Span span = 3; + Resolution resolution = 4; +} + +message ReplyInsertObject { + Span span = 1; +} + +message RequestRemoveObject { + DomObjId oid = 1; + ObjSpec object = 2; + Span span = 3; + bool tree = 4; +} + +message ReplyRemoveObject { +} + +message RequestSetValue { + DomObjId oid = 1; + ValSpec value = 2; + Resolution resolution = 3; +} + +message ReplySetValue { + Span span = 1; +} + +enum ValueKinds { + VK_ELEMENTS = 0; + VK_ATTRIBUTES = 1; + VK_BOTH = 2; +} + +message RequestRetainValues { + DomObjId oid = 1; + ObjSpec object = 2; + Span span = 3; + ValueKinds kinds = 4; + repeated string keys = 5; +} + +message ReplyRetainValues { +} + +message RequestGetObject { + DomObjId oid = 1; + ObjSpec object = 2; +} + +message ReplyGetObject { + ObjDesc object = 1; +} + +message RequestGetValues { + DomObjId oid = 1; + Span span = 2; + ObjPath pattern = 3; +} + +message ReplyGetValues { + repeated ValDesc values = 1; +} + +message RequestGetValuesIntersecting { + DomObjId oid = 1; + Box box = 2; +} + +// Analysis operations + +message RequestDisassemble { + DomObjId oid = 1; + Snap snap = 2; + Addr start = 3; +} + +message ReplyDisassemble { + int64 length = 1; +} + +// UI operations + +message RequestActivate { + DomObjId oid = 1; + ObjSpec object = 2; +} + +message ReplyActivate { +} + +// Snapshots + +message RequestSnapshot { + DomObjId oid = 1; + string description = 2; + string datetime = 3; + Snap snap = 4; +} + +message ReplySnapshot { +} + +// Client commands + +message MethodParameter { + string name = 1; + ValueType type = 2; + bool required = 3; + Value default_value = 4; + string display = 5; + string description = 6; +} + +message MethodArgument { + string name = 1; + Value value = 2; +} + +message Method { + string name = 1; + string action = 2; + string description = 3; + repeated MethodParameter parameters = 4; + // I'd like to make them all void, but I think executing a command and capturing its output + // justifies being able to return a result. It should be used very sparingly. + ValueType return_type = 5; +} + +message RequestNegotiate { + string version = 1; + repeated Method methods = 2; +} + +message ReplyNegotiate { +} + +message XRequestInvokeMethod { + optional DomObjId oid = 1; + string name = 2; + repeated MethodArgument arguments = 3; +} + +message XReplyInvokeMethod { + string error = 1; + Value return_value = 2; +} + +// Root + +message RootMessage { + oneof msg { + ReplyError error = 1; + + RequestNegotiate request_negotiate = 2; + ReplyNegotiate reply_negotiate = 3; + + RequestCreateTrace request_create_trace = 4; + ReplyCreateTrace reply_create_trace = 5; + + RequestSaveTrace request_save_trace = 6; + ReplySaveTrace reply_save_trace = 7; + + RequestCloseTrace request_close_trace = 8; + ReplyCloseTrace reply_close_trace = 9; + + RequestStartTx request_start_tx = 10; + ReplyStartTx reply_start_tx = 11; + + RequestEndTx request_end_tx = 12; + ReplyEndTx reply_end_tx = 13; + + RequestCreateOverlaySpace request_create_overlay = 14; + ReplyCreateOverlaySpace reply_create_overlay = 15; + + RequestSetMemoryState request_set_memory_state = 16; + ReplySetMemoryState reply_set_memory_state = 17; + + RequestPutBytes request_put_bytes = 18; + ReplyPutBytes reply_put_bytes = 19; + + RequestDeleteBytes request_delete_bytes = 20; + ReplyDeleteBytes reply_delete_bytes = 21; + + RequestPutRegisterValue request_put_register_value = 22; + ReplyPutRegisterValue reply_put_register_value = 23; + + RequestDeleteRegisterValue request_delete_register_value = 24; + ReplyDeleteRegisterValue reply_delete_register_value = 25; + + RequestCreateRootObject request_create_root_object = 26; + // Use same reply as CreateObject + RequestCreateObject request_create_object = 27; + ReplyCreateObject reply_create_object = 28; + + RequestInsertObject request_insert_object = 29; + ReplyInsertObject reply_insert_object = 30; + + RequestRemoveObject request_remove_object = 31; + ReplyRemoveObject reply_remove_object = 32; + + RequestSetValue request_set_value = 33; + ReplySetValue reply_set_value = 34; + + RequestRetainValues request_retain_values = 35; + ReplyRetainValues reply_retain_values = 36; + + RequestGetObject request_get_object = 37; + ReplyGetObject reply_get_object = 38; + + RequestGetValues request_get_values = 39; + ReplyGetValues reply_get_values = 40; + + RequestGetValuesIntersecting request_get_values_intersecting = 41; + // Reuse reply_get_values + + RequestDisassemble request_disassemble = 42; + ReplyDisassemble reply_disassemble = 43; + + RequestActivate request_activate = 44; + ReplyActivate reply_activate = 45; + + RequestSnapshot request_snapshot = 46; + ReplySnapshot reply_snapshot = 47; + + XRequestInvokeMethod xrequest_invoke_method = 48; + XReplyInvokeMethod xreply_invoke_method = 49; + } +} diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/py/LICENSE b/Ghidra/Debug/Debugger-rmi-trace/src/main/py/LICENSE new file mode 100644 index 0000000000..c026b6b79a --- /dev/null +++ b/Ghidra/Debug/Debugger-rmi-trace/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-rmi-trace/src/main/py/README.md b/Ghidra/Debug/Debugger-rmi-trace/src/main/py/README.md new file mode 100644 index 0000000000..f056742d2c --- /dev/null +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/py/README.md @@ -0,0 +1,3 @@ +# Ghidra Trace RMI + +Python 3 bindings for Ghidra's Trace RMI. \ No newline at end of file diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/py/pyproject.toml b/Ghidra/Debug/Debugger-rmi-trace/src/main/py/pyproject.toml new file mode 100644 index 0000000000..ee4b7aab28 --- /dev/null +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/py/pyproject.toml @@ -0,0 +1,25 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "ghidratrace" +version = "10.4" +authors = [ + { name="Ghidra Development Team" }, +] +description = "Ghidra's TraceRmi for Python3" +readme = "README.md" +requires-python = ">=3.7" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent", +] +dependencies = [ + "protobuf >= 3, < 4", +] + +[project.urls] +"Homepage" = "https://github.com/NationalSecurityAgency/ghidra" +"Bug Tracker" = "https://github.com/NationalSecurityAgency/ghidra/issues" diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/py/src/ghidratrace/__init__.py b/Ghidra/Debug/Debugger-rmi-trace/src/main/py/src/ghidratrace/__init__.py new file mode 100644 index 0000000000..8c2a3298a5 --- /dev/null +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/py/src/ghidratrace/__init__.py @@ -0,0 +1,15 @@ +## ### +# 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. +## diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/py/src/ghidratrace/client.py b/Ghidra/Debug/Debugger-rmi-trace/src/main/py/src/ghidratrace/client.py new file mode 100644 index 0000000000..6f3187727f --- /dev/null +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/py/src/ghidratrace/client.py @@ -0,0 +1,1107 @@ +## ### +# 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 deque, namedtuple +from concurrent.futures import Future +from contextlib import contextmanager +from dataclasses import dataclass +import inspect +import sys +from threading import Thread, Lock +import traceback +from typing import Any, List + +from . import sch +from . import trace_rmi_pb2 as bufs +from .util import send_delimited, recv_delimited + +# This need not be incremented every Ghidra release. When a breaking protocol +# change is made, this should be updated to match the first Ghidra release that +# includes the change. +VERSION = '10.4' + + +class RemoteResult(Future): + __slots__ = ('field_name', 'handler') + + def __init__(self, field_name, handler): + super().__init__() + self.field_name = field_name + self.handler = handler + + +class Receiver(Thread): + __slots__ = ('client', 'req_queue', '_is_shutdown') + + def __init__(self, client): + super().__init__(daemon=True) + self.client = client + self.req_queue = deque() + self.qlock = Lock() + self._is_shutdown = False + + def shutdown(self): + self._is_shutdown = True + + def _handle_invoke_method(self, request): + reply = bufs.RootMessage() + try: + result = self.client._handle_invoke_method(request) + Client._write_value( + reply.xreply_invoke_method.return_value, result) + except Exception as e: + reply.xreply_invoke_method.error = ''.join( + traceback.format_exc()) + self.client._send(reply) + + def _handle_reply(self, reply): + with self.qlock: + request = self.req_queue.popleft() + if reply.HasField('error'): + request.set_exception(TraceRmiError(reply.error.message)) + elif not reply.HasField(request.field_name): + request.set_exception(ProtocolError('expected {}, but got {}'.format( + request.field_name, reply.WhichOneof('msg')))) + else: + try: + result = request.handler( + getattr(reply, request.field_name)) + request.set_result(result) + except Exception as e: + request.set_exception(e) + + def _recv(self, field_name, handler): + fut = RemoteResult(field_name, handler) + with self.qlock: + self.req_queue.append(fut) + return fut + + def run(self): + dbg_seq = 0 + while not self._is_shutdown: + #print("Receiving message") + reply = recv_delimited(self.client.s, bufs.RootMessage(), dbg_seq) + #print(f"Got one: {reply.WhichOneof('msg')}") + dbg_seq += 1 + try: + if reply.HasField('xrequest_invoke_method'): + self.client._method_registry._executor.submit( + self._handle_invoke_method, reply.xrequest_invoke_method) + else: + self._handle_reply(reply) + except: + traceback.print_exc() + + +class TraceRmiError(Exception): + pass + + +class ProtocolError(Exception): + pass + + +class Transaction(object): + + def __init__(self, trace, id): + self.closed = False + self.trace = trace + self.id = id + self.lock = Lock() + + def __repr__(self): + return "".format( + self.id, self.trace, self.close) + + def commit(self): + with self.lock: + if self.closed: + return + self.closed = True + self.trace._end_tx(self.id, abort=False) + + def abort(self): + with self.lock: + if self.closed: + return + self.closed = True + self.trace._end_tx(self.id, abort=True) + + +RegVal = namedtuple('RegVal', ['name', 'value']) + + +class Address(namedtuple('BaseAddress', ['space', 'offset'])): + + def extend(self, length): + return AddressRange.extend(self, length) + + +class AddressRange(namedtuple('BaseAddressRange', ['space', 'min', 'max'])): + + @classmethod + def extend(cls, min, length): + return cls(min.space, min.offset, min.offset + length - 1) + + def length(self): + return self.max - self.min + 1 + + +class Lifespan(namedtuple('BaseLifespan', ['min', 'max'])): + + def __new__(cls, min, max=None): + if min is None: + min = -1 << 63 + if max is None: + max = (1 << 63) - 1 + if min > max and not (min == 0 and max == -1): + raise ValueError("min cannot exceed max") + return super().__new__(cls, min, max) + + def is_empty(self): + return self.min == 0 and self.max == -1 + + def __str__(self): + if self.is_empty(): + return "(EMPTY)" + min = '(-inf' if self.min == -1 << 63 else '[{}'.format(self.min) + max = '+inf)' if self.max == (1 << 63) - 1 else '{}]'.format(self.max) + return '{},{}'.format(min, max) + + def __repr__(self): + return 'Lifespan' + self.__str__() + + +DetachedObject = namedtuple('DetachedObject', ['id', 'path']) + + +class TraceObject(namedtuple('BaseTraceObject', ['trace', 'id', 'path'])): + """ + A proxy for a TraceObject + """ + __slots__ = () + + @classmethod + def from_id(cls, trace, id): + return cls(trace=trace, id=id, path=None) + + @classmethod + def from_path(cls, trace, path): + return cls(trace=trace, id=None, path=path) + + def insert(self, span=None, resolution='adjust'): + if span is None: + span = Lifespan(self.trace.snap()) + return self.trace._insert_object(self, span, resolution) + + def remove(self, span=None, tree=False): + if span is None: + span = Lifespan(self.trace.snap()) + return self.trace._remove_object(self, span, tree) + + def set_value(self, key, value, schema=None, span=None, resolution='adjust'): + if span is None: + span = Lifespan(self.trace.snap()) + return self.trace._set_value(self, span, key, value, schema, resolution) + + def retain_values(self, keys, span=None, kinds='elements'): + if span is None: + span = Lifespan(self.trace.snap()) + return self.trace._retain_values(self, span, kinds, keys) + + def activate(self): + self.trace._activate_object(self) + + +class TraceObjectValue(namedtuple('BaseTraceObjectValue', [ + 'parent', 'span', 'key', 'value', 'schema'])): + """ + A record of a TraceObjectValue + """ + __slots__ = () + + +class Trace(object): + + def __init__(self, client, id): + self._next_tx = 0 + self._txlock = Lock() + + self.closed = False + self.client = client + self.id = id + self.overlays = set() + + self._snap = None + self._snlock = Lock() + + def __repr__(self): + return "".format(self.id, self.closed) + + def close(self): + if self.closed: + return + self.client._close_trace(self.id) + self.closed = True + + def save(self): + return self.client._save_trace(self.id) + + def start_tx(self, description, undoable=False): + with self._txlock: + txid = self._next_tx + self._next_tx += 1 + self.client._start_tx(self.id, description, undoable, txid) + return Transaction(self, txid) + + @contextmanager + def open_tx(self, description, undoable=False): + tx = self.start_tx(description, undoable) + yield tx + tx.commit() + + def _end_tx(self, txid, abort): + return self.client._end_tx(self.id, txid, abort) + + def _next_snap(self): + with self._snlock: + if self._snap is None: + self._snap = 0 + else: + self._snap += 1 + return self._snap + + def snapshot(self, description, datetime=None): + """ + Create a snapshot. + + Future state operations implicitly modify this new snapshot. + """ + + snap = self._next_snap() + self.client._snapshot(self.id, description, datetime, snap) + return snap + + def snap(self): + return self._snap or 0 + + def set_snap(self, snap): + self._snap = snap + + def create_overlay_space(self, base, name): + if name in self.overlays: + return + result = self.client._create_overlay_space(self.id, base, name) + self.overlays.add(name) + return result + + def put_bytes(self, address, data, snap=None): + if snap is None: + snap = self.snap() + return self.client._put_bytes(self.id, snap, address, data) + + @staticmethod + def validate_state(state): + if not state in ('unknown', 'known', 'error'): + raise gdb.GdbError("Invalid memory state: {}".format(state)) + + def set_memory_state(self, range, state, snap=None): + if snap is None: + snap = self.snap() + return self.client._set_memory_state(self.id, snap, range, state) + + def delete_bytes(self, range, snap=None): + if snap is None: + snap = self.snap() + return self.client._delete_bytes(self.id, snap, range) + + def put_registers(self, space, values, snap=None): + if snap is None: + snap = self.snap() + return self.client._put_registers(self.id, snap, space, values) + + def delete_registers(self, space, names, snap=None): + if snap is None: + snap = self.snap() + return self.client._delete_registers(self.id, snap, space, names) + + def create_root_object(self, xml_context, schema): + return TraceObject(self, self.client._create_root_object(self.id, xml_context, schema), "") + + def create_object(self, path): + return TraceObject(self, self.client._create_object(self.id, path), path) + + def _insert_object(self, object, span, resolution): + return self.client._insert_object(self.id, object, span, resolution) + + def _remove_object(self, object, span, tree): + return self.client._remove_object(self.id, object, span, tree) + + def _set_value(self, object, span, key, value, schema, resolution): + return self.client._set_value(self.id, object, span, key, value, schema, resolution) + + def _retain_values(self, object, span, kinds, keys): + return self.client._retain_values(self.id, object, span, kinds, keys) + + def proxy_object_id(self, id): + return TraceObject.from_id(self, id) + + def proxy_object_path(self, path): + return TraceObject.from_path(self, path) + + def proxy_object(self, id=None, path=None): + if id is None and path is None: + raise ValueError("Must have id or path") + return TraceObject(self, id, path) + + def get_object(self, path_or_id): + id, path = self.client._get_object(self.id, path_or_id) + return TraceObject(self, id, path) + + def _fix_value(self, value, schema): + if schema != sch.OBJECT: + return value + id, path = value + return TraceObject(self, id, path) + + def _make_values(self, values): + return [ + TraceObjectValue(TraceObject(self, id, path), + span, key, self._fix_value(value, schema), schema) + for (id, path), span, key, (value, schema) in values + ] + + def get_values(self, pattern, span=None): + if span is None: + # singleton for getters + span = Lifespan(self.snap(), self.snap()) + return self._make_values(self.client._get_values(self.id, span, pattern)) + + def get_values_intersecting(self, rng, span=None): + if span is None: + span = Lifespan(self.snap(), self.snap()) + return self._make_values(self.client._get_values_intersecting(self.id, span, rng)) + + def _activate_object(self, object): + self.client._activate_object(self.id, object) + + def disassemble(self, start, snap=None): + if snap is None: + snap = self.snap() + return self.client._disassemble(self.id, snap, start) + + +@dataclass(frozen=True) +class RemoteParameter: + name: str + schema: sch.Schema + required: bool + default: Any + display: str + description: str + + +# Use instances as type annotations +@dataclass(frozen=True) +class ParamDesc: + type: Any + display: str + description: str = "" + + +@dataclass(frozen=True) +class RemoteMethod: + name: str + action: str + description: str + parameters: List[RemoteParameter] + return_schema: sch.Schema + callback: Any + + +class MethodRegistry(object): + + def __init__(self, executor): + self._methods = {} + self._executor = executor + + def register_method(self, method: RemoteMethod): + self._methods[method.name] = method + + @classmethod + def _to_schema(cls, p, annotation): + if isinstance(annotation, ParamDesc): + annotation = annotation.type + if isinstance(annotation, sch.Schema): + return annotation + elif isinstance(annotation, str): + return sch.Schema(annotation) + elif annotation is p.empty: + return sch.ANY + elif annotation is bool: + return sch.BOOL + elif annotation is int: + return sch.LONG + elif annotation is str: + return sch.STRING + elif annotation is bytes: + return sch.BYTE_ARR + elif annotation is Address: + return sch.ADDRESS + elif annotation is AddressRange: + return sch.RANGE + + @classmethod + def _to_display(cls, annotation): + if isinstance(annotation, ParamDesc): + return annotation.display + return '' + + @classmethod + def _to_description(cls, annotation): + if isinstance(annotation, ParamDesc): + return annotation.description + return '' + + @classmethod + def _make_param(cls, p): + schema = cls._to_schema(p, p.annotation) + required = p.default is p.empty + return RemoteParameter( + p.name, schema, required, None if required else p.default, + cls._to_display(p.annotation), cls._to_description(p.annotation)) + + @classmethod + def create_method(cls, function, name=None, action=None, description=None) -> RemoteMethod: + if name is None: + name = function.__name__ + if action is None: + action = name + if description is None: + description = function.__doc__ or '' + sig = inspect.signature(function) + params = [] + for p in sig.parameters.values(): + params.append(cls._make_param(p)) + return_schema = cls._to_schema(sig, sig.return_annotation) + return RemoteMethod(name, action, description, params, return_schema, function) + + def method(self, func=None, *, name=None, action=None, description='', + condition=True): + + def _method(func): + if condition: + method = self.create_method(func, name, action, description) + self.register_method(method) + return func + + if func is not None: + return _method(func) + return _method + + +class Batch(object): + + def __init__(self): + self.futures = [] + self.count = 0 + + def inc(self): + self.count += 1 + return self.count + + def dec(self): + self.count -= 1 + return self.count + + def append(self, fut): + self.futures.append(fut) + + def results(self, timeout=None): + return [f.result(timeout) for f in self.futures] + + +class Client(object): + + @staticmethod + def _write_address(to, address): + to.space = address.space + to.offset = address.offset + + @staticmethod + def _read_address(msg): + return Address(msg.space, msg.offset) + + @staticmethod + def _write_range(to, range): + to.space = range.space + to.offset = range.min + to.extend = range.length() - 1 + + @staticmethod + def _read_range(msg): + return Address(msg.space, msg.offset).extend(msg.extend + 1) + + @staticmethod + def _write_span(to, span): + to.min = span.min + to.max = span.max + + @staticmethod + def _read_span(msg): + return Lifespan(msg.min, msg.max) + + @staticmethod + def _write_obj_spec(to, path_or_id): + if isinstance(path_or_id, int): + to.id = path_or_id + elif isinstance(path_or_id, str): + to.path.path = path_or_id + elif isinstance(path_or_id.id, Future) and path_or_id.id.done(): + to.id = path_or_id.id.result() + elif isinstance(path_or_id.id, int): + to.id = path_or_id.id + elif path_or_id.path is not None: + to.path.path = path_or_id.path + else: + raise ValueError( + "Object/proxy has neither id nor path!: {}".format(path_or_id)) + + @staticmethod + def _read_obj_desc(msg): + return DetachedObject(msg.id, msg.path.path) + + @staticmethod + def _write_value(to, value, schema=None): + if value is None: + to.null_value.SetInParent() + return + elif isinstance(value, bool): + to.bool_value = value + return + elif isinstance(value, int): + if schema == sch.BYTE: + to.byte_value = value + return + elif schema == sch.CHAR: + to.char_value = value + return + elif schema == sch.SHORT: + to.short_value = value + return + elif schema == sch.INT: + to.int_value = value + return + elif schema == sch.LONG: + to.long_value = value + return + elif schema is None: + to.long_value = value + return + elif isinstance(value, str): + if schema == sch.CHAR_ARR: + to.char_arr_value = value + return + to.string_value = value + return + elif isinstance(value, bytes): + to.bytes_value = value + return + elif isinstance(value, Address): + Client._write_address(to.address_value, value) + return + elif isinstance(value, AddressRange): + Client._write_range(to.range_value, value) + return + elif isinstance(value, TraceObject): + Client._write_obj_spec(to.child_spec, value) + return + Client._try_write_array(to, value, schema) + + @staticmethod + def _try_write_array(to, value, schema): + if schema == sch.BOOL_ARR: + to.bool_arr_value.arr[:] = value + return + elif schema == sch.SHORT_ARR: + to.short_arr_value.arr[:] = value + return + elif schema == sch.INT_ARR: + to.int_arr_value.arr[:] = value + return + elif schema == sch.LONG_ARR: + to.long_arr_value.arr[:] = value + return + elif schema == sch.STRING_ARR: + to.string_arr_value.arr[:] = value + return + raise ValueError( + f"Cannot write Value: {schema}, {value}, {type(value)}") + + @staticmethod + def _write_parameter(to, p): + to.name = p.name + to.type.name = p.schema.name + to.required = p.required + Client._write_value(to.default_value, p.default) + to.display = p.display + to.description = p.description + + @staticmethod + def _write_parameters(to, parameters): + for i, p in enumerate(parameters): + to.add() + Client._write_parameter(to[i], p) + + @staticmethod + def _write_method(to: bufs.Method, method: RemoteMethod): + to.name = method.name + to.action = method.action + to.description = method.description + Client._write_parameters(to.parameters, method.parameters) + to.return_type.name = method.return_schema.name + + @staticmethod + def _write_methods(to, methods): + for i, method in enumerate(methods): + to.add() + Client._write_method(to[i], method) + + @staticmethod + def _read_value(msg): + name = msg.WhichOneof('value') + if name == 'null_value': + return None, sch.VOID + if name == 'bool_value': + return msg.bool_value, sch.BOOL + if name == 'byte_value': + return msg.byte_value, sch.BYTE + if name == 'char_value': + return chr(msg.char_value), sch.CHAR + if name == 'short_value': + return msg.short_value, sch.SHORT + if name == 'int_value': + return msg.int_value, sch.INT + if name == 'long_value': + return msg.long_value, sch.LONG + if name == 'string_value': + return msg.string_value, sch.STRING + if name == 'bool_arr_value': + return list(msg.bool_arr_value.arr), sch.BOOL_ARR + if name == 'bytes_value': + return msg.bytes_value, sch.BYTE_ARR + if name == 'char_arr_value': + return msg.char_arr_value, sch.CHAR_ARR + if name == 'short_arr_value': + return list(msg.short_arr_value.arr), sch.SHORT_ARR + if name == 'int_arr_value': + return list(msg.int_arr_value.arr), sch.INT_ARR + if name == 'long_arr_value': + return list(msg.long_arr_value.arr), sch.LONG_ARR + if name == 'string_arr_value': + return list(msg.string_arr_value.arr), sch.STRING_ARR + if name == 'address_value': + return Client._read_address(msg.address_value), sch.ADDRESS + if name == 'range_value': + return Client._read_range(msg.range_value), sch.RANGE + if name == 'child_desc': + return Client._read_obj_desc(msg.child_desc), sch.OBJECT + raise ValueError("Could not read value: {}".format(msg)) + + def __init__(self, s, method_registry: MethodRegistry): + self._traces = {} + self._next_trace_id = 1 + self.tlock = Lock() + + self.receiver = Receiver(self) + self.cur_batch = None + self._block = Lock() + self.s = s + self.slock = Lock() + self.receiver.start() + self._method_registry = method_registry + self._negotiate() + + def close(self): + self.s.close() + self.receiver.shutdown() + + def start_batch(self): + with self._block: + if self.cur_batch is None: + self.cur_batch = Batch() + self.cur_batch.inc() + return self.cur_batch + + def end_batch(self): + cb = None + with self._block: + if 0 == self.cur_batch.dec(): + cb = self.cur_batch + self.cur_batch = None + return cb.results() if cb else None + + @contextmanager + def batch(self): + """ + Execute a number of RMI calls in an asynchronous batch. + + This returns a context manager, meant to be used as follows: + + with client.batch(): + trace.set_value(...) + trace.set_value(...) + ... + + This is highly recommended when you know you will be making many rapid + RMI calls. All calls to the API that could involve RMI will instead + return a future within this context manager. The RMI message is sent + immediately, but the handling of the reply is off-loaded to a + background executor. Upon exiting the context, all futures created in + that context will be joined, so that every returned value is guaranteed + to be finished, notwithstanding catastrophic errors. Without this + context manager, every call will require a round trip, which will slow + things down. With the context, all the messages can be sent in rapid + succession, and then all the results awaited at once. + """ + + self.start_batch() + yield self.cur_batch + return self.end_batch() + + def _batch_or_now(self, root, field_name, handler): + with self.slock: + fut = self._recv(field_name, handler) + send_delimited(self.s, root) + if self.cur_batch is None: + return fut.result() + self.cur_batch.append(fut) + return fut + + def _now(self, root, field_name, handler): + with self.slock: + fut = self._recv(field_name, handler) + send_delimited(self.s, root) + return fut.result() + + def _send(self, root): + with self.slock: + send_delimited(self.s, root) + + def _recv(self, name, handler): + return self.receiver._recv(name, handler) + + def create_trace(self, path, language, compiler='default'): + root = bufs.RootMessage() + root.request_create_trace.path.path = path + root.request_create_trace.language.id = language + root.request_create_trace.compiler.id = compiler + with self.tlock: + root.request_create_trace.oid.id = self._next_trace_id + self._next_trace_id += 1 + trace = Trace(self, root.request_create_trace.oid.id) + self._traces[trace.id] = trace + + def _handle(reply): + pass + self._batch_or_now(root, 'reply_create_trace', _handle) + return trace + + def _close_trace(self, id): + root = bufs.RootMessage() + root.request_close_trace.oid.id = id + del self._traces[id] + + def _handle(reply): + pass + return self._batch_or_now(root, 'reply_close_trace', _handle) + + def _save_trace(self, id): + root = bufs.RootMessage() + root.request_save_trace.oid.id = id + + def _handle(reply): + pass + return self._batch_or_now(root, 'reply_save_trace', _handle) + + def _start_tx(self, id, description, undoable, txid): + root = bufs.RootMessage() + root.request_start_tx.oid.id = id + root.request_start_tx.undoable = undoable + root.request_start_tx.description = description + root.request_start_tx.txid.id = txid + + def _handle(reply): + pass + return self._batch_or_now(root, 'reply_start_tx', _handle) + + def _end_tx(self, id, txid, abort): + root = bufs.RootMessage() + root.request_end_tx.oid.id = id + root.request_end_tx.txid.id = txid + root.request_end_tx.abort = abort + + def _handle(reply): + pass + return self._batch_or_now(root, 'reply_end_tx', _handle) + + def _snapshot(self, id, description, datetime, snap): + root = bufs.RootMessage() + root.request_snapshot.oid.id = id + root.request_snapshot.description = description + root.request_snapshot.datetime = "" if datetime is None else datetime + root.request_snapshot.snap.snap = snap + + def _handle(reply): + pass + return self._batch_or_now(root, 'reply_snapshot', _handle) + + def _create_overlay_space(self, id, base, name): + root = bufs.RootMessage() + root.request_create_overlay.oid.id = id + root.request_create_overlay.baseSpace = base + root.request_create_overlay.name = name + + def _handle(reply): + pass + return self._batch_or_now(root, 'reply_create_overlay', _handle) + + def _put_bytes(self, id, snap, start, data): + root = bufs.RootMessage() + root.request_put_bytes.oid.id = id + root.request_put_bytes.snap.snap = snap + self._write_address(root.request_put_bytes.start, start) + root.request_put_bytes.data = data + + def _handle(reply): + return reply.written + return self._batch_or_now(root, 'reply_put_bytes', _handle) + + def _set_memory_state(self, id, snap, range, state): + root = bufs.RootMessage() + root.request_set_memory_state.oid.id = id + root.request_set_memory_state.snap.snap = snap + self._write_range(root.request_set_memory_state.range, range) + root.request_set_memory_state.state = getattr( + bufs, 'MS_' + state.upper()) + + def _handle(reply): + pass + return self._batch_or_now(root, 'reply_set_memory_state', _handle) + + def _delete_bytes(self, id, snap, range): + root = bufs.RootMessage() + root.request_delete_bytes.oid.id = id + root.request_delete_bytes.snap.snap = snap + self._write_range(root.request_delete_bytes.range, range) + + def _handle(reply): + pass + return self._batch_or_now(root, 'reply_delete_bytes', _handle) + + def _put_registers(self, id, snap, space, values): + root = bufs.RootMessage() + root.request_put_register_value.oid.id = id + root.request_put_register_value.snap.snap = snap + root.request_put_register_value.space = space + for v in values: + rv = bufs.RegVal() + rv.name = v.name + rv.value = v.value + root.request_put_register_value.values.append(rv) + + def _handle(reply): + return list(reply.skipped_names) + return self._batch_or_now(root, 'reply_put_register_value', _handle) + + def _delete_registers(self, id, snap, space, names): + root = bufs.RootMessage() + root.request_delete_register_value.oid.id = id + root.request_delete_register_value.snap.snap = snap + root.request_delete_register_value.space = space + root.request_delete_register_value.names.extend(names) + + def _handle(reply): + pass + return self._batch_or_now(root, 'reply_delete_register_value', _handle) + + def _create_root_object(self, id, xml_context, schema): + # TODO: An actual SchemaContext class? + root = bufs.RootMessage() + root.request_create_root_object.oid.id = id + root.request_create_root_object.schema_context = xml_context + root.request_create_root_object.root_schema = schema + + def _handle(reply): + return reply.object.id + return self._batch_or_now(root, 'reply_create_object', _handle) + + def _create_object(self, id, path): + root = bufs.RootMessage() + root.request_create_object.oid.id = id + root.request_create_object.path.path = path + + def _handle(reply): + return reply.object.id + return self._batch_or_now(root, 'reply_create_object', _handle) + + def _insert_object(self, id, object, span, resolution): + root = bufs.RootMessage() + root.request_insert_object.oid.id = id + self._write_obj_spec(root.request_insert_object.object, object) + self._write_span(root.request_insert_object.span, span) + root.request_insert_object.resolution = getattr( + bufs, 'CR_' + resolution.upper()) + + def _handle(reply): + return self._read_span(reply.span) + return self._batch_or_now(root, 'reply_insert_object', _handle) + + def _remove_object(self, id, object, span, tree): + root = bufs.RootMessage() + root.request_remove_object.oid.id = id + self._write_obj_spec(root.request_remove_object.object, object) + self._write_span(root.request_remove_object.span, span) + root.request_remove_object.tree = tree + + def _handle(reply): + pass + return self._batch_or_now(root, 'reply_remove_object', _handle) + + def _set_value(self, id, object, span, key, value, schema, resolution): + root = bufs.RootMessage() + root.request_set_value.oid.id = id + self._write_obj_spec(root.request_set_value.value.parent, object) + self._write_span(root.request_set_value.value.span, span) + root.request_set_value.value.key = key + self._write_value(root.request_set_value.value.value, value, schema) + root.request_set_value.resolution = getattr( + bufs, 'CR_' + resolution.upper()) + + def _handle(reply): + return Lifespan(reply.span.min, reply.span.max) + return self._batch_or_now(root, 'reply_set_value', _handle) + + def _retain_values(self, id, object, span, kinds, keys): + root = bufs.RootMessage() + root.request_retain_values.oid.id = id + self._write_obj_spec(root.request_retain_values.object, object) + self._write_span(root.request_retain_values.span, span) + root.request_retain_values.kinds = getattr( + bufs, 'VK_' + kinds.upper()) + root.request_retain_values.keys[:] = keys + + def _handle(reply): + pass + return self._batch_or_now(root, 'reply_retain_values', _handle) + + def _get_object(self, id, path_or_id): + root = bufs.RootMessage() + root.request_get_object.oid.id = id + self._write_obj_spec(root.request_get_object.object, path_or_id) + + def _handle(reply): + return self._read_obj_desc(reply.object) + return self._batch_or_now(root, 'reply_get_object', _handle) + + @staticmethod + def _read_values(reply): + return [ + (Client._read_obj_desc(v.parent), Client._read_span(v.span), + v.key, Client._read_value(v.value)) + for v in reply.values + ] + + @staticmethod + def _read_argument(arg, trace): + name = arg.name + value, schema = Client._read_value(arg.value) + if schema is sch.OBJECT: + if trace is None: + raise TypeError("Method requires trace binding") + id, path = value + return name, trace.proxy_object(id=id, path=path) + return name, value + + @staticmethod + def _read_arguments(arguments, trace): + kwargs = {} + for arg in arguments: + name, value = Client._read_argument(arg, trace) + kwargs[name] = value + return kwargs + + def _get_values(self, id, span, pattern): + root = bufs.RootMessage() + root.request_get_values.oid.id = id + self._write_span(root.request_get_values.span, span) + root.request_get_values.pattern.path = pattern + + def _handle(reply): + return self._read_values(reply) + return self._batch_or_now(root, 'reply_get_values', _handle) + + def _get_values_intersecting(self, id, span, rng): + root = bufs.RootMessage() + root.request_get_values_intersecting.oid.id = id + self._write_span(root.request_get_values_intersecting.box.span, span) + self._write_range(root.request_get_values_intersecting.box.range, rng) + + def _handle(reply): + return self._read_values(reply) + return self._batch_or_now(root, 'reply_get_values', _handle) + + def _activate_object(self, id, object): + root = bufs.RootMessage() + root.request_activate.oid.id = id + self._write_obj_spec(root.request_activate.object, object) + + def _handle(reply): + pass + return self._batch_or_now(root, 'reply_activate', _handle) + + def _disassemble(self, id, snap, start): + root = bufs.RootMessage() + root.request_disassemble.oid.id = id + root.request_disassemble.snap.snap = snap + self._write_address(root.request_disassemble.start, start) + + def _handle(reply): + return reply.length + return self._batch_or_now(root, 'reply_disassemble', _handle) + + def _negotiate(self): + root = bufs.RootMessage() + root.request_negotiate.version = VERSION + self._write_methods(root.request_negotiate.methods, + self._method_registry._methods.values()) + + def _handle(reply): + pass + self._now(root, 'reply_negotiate', _handle) + + def _handle_invoke_method(self, request): + if request.HasField('oid'): + if request.oid.id not in self._traces: + raise KeyError(f"Invalid domain object id: {request.oid.id}") + trace = self._traces[request.oid.id] + else: + trace = None + name = request.name + if not name in self._method_registry._methods: + raise KeyError(f"Invalid method name: {name}") + method = self._method_registry._methods[name] + kwargs = self._read_arguments(request.arguments, trace) + return method.callback(**kwargs) diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/py/src/ghidratrace/sch.py b/Ghidra/Debug/Debugger-rmi-trace/src/main/py/src/ghidratrace/sch.py new file mode 100644 index 0000000000..2505de01e1 --- /dev/null +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/py/src/ghidratrace/sch.py @@ -0,0 +1,47 @@ +## ### +# 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 dataclasses import dataclass + + +# Use instances as type annotations or as schema +@dataclass(frozen=True) +class Schema: + name: str + + def __str__(self): + return self.name + + +ANY = Schema('ANY') +OBJECT = Schema('OBJECT') +VOID = Schema('VOID') +BOOL = Schema('BOOL') +BYTE = Schema('BYTE') +CHAR = Schema('CHAR') +SHORT = Schema('SHORT') +INT = Schema('INT') +LONG = Schema('LONG') +STRING = Schema('STRING') +ADDRESS = Schema('ADDRESS') +RANGE = Schema('RANGE') + +BOOL_ARR = Schema('BOOL_ARR') +BYTE_ARR = Schema('BYTE_ARR') +CHAR_ARR = Schema('CHAR_ARR') +SHORT_ARR = Schema('SHORT_ARR') +INT_ARR = Schema('INT_ARR') +LONG_ARR = Schema('LONG_ARR') +STRING_ARR = Schema('STRING_ARR') diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/py/src/ghidratrace/util.py b/Ghidra/Debug/Debugger-rmi-trace/src/main/py/src/ghidratrace/util.py new file mode 100644 index 0000000000..e1fe43f175 --- /dev/null +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/py/src/ghidratrace/util.py @@ -0,0 +1,63 @@ +## ### +# 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 socket +import traceback + + +def send_all(s, data): + sent = 0 + while sent < len(data): + l = s.send(data[sent:]) + if l == 0: + raise Exception("Socket closed") + sent += l + + +def send_length(s, value): + send_all(s, value.to_bytes(4, 'big')) + + +def send_delimited(s, msg): + data = msg.SerializeToString() + send_length(s, len(data)) + send_all(s, data) + + +def recv_all(s, size): + buf = b'' + while len(buf) < size: + part = s.recv(size - len(buf)) + if len(part) == 0: + return buf + buf += part + return buf + #return s.recv(size, socket.MSG_WAITALL) + + +def recv_length(s): + buf = recv_all(s, 4) + if len(buf) < 4: + raise Exception("Socket closed") + return int.from_bytes(buf, 'big') + + +def recv_delimited(s, msg, dbg_seq): + size = recv_length(s) + buf = recv_all(s, size) + if len(buf) < size: + raise Exception("Socket closed") + msg.ParseFromString(buf) + return msg diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/py/tests/EMPTY b/Ghidra/Debug/Debugger-rmi-trace/src/main/py/tests/EMPTY new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/DebuggerCoordinates.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/DebuggerCoordinates.java index eb00644089..9796355d9f 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/DebuggerCoordinates.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/DebuggerCoordinates.java @@ -569,7 +569,10 @@ public class DebuggerCoordinates { } public TraceProgramView getView() { - return view; + if (trace == null) { + return view; // probably null + } + return view == null ? trace.getProgramView() : view; } public long getSnap() { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/CurrentPlatformTraceDisassembleAction.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/CurrentPlatformTraceDisassembleAction.java index 53f68583cb..c909e5edfe 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/CurrentPlatformTraceDisassembleAction.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/CurrentPlatformTraceDisassembleAction.java @@ -64,6 +64,9 @@ public class CurrentPlatformTraceDisassembleAction extends DockingAction { TraceObject object = current.getObject(); DebuggerPlatformMapper mapper = plugin.platformService.getMapper(trace, object, view.getSnap()); + if (mapper == null) { + return null; + } return new Reqs(mapper, thread, object, view); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/DebuggerDisassemblerPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/DebuggerDisassemblerPlugin.java index a6deeaf25d..046a4644aa 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/DebuggerDisassemblerPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/DebuggerDisassemblerPlugin.java @@ -76,7 +76,7 @@ public class DebuggerDisassemblerPlugin extends Plugin implements PopupActionPro } } - protected static RegisterValue deriveAlternativeDefaultContext(Language language, + public static RegisterValue deriveAlternativeDefaultContext(Language language, LanguageID alternative, Address address) { LanguageService langServ = DefaultLanguageService.getLanguageService(); Language altLang; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/DebuggerTrackLocationTrait.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/DebuggerTrackLocationTrait.java index 843d49f8ed..19c849ce76 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/DebuggerTrackLocationTrait.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/DebuggerTrackLocationTrait.java @@ -248,6 +248,9 @@ public class DebuggerTrackLocationTrait { // Change of current frame // Change of tracking settings DebuggerCoordinates cur = current; + if (cur.getView() == null) { + return AsyncUtils.nil(); + } TraceThread thread = cur.getThread(); if (thread == null || spec == null) { return AsyncUtils.nil(); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectsTreePanel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectsTreePanel.java index 7705999292..0e73710a53 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectsTreePanel.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectsTreePanel.java @@ -306,7 +306,9 @@ public class ObjectsTreePanel extends JPanel { return; } AbstractNode node = getNode(object.getCanonicalPath()); - tree.addSelectionPath(node.getTreePath()); + if (node != null) { + tree.addSelectionPath(node.getTreePath()); + } } public void selectCurrent() { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/register/DebuggerRegistersProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/register/DebuggerRegistersProvider.java index 0906c1312f..bdf3585fcb 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/register/DebuggerRegistersProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/register/DebuggerRegistersProvider.java @@ -937,7 +937,7 @@ public class DebuggerRegistersProvider extends ComponentProviderAdapter } TraceData getRegisterData(Register register) { - TraceCodeSpace space = getRegisterCodeSpace(false); + TraceCodeSpace space = getRegisterCodeSpace(register.getAddressSpace(), false); if (space == null) { return null; } @@ -1153,14 +1153,24 @@ public class DebuggerRegistersProvider extends ComponentProviderAdapter return getRegisterMemorySpace(current, space, createIfAbsent); } - protected TraceCodeSpace getRegisterCodeSpace(boolean createIfAbsent) { - TraceThread curThread = current.getThread(); - if (curThread == null) { + protected static TraceCodeSpace getRegisterCodeSpace(DebuggerCoordinates coords, + AddressSpace space, boolean createIfAbsent) { + if (!space.isRegisterSpace()) { + return coords.getTrace() + .getCodeManager() + .getCodeSpace(space, createIfAbsent); + } + TraceThread thread = coords.getThread(); + if (thread == null) { return null; } - return current.getTrace() + return coords.getTrace() .getCodeManager() - .getCodeRegisterSpace(curThread, current.getFrame(), createIfAbsent); + .getCodeRegisterSpace(thread, coords.getFrame(), createIfAbsent); + } + + protected TraceCodeSpace getRegisterCodeSpace(AddressSpace space, boolean createIfAbsent) { + return getRegisterCodeSpace(current, space, createIfAbsent); } protected Set collectBaseRegistersWithKnownValues(TraceThread thread) { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/utils/ManagedDomainObject.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/utils/ManagedDomainObject.java new file mode 100644 index 0000000000..38cc076786 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/utils/ManagedDomainObject.java @@ -0,0 +1,64 @@ +/* ### + * 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.utils; + +import java.io.IOException; +import java.lang.ref.Cleaner; + +import ghidra.framework.model.DomainFile; +import ghidra.framework.model.DomainObject; +import ghidra.util.exception.CancelledException; +import ghidra.util.exception.VersionException; +import ghidra.util.task.TaskMonitor; + +public class ManagedDomainObject implements AutoCloseable { + public static final Cleaner CLEANER = Cleaner.create(); + + private static class ObjectState implements Runnable { + private DomainObject obj; + + @Override + public synchronized void run() { + if (obj.getConsumerList().contains(this)) { + obj.release(this); + } + } + + public synchronized DomainObject get() { + if (!obj.getConsumerList().contains(this)) { + throw new IllegalStateException("Domain object is closed"); + } + return obj; + } + } + + private final ObjectState state = new ObjectState(); + + public ManagedDomainObject(DomainFile file, boolean okToUpgrade, boolean okToRecover, + TaskMonitor monitor) throws VersionException, CancelledException, IOException { + state.obj = file.getDomainObject(state, okToUpgrade, okToRecover, monitor); + CLEANER.register(this, state); + } + + @Override + public void close() throws Exception { + state.run(); + } + + public DomainObject get() { + return state.get(); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerListingService.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerListingService.java index 031c65922b..ab92e1e6a1 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerListingService.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerListingService.java @@ -26,10 +26,9 @@ import ghidra.program.util.ProgramSelection; /** * A service providing access to the main listing panel */ -@ServiceInfo( // - defaultProvider = DebuggerListingPlugin.class, // - description = "Replacement CodeViewerService for Debugger" // -) +@ServiceInfo( + defaultProvider = DebuggerListingPlugin.class, + description = "Replacement CodeViewerService for Debugger") public interface DebuggerListingService extends CodeViewerService { /** diff --git a/Ghidra/Debug/Framework-Debugging/src/expCloneExit/c/expCloneExit.c b/Ghidra/Debug/Framework-Debugging/src/expCloneExit/c/expCloneExit.c index 338e500fdf..8c62b27337 100644 --- a/Ghidra/Debug/Framework-Debugging/src/expCloneExit/c/expCloneExit.c +++ b/Ghidra/Debug/Framework-Debugging/src/expCloneExit/c/expCloneExit.c @@ -30,5 +30,6 @@ void* work(void* param) { int main() { pthread_create(&thread, NULL, work, (void*)1); + sleep(1); // Not ideal, but some assurance that we break with two threads return (int)work(NULL); } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetMemoryRegion.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetMemoryRegion.java index 323b3e25e5..65218756e0 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetMemoryRegion.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetMemoryRegion.java @@ -53,7 +53,6 @@ public interface TargetMemoryRegion extends TargetObject { * @return true if write is permitted */ @TargetAttributeType(name = WRITABLE_ATTRIBUTE_NAME, required = true, hidden = true) - public default boolean isWritable() { return getTypedAttributeNowByName(WRITABLE_ATTRIBUTE_NAME, Boolean.class, false); } @@ -64,7 +63,6 @@ public interface TargetMemoryRegion extends TargetObject { * @return true if execute is permitted */ @TargetAttributeType(name = EXECUTABLE_ATTRIBUTE_NAME, required = true, hidden = true) - public default boolean isExecutable() { return getTypedAttributeNowByName(EXECUTABLE_ATTRIBUTE_NAME, Boolean.class, false); } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/EnumerableTargetObjectSchema.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/EnumerableTargetObjectSchema.java index 62df310f2f..a4aa50d1a8 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/EnumerableTargetObjectSchema.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/EnumerableTargetObjectSchema.java @@ -89,7 +89,16 @@ public enum EnumerableTargetObjectSchema implements TargetObjectSchema { SET_ATTACH_KIND(TargetAttachKindSet.class), SET_BREAKPOINT_KIND(TargetBreakpointKindSet.class), SET_STEP_KIND(TargetStepKindSet.class), - EXECUTION_STATE(TargetExecutionState.class); + EXECUTION_STATE(TargetExecutionState.class), + // Additional types supported by the Trace database + CHAR(Character.class, char.class), + BOOL_ARR(boolean[].class), + BYTE_ARR(byte[].class), + CHAR_ARR(char[].class), + SHORT_ARR(short[].class), + INT_ARR(int[].class), + LONG_ARR(long[].class), + STRING_ARR(String[].class); public static final class MinimalSchemaContext extends DefaultSchemaContext { public static final SchemaContext INSTANCE = new MinimalSchemaContext(); diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelFactoryTest.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelFactoryTest.java index 53cb38ccb8..e473942284 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelFactoryTest.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelFactoryTest.java @@ -25,8 +25,7 @@ import org.junit.Test; import ghidra.dbg.DebugModelConventions.AsyncAccess; import ghidra.dbg.error.DebuggerModelTerminatingException; import ghidra.dbg.target.TargetObject; -import ghidra.dbg.target.schema.EnumerableTargetObjectSchema; -import ghidra.dbg.target.schema.TargetObjectSchema; +import ghidra.dbg.target.schema.*; import ghidra.util.Msg; public abstract class AbstractDebuggerModelFactoryTest extends AbstractDebuggerModelTest { @@ -78,6 +77,7 @@ public abstract class AbstractDebuggerModelFactoryTest extends AbstractDebuggerM TargetObjectSchema rootSchema = m.getModel().getRootSchema(); Msg.info(this, rootSchema.getContext()); + Msg.info(this, XmlSchemaContext.serialize(rootSchema.getContext())); assertFalse(rootSchema instanceof EnumerableTargetObjectSchema); } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/data/AbstractPcodeTraceDataAccess.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/data/AbstractPcodeTraceDataAccess.java index 532ea340e5..afdaa69f00 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/data/AbstractPcodeTraceDataAccess.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/data/AbstractPcodeTraceDataAccess.java @@ -108,7 +108,12 @@ public abstract class AbstractPcodeTraceDataAccess implements InternalPcodeTrace if (hostRange == null) { return; } - getMemoryOps(true).setState(snap, toOverlay(hostRange), state); + TraceMemoryOperations ops = getMemoryOps(true); + if (ops == null) { + throw new AssertionError("Cannot get memory operations for writing. " + + "This usually indicates a schema issue."); + } + ops.setState(snap, toOverlay(hostRange), state); } @Override @@ -178,7 +183,12 @@ public abstract class AbstractPcodeTraceDataAccess implements InternalPcodeTrace if (hostStart == null) { return 0; } - return getMemoryOps(true).putBytes(snap, toOverlay(hostStart), buf); + TraceMemoryOperations ops = getMemoryOps(true); + if (ops == null) { + throw new AssertionError("Cannot get memory operations for writing. " + + "This usually indicates a schema issue."); + } + return ops.putBytes(snap, toOverlay(hostStart), buf); } @Override diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceMemoryBufferEntry.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceMemoryBufferEntry.java index 044fe9afb6..df3ac3ec85 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceMemoryBufferEntry.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceMemoryBufferEntry.java @@ -131,9 +131,16 @@ public class DBTraceMemoryBufferEntry extends DBAnnotatedObject { if (compressed) { decompress(); } - buffer.put((blockNum << DBTraceMemorySpace.BLOCK_SHIFT) + dstOffset, buf.array(), - buf.arrayOffset() + buf.position(), len); - buf.position(buf.position() + len); + int bufOffset = (blockNum << DBTraceMemorySpace.BLOCK_SHIFT) + dstOffset; + if (buf.isReadOnly()) { + byte[] temp = new byte[len]; + buf.get(temp); + buffer.put(bufOffset, temp); + } + else { + buffer.put(bufOffset, buf.array(), buf.arrayOffset() + buf.position(), len); + buf.position(buf.position() + len); + } return len; } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceMemorySpace.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceMemorySpace.java index 72a269060e..69f7fb43d0 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceMemorySpace.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceMemorySpace.java @@ -645,7 +645,7 @@ public class DBTraceMemorySpace @Override public int putBytes(long snap, Address start, ByteBuffer buf) { assertInSpace(start); - int arrOff = buf.arrayOffset() + buf.position(); + int pos = buf.position(); try (LockHold hold = LockHold.lock(lock.writeLock())) { ByteBuffer oldBytes = ByteBuffer.allocate(buf.remaining()); @@ -659,7 +659,8 @@ public class DBTraceMemorySpace doSetState(snap, start, end, TraceMemoryState.KNOWN); // Read back the written bytes and fire event - byte[] bytes = Arrays.copyOfRange(buf.array(), arrOff, arrOff + result); + byte[] bytes = new byte[result]; + buf.get(pos, bytes); ImmutableTraceAddressSnapRange tasr = new ImmutableTraceAddressSnapRange(start, start.add(result - 1), snap, lastSnap.snap); trace.setChanged(new TraceChangeRecord<>(TraceMemoryBytesChangeType.CHANGED, diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/space/AbstractDBTraceSpaceBasedManager.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/space/AbstractDBTraceSpaceBasedManager.java index ec734aa51e..e5e0704167 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/space/AbstractDBTraceSpaceBasedManager.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/space/AbstractDBTraceSpaceBasedManager.java @@ -95,6 +95,10 @@ public abstract class AbstractDBTraceSpaceBasedManager newRegSpaces = new HashMap<>(); Map newMemSpaces = new HashMap<>(); for (TabledSpace ts : getTabledSpaces()) { - if (ts.isRegisterSpace()) { + if (ts.isRegisterSpace() && !ts.isOverlaySpace()) { newRegSpaces.put(ts.frame(), ts); } else { diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObject.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObject.java index 56747f5d3d..6858c2a2f3 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObject.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObject.java @@ -223,6 +223,8 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject { } DBTraceObject parent = doCreateCanonicalParentObject(); InternalTraceObjectValue value = parent.setValue(lifespan, path.key(), this, resolution); + // TODO: Should I re-order the recursion, so values are inserted from root to this? + // TODO: Should child lifespans be allowed to exceed the parent's? DBTraceObjectValPath path = parent.doInsert(lifespan, resolution); return path.append(value); } @@ -505,12 +507,39 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject { } } + protected Lifespan doAdjust(Lifespan span, String key, Object value) { + // Ordered by min, so I only need to consider the first conflict + // If start is contained in an entry, assume the user means to overwrite it. + for (InternalTraceObjectValue val : doGetValues(span, key)) { + if (Objects.equals(value, val.getValue())) { + continue; // not a conflict + } + if (val.getLifespan().contains(span.min())) { + continue; // user probably wants to overwrite the remainder of this entry + } + // Every entry intersects the span, so if we get one, adjust + return span.withMax(val.getMinSnap() - 1); + } + return span; + } + // TODO: Could/should this return Stream instead? protected Collection doGetValues(Lifespan span, String key) { return doGetValues(span.lmin(), span.lmax(), key); } + /** + * The implementation of {@link #getValues(Lifespan, String)} + * + *

+ * This collects entries ordered by min snap + * + * @param lower the lower snap + * @param upper the upper snap + * @param key the key + * @return the collection of values + */ protected Collection doGetValues(long lower, long upper, String key) { // Collect triplet-indexed values @@ -746,6 +775,9 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject { if (resolution == ConflictResolution.DENY) { doCheckConflicts(lifespan, key, value); } + else if (resolution == ConflictResolution.ADJUST) { + lifespan = doAdjust(lifespan, key, value); + } var setter = new ValueLifespanSetter(lifespan, value) { DBTraceObject canonicalLifeChanged = null; diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectManager.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectManager.java index 9bf9acbf8b..e990bb5c64 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectManager.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectManager.java @@ -23,6 +23,7 @@ import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.apache.commons.collections4.IteratorUtils; import org.jdom.JDOMException; import db.*; @@ -192,7 +193,18 @@ public class DBTraceObjectManager implements TraceObjectManager, DBTraceManager valueStore.getIndex(DBTraceObject.class, DBTraceObjectValue.CHILD_COLUMN); objectsView = Collections.unmodifiableCollection(objectStore.asMap().values()); - valuesView = Collections.unmodifiableCollection(valueStore.asMap().values()); + valuesView = new AbstractCollection<>() { + @Override + public Iterator iterator() { + return IteratorUtils.chainedIterator(valueStore.asMap().values().iterator(), + rangeValueMap.values().iterator()); + } + + @Override + public int size() { + return objectStore.getRecordCount() + rangeValueMap.size(); + } + }; } protected void loadRootSchema() { diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/InternalTraceObjectValue.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/InternalTraceObjectValue.java index 676299318c..1a014bd46f 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/InternalTraceObjectValue.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/InternalTraceObjectValue.java @@ -135,6 +135,9 @@ interface InternalTraceObjectValue extends TraceObjectValue { if (resolution == ConflictResolution.DENY) { getParent().doCheckConflicts(lifespan, getEntryKey(), getValue()); } + else if (resolution == ConflictResolution.ADJUST) { + lifespan = getParent().doAdjust(lifespan, getEntryKey(), getValue()); + } new ValueLifespanSetter(lifespan, getValue(), this) { @Override protected Iterable getIntersecting(Long lower, @@ -151,7 +154,8 @@ interface InternalTraceObjectValue extends TraceObjectValue { }.set(lifespan, getValue()); if (isObject()) { DBTraceObject child = getChild(); - child.emitEvents(new TraceChangeRecord<>(TraceObjectChangeType.LIFE_CHANGED, null, child)); + child.emitEvents( + new TraceChangeRecord<>(TraceObjectChangeType.LIFE_CHANGED, null, child)); } } } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/listing/TraceDefinedDataView.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/listing/TraceDefinedDataView.java index e1af4a7542..c10a315987 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/listing/TraceDefinedDataView.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/listing/TraceDefinedDataView.java @@ -21,7 +21,6 @@ import ghidra.program.model.lang.Register; import ghidra.program.model.util.CodeUnitInsertionException; import ghidra.trace.model.Lifespan; import ghidra.trace.model.guest.TracePlatform; -import ghidra.trace.util.TraceRegisterUtils; /** * A view of defined data units @@ -74,8 +73,8 @@ public interface TraceDefinedDataView extends TraceBaseDefinedUnitsView the desired type + * @return the value + */ + @SuppressWarnings("unchecked") + default T castValue() { + return (T) getValue(); + } + /** * Get the value as an object * diff --git a/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/database/ToyDBTraceBuilder.java b/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/database/ToyDBTraceBuilder.java index b071e40189..e139df6c85 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/database/ToyDBTraceBuilder.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/database/ToyDBTraceBuilder.java @@ -26,11 +26,13 @@ import java.nio.charset.CharsetEncoder; import java.nio.file.Files; import java.nio.file.Path; import java.util.Collection; +import java.util.List; -import db.Transaction; import db.DBHandle; +import db.Transaction; import generic.theme.GThemeDefaults.Colors.Messages; import ghidra.app.plugin.processors.sleigh.SleighLanguage; +import ghidra.dbg.util.PathPredicates; import ghidra.pcode.exec.*; import ghidra.pcode.exec.trace.TraceSleighUtils; import ghidra.program.disassemble.Disassembler; @@ -52,6 +54,7 @@ import ghidra.trace.model.*; import ghidra.trace.model.guest.TraceGuestPlatform; import ghidra.trace.model.guest.TracePlatform; import ghidra.trace.model.symbol.TraceReferenceManager; +import ghidra.trace.model.target.*; import ghidra.trace.model.thread.TraceThread; import ghidra.util.Msg; import ghidra.util.database.DBOpenMode; @@ -750,6 +753,60 @@ public class ToyDBTraceBuilder implements AutoCloseable { return getLanguage(langID).getCompilerSpecByID(new CompilerSpecID(compID)); } + /** + * Get an object by its canonical path + * + * @param canonicalPath the canonical path + * @return the object or null + */ + public TraceObject obj(String canonicalPath) { + return trace.getObjectManager() + .getObjectByCanonicalPath(TraceObjectKeyPath.parse(canonicalPath)); + } + + + /** + * Get an object by its path pattern + * + * @param path the path pattern + * @return the object or null + */ + public TraceObject objAny(String pat) { + return objAny(pat, Lifespan.at(0)); + } + public TraceObject objAny(String path, Lifespan span) { + return trace.getObjectManager().getObjectsByPath(span, TraceObjectKeyPath.parse(path)) + .findFirst() + .orElse(null); + } + + /** + * Get the value (not value entry) of an object + * + * @param obj the object + * @param snap the snapshot key + * @param key the entry key + * @return the value, possibly null + */ + public Object objValue(TraceObject obj, long snap, String key) { + TraceObjectValue value = obj.getValue(snap, key); + return value == null ? null : value.getValue(); + } + + /** + * List all values matching the given pattern at the given stnap. + * + * @param snap the snap + * @param pattern the pattern + * @return the list of values + */ + public List objValues(long snap, String pattern) { + return trace.getObjectManager() + .getValuePaths(Lifespan.at(snap), PathPredicates.parse(pattern)) + .map(p -> p.getDestinationValue(trace.getObjectManager().getRootObject())) + .toList(); + } + @Override public void close() { if (trace.getConsumerList().contains(this)) { diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/database/DBCachedObjectStoreFactory.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/database/DBCachedObjectStoreFactory.java index f8de282c18..3e1c7f8208 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/database/DBCachedObjectStoreFactory.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/database/DBCachedObjectStoreFactory.java @@ -929,6 +929,7 @@ public class DBCachedObjectStoreFactory { PrimitiveCodec STRING_ARR = new ArrayObjectCodec<>(new LengthBoundCodec<>(STRING)); + // TODO: No floats? Map> CODECS_BY_SELECTOR = Stream .of(BOOL, BYTE, CHAR, SHORT, INT, LONG, STRING, BOOL_ARR, BYTE_ARR, CHAR_ARR, SHORT_ARR, INT_ARR, LONG_ARR, STRING_ARR) diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/agent/gdb/rmi/AbstractGdbTraceRmiTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/agent/gdb/rmi/AbstractGdbTraceRmiTest.java new file mode 100644 index 0000000000..3b60d22608 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/agent/gdb/rmi/AbstractGdbTraceRmiTest.java @@ -0,0 +1,501 @@ +/* ### + * 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 agent.gdb.rmi; + +import static org.hamcrest.Matchers.startsWith; +import static org.junit.Assert.*; + +import java.io.*; +import java.net.*; +import java.nio.file.*; +import java.util.*; +import java.util.concurrent.*; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.junit.Before; +import org.junit.BeforeClass; + +import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest; +import ghidra.app.plugin.core.debug.service.rmi.trace.*; +import ghidra.app.plugin.core.debug.utils.ManagedDomainObject; +import ghidra.app.services.TraceRmiService; +import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; +import ghidra.dbg.testutil.DummyProc; +import ghidra.framework.TestApplicationUtils; +import ghidra.framework.main.ApplicationLevelOnlyPlugin; +import ghidra.framework.model.DomainFile; +import ghidra.framework.plugintool.Plugin; +import ghidra.framework.plugintool.util.*; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressRangeImpl; +import ghidra.trace.model.breakpoint.TraceBreakpointKind; +import ghidra.trace.model.breakpoint.TraceBreakpointKind.TraceBreakpointKindSet; +import ghidra.trace.model.target.*; +import ghidra.util.Msg; +import ghidra.util.NumericUtilities; + +public abstract class AbstractGdbTraceRmiTest extends AbstractGhidraHeadedDebuggerGUITest { + // Connecting should be the first thing the script does, so use a tight timeout. + protected static final int CONNECT_TIMEOUT_MS = 3000; + protected static final int TIMEOUT_SECONDS = 300; + protected static final int QUIT_TIMEOUT_MS = 1000; + public static final String INSTRUMENT_STOPPED = """ + ghidra trace tx-open "Fake" 'ghidra trace create-obj Inferiors[1]' + define do-set-stopped + ghidra trace set-value Inferiors[1] _state '"STOPPED"' + end + define set-stopped + ghidra trace tx-open Stopped do-set-stopped + end + python gdb.events.stop.connect(lambda e: gdb.execute("set-stopped"))"""; + public static final String INSTRUMENT_RUNNING = """ + ghidra trace tx-open "Fake" 'ghidra trace create-obj Inferiors[1]' + define do-set-running + ghidra trace set-value Inferiors[1] _state '"RUNNING"' + end + define set-running + ghidra trace tx-open Running do-set-running + end + python gdb.events.cont.connect(lambda e: gdb.execute("set-running"))"""; + + protected TraceRmiService traceRmi; + private Path gdbPath; + private Path outFile; + private Path errFile; + + @BeforeClass + public static void setupPython() throws Throwable { + new ProcessBuilder("gradle", "Debugger-agent-gdb:installPyPackage") + .directory(TestApplicationUtils.getInstallationDirectory()) + .inheritIO() + .start() + .waitFor(); + } + + protected Path getGdbPath() { + return Paths.get(DummyProc.which("gdb")); + } + + @Before + public void setupTraceRmi() throws Throwable { + traceRmi = addPlugin(tool, TraceRmiPlugin.class); + + gdbPath = getGdbPath(); + outFile = Files.createTempFile("gdbout", null); + errFile = Files.createTempFile("gdberr", null); + } + + protected void addAllDebuggerPlugins() throws PluginException { + PluginsConfiguration plugConf = new PluginsConfiguration() { + @Override + protected boolean accepts(Class pluginClass) { + return !ApplicationLevelOnlyPlugin.class.isAssignableFrom(pluginClass); + } + }; + + for (PluginDescription pd : plugConf + .getPluginDescriptions(PluginPackage.getPluginPackage("Debugger"))) { + addPlugin(tool, pd.getPluginClass()); + } + } + + protected static String addrToStringForGdb(InetAddress address) { + if (address.isAnyLocalAddress()) { + return "127.0.0.1"; // Can't connect to 0.0.0.0 as such. Choose localhost. + } + return address.getHostAddress(); + } + + protected static String sockToStringForGdb(SocketAddress address) { + if (address instanceof InetSocketAddress tcp) { + return addrToStringForGdb(tcp.getAddress()) + ":" + tcp.getPort(); + } + throw new AssertionError("Unhandled address type " + address); + } + + protected record GdbResult(boolean timedOut, int exitCode, String stdout, String stderr) { + protected String handle() { + if (!"".equals(stderr) | 0 != exitCode) { + throw new GdbError(exitCode, stdout, stderr); + } + return stdout; + } + } + + protected record ExecInGdb(Process gdb, CompletableFuture future) { + } + + @SuppressWarnings("resource") // Do not close stdin + protected ExecInGdb execInGdb(String script) throws IOException { + ProcessBuilder pb = new ProcessBuilder(gdbPath.toString()); + // If commands come from file, GDB will quit after EOF. + Msg.info(this, "outFile: " + outFile); + Msg.info(this, "errFile: " + errFile); + pb.redirectInput(ProcessBuilder.Redirect.PIPE); + pb.redirectOutput(outFile.toFile()); + pb.redirectError(errFile.toFile()); + Process gdbProc = pb.start(); + OutputStream stdin = gdbProc.getOutputStream(); + stdin.write(script.getBytes()); + stdin.flush(); + return new ExecInGdb(gdbProc, CompletableFuture.supplyAsync(() -> { + try { + if (!gdbProc.waitFor(TIMEOUT_SECONDS, TimeUnit.SECONDS)) { + Msg.error(this, "Timed out waiting for GDB"); + gdbProc.destroyForcibly(); + gdbProc.waitFor(TIMEOUT_SECONDS, TimeUnit.SECONDS); + return new GdbResult(true, -1, Files.readString(outFile), + Files.readString(errFile)); + } + Msg.info(this, "GDB exited with code " + gdbProc.exitValue()); + return new GdbResult(false, gdbProc.exitValue(), Files.readString(outFile), + Files.readString(errFile)); + } + catch (Exception e) { + return ExceptionUtils.rethrow(e); + } + finally { + gdbProc.destroyForcibly(); + } + })); + } + + protected static class GdbError extends RuntimeException { + public final int exitCode; + public final String stdout; + public final String stderr; + + public GdbError(int exitCode, String stdout, String stderr) { + super(""" + exitCode=%d: + ----stdout---- + %s + ----stderr---- + %s + """.formatted(exitCode, stdout, stderr)); + this.exitCode = exitCode; + this.stdout = stdout; + this.stderr = stderr; + } + } + + protected String runThrowError(String script) throws Exception { + CompletableFuture result = execInGdb(script).future; + return result.get(TIMEOUT_SECONDS, TimeUnit.SECONDS).handle(); + } + + protected record GdbAndHandler(ExecInGdb exec, TraceRmiHandler handler) + implements AutoCloseable { + protected RemoteMethod getMethod(String name) { + return Objects.requireNonNull(handler.getMethods().get(name)); + } + + public void execute(String cmd) { + RemoteMethod execute = getMethod("execute"); + execute.invoke(Map.of("cmd", cmd)); + } + + public RemoteAsyncResult executeAsync(String cmd) { + RemoteMethod execute = getMethod("execute"); + return execute.invokeAsync(Map.of("cmd", cmd)); + } + + public String executeCapture(String cmd) { + RemoteMethod execute = getMethod("execute"); + return (String) execute.invoke(Map.of("cmd", cmd, "to_string", true)); + } + + @Override + public void close() throws Exception { + Msg.info(this, "Cleaning up gdb"); + try { + try { + RemoteAsyncResult asyncQuit = executeAsync("quit"); + try { + asyncQuit.get(QUIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); + } + catch (TimeoutException e) { + /** + * This seems like a bug in gdb. AFAICT, it's a rehash or regression of + * https://sourceware.org/bugzilla/show_bug.cgi?id=17247. If I attach to the + * hung gdb, I get a similar stack trace, but with Python frames on the + * stack. The workaround given in the comments works here, too. I hesitate + * to point fingers, though, because I'm testing with a modern gdb-13.1 + * compiled from source on a rather un-modern distro. + */ + Msg.warn(this, "gdb hung on quit. Sending SIGCONT."); + Runtime.getRuntime().exec("kill -SIGCONT %d".formatted(exec.gdb.pid())); + asyncQuit.get(QUIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); + } + } + catch (TraceRmiError e) { + // expected + } + catch (ExecutionException e) { + if (!(e.getCause() instanceof TraceRmiError)) { + throw e; + } + } + GdbResult r = exec.future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + r.handle(); + waitForPass(() -> assertTrue(handler.isClosed())); + } + finally { + exec.gdb.destroyForcibly(); + } + } + } + + protected GdbAndHandler startAndConnectGdb(Function scriptSupplier) + throws Exception { + TraceRmiAcceptor acceptor = traceRmi.acceptOne(null); + ExecInGdb exec = execInGdb(scriptSupplier.apply(sockToStringForGdb(acceptor.getAddress()))); + acceptor.setTimeout(CONNECT_TIMEOUT_MS); + try { + TraceRmiHandler handler = acceptor.accept(); + return new GdbAndHandler(exec, handler); + } + catch (SocketTimeoutException e) { + exec.gdb.destroyForcibly(); + exec.future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS).handle(); + throw e; + } + } + + protected GdbAndHandler startAndConnectGdb() throws Exception { + return startAndConnectGdb(addr -> """ + set python print-stack full + python import ghidragdb + ghidra trace connect %s + """.formatted(addr)); + } + + @SuppressWarnings("resource") + protected String runThrowError(Function scriptSupplier) + throws Exception { + GdbAndHandler conn = startAndConnectGdb(scriptSupplier); + GdbResult r = conn.exec.future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + String stdout = r.handle(); + waitForPass(() -> assertTrue(conn.handler.isClosed())); + return stdout; + } + + protected void waitState(int infnum, Supplier snapSupplier, TargetExecutionState state) { + TraceObjectKeyPath infPath = TraceObjectKeyPath.parse("Inferiors").index(infnum); + TraceObject inf = + Objects.requireNonNull(tb.trace.getObjectManager().getObjectByCanonicalPath(infPath)); + waitForPass( + () -> assertEquals(state.name(), tb.objValue(inf, snapSupplier.get(), "_state"))); + waitTxDone(); + } + + protected void waitStopped() { + waitState(1, () -> 0L, TargetExecutionState.STOPPED); + } + + protected void waitRunning() { + waitState(1, () -> 0L, TargetExecutionState.RUNNING); + } + + protected String extractOutSection(String out, String head) { + return out.split(head)[1].split("---")[0].replace("(gdb)", "").trim(); + } + + record MemDump(long address, byte[] data) { + } + + protected MemDump parseHexDump(String dump) throws IOException { + // First, get the address. Assume contiguous, so only need top line. + List lines = List.of(dump.split("\n")); + List toksLine0 = List.of(lines.get(0).split("\\s+")); + assertThat(toksLine0.get(0), startsWith("0x")); + long address = Long.decode(toksLine0.get(0)); + + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + for (String l : lines) { + List parts = List.of(l.split(":")); + assertEquals(2, parts.size()); + String hex = parts.get(1).replaceAll("\\s*0x", ""); + byte[] lineData = NumericUtilities.convertStringToBytes(hex); + assertNotNull("Converted to null: " + hex, parts.get(1)); + buf.write(lineData); + } + return new MemDump(address, buf.toByteArray()); + } + + record RegDump() { + } + + protected RegDump parseRegDump(String dump) { + return new RegDump(); + } + + protected ManagedDomainObject openDomainObject(String path) throws Exception { + DomainFile df = env.getProject().getProjectData().getFile(path); + assertNotNull(df); + return new ManagedDomainObject(df, false, false, monitor); + } + + protected ManagedDomainObject waitDomainObject(String path) throws Exception { + DomainFile df; + long start = System.currentTimeMillis(); + while (true) { + df = env.getProject().getProjectData().getFile(path); + if (df != null) { + return new ManagedDomainObject(df, false, false, monitor); + } + Thread.sleep(1000); + if (System.currentTimeMillis() - start > 30000) { + throw new TimeoutException("30 seconds expired waiting for domain file"); + } + } + } + + protected void assertBreakLoc(TraceObjectValue locVal, String key, Address addr, int len, + Set kinds, String expression) throws Exception { + assertEquals(key, locVal.getEntryKey()); + TraceObject loc = locVal.getChild(); + TraceObject spec = loc.getCanonicalParent(0).getParent(); + assertEquals(new AddressRangeImpl(addr, len), loc.getValue(0, "_range").getValue()); + assertEquals(TraceBreakpointKindSet.encode(kinds), spec.getValue(0, "_kinds").getValue()); + assertEquals(expression, spec.getValue(0, "_expression").getValue()); + } + + protected void waitTxDone() { + waitFor(() -> tb.trace.getCurrentTransactionInfo() == null); + } + + private record Cut(String head, int begin, int end) { + String parseCell(String line) { + int begin = Math.min(line.length(), this.begin); + int end = Math.min(line.length(), this.end); + /** + * NOTE: Do not assert previous char is space. + * + * When breakpoints table spells out locations, Address and What cells are indented and + * no longer align with their column headers. + */ + return line.substring(begin, end).trim(); + } + } + + protected record Row(Map cells) { + private static Row parse(List cuts, String line) { + return new Row( + cuts.stream().collect(Collectors.toMap(Cut::head, c -> c.parseCell(line)))); + } + + public String getCell(String head) { + return cells.get(head); + } + } + + protected record Tabular(List headings, List rows) { + static final Pattern SPACES = Pattern.compile(" *"); + static final Pattern WORDS = Pattern.compile("\\w+"); + + private static List findCuts(String header) { + List result = new ArrayList<>(); + Matcher spaceMatcher = SPACES.matcher(header); + Matcher wordMatcher = WORDS.matcher(header); + int start = 0; + while (start < header.length()) { + if (!spaceMatcher.find(start)) { + throw new AssertionError(); + } + start = spaceMatcher.end(); + if (start >= header.length()) { + break; + } + if (!wordMatcher.find(start)) { + throw new AssertionError(); + } + result.add(new Cut(wordMatcher.group(), wordMatcher.start(), wordMatcher.end())); + start = wordMatcher.end(); + } + return result; + } + + private static List adjustCuts(List cuts) { + List result = new ArrayList<>(); + for (int i = 0; i < cuts.size(); i++) { + Cut cut = cuts.get(i); + int j = i + 1; + int end = j < cuts.size() ? cuts.get(j).begin : Integer.MAX_VALUE; + result.add(new Cut(cut.head, cut.begin, end)); + } + return result; + } + + /** + * Parse a table. + * + *

+ * This is far from perfect, but good enough for making assertions in tests. For example, in + * the breakpoints table, gdb may insert an extra informational line under a breakpoint row. + * This line will get mangled and parsed as if it were an entry. However, it's "Num" cell + * will be empty, so they will not likely interfere. + * + * @param out the output in tabular form + * @return the table object, more or less + */ + public static Tabular parse(String out) { + List lines = List.of(out.split("\n")); + if (lines.isEmpty()) { + throw new AssertionError("Output is not tabular"); + } + List cuts = adjustCuts(findCuts(lines.get(0))); + return new Tabular(cuts.stream().map(Cut::head).toList(), + lines.stream().skip(1).map(l -> Row.parse(cuts, l)).toList()); + } + + public Row findRow(String head, String contents) { + return rows.stream() + .filter(r -> Objects.equals(contents, r.getCell(head))) + .findFirst() + .orElse(null); + } + } + + public static void waitForPass(Runnable runnable, long timeoutMs, long retryDelayMs) { + long start = System.currentTimeMillis(); + AssertionError lastError = null; + while (System.currentTimeMillis() - start < timeoutMs) { + try { + runnable.run(); + return; + } + catch (AssertionError e) { + lastError = e; + } + try { + Thread.sleep(retryDelayMs); + } + catch (InterruptedException e) { + // Retry sooner, I guess. + } + } + if (lastError == null) { + throw new AssertionError("Timed out before first try?"); + } + throw lastError; + } +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/agent/gdb/rmi/GdbCommandsTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/agent/gdb/rmi/GdbCommandsTest.java new file mode 100644 index 0000000000..92ca1b65c3 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/agent/gdb/rmi/GdbCommandsTest.java @@ -0,0 +1,1213 @@ +/* ### + * 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 agent.gdb.rmi; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.nio.ByteBuffer; +import java.util.*; +import java.util.Map.Entry; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.junit.Test; + +import db.Transaction; +import generic.Unique; +import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiAcceptor; +import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiHandler; +import ghidra.app.plugin.core.debug.utils.ManagedDomainObject; +import ghidra.dbg.testutil.DummyProc; +import ghidra.dbg.util.PathPredicates; +import ghidra.framework.model.DomainFile; +import ghidra.program.model.address.*; +import ghidra.program.model.data.Float10DataType; +import ghidra.program.model.lang.RegisterValue; +import ghidra.program.model.listing.CodeUnit; +import ghidra.trace.database.ToyDBTraceBuilder; +import ghidra.trace.model.*; +import ghidra.trace.model.breakpoint.TraceBreakpointKind; +import ghidra.trace.model.listing.TraceCodeSpace; +import ghidra.trace.model.listing.TraceData; +import ghidra.trace.model.memory.*; +import ghidra.trace.model.modules.TraceModule; +import ghidra.trace.model.target.*; +import ghidra.trace.model.time.TraceSnapshot; +import ghidra.util.Msg; + +public class GdbCommandsTest extends AbstractGdbTraceRmiTest { + + //@Test + public void testManual() throws Exception { + TraceRmiAcceptor acceptor = traceRmi.acceptOne(null); + Msg.info(this, + "Use: ghidra trace connect " + sockToStringForGdb(acceptor.getAddress())); + TraceRmiHandler handler = acceptor.accept(); + Msg.info(this, "Connected: " + sockToStringForGdb(handler.getRemoteAddress())); + handler.waitClosed(); + Msg.info(this, "Closed"); + } + + @Test + public void testConnectErrorNoArg() throws Exception { + try { + runThrowError(""" + set python print-stack full + python import ghidragdb + ghidra trace connect + quit + """); + fail(); + } + catch (GdbError e) { + assertThat(e.stderr, containsString("'ghidra trace connect'")); + assertThat(e.stderr, containsString("'address'")); + } + } + + @Test + public void testConnect() throws Exception { + runThrowError(addr -> """ + set python print-stack full + python import ghidragdb + ghidra trace connect %s + quit + """.formatted(addr)); + } + + @Test + public void testDisconnect() throws Exception { + runThrowError(addr -> """ + set python print-stack full + python import ghidragdb + ghidra trace connect %s + ghidra trace disconnect + quit + """.formatted(addr)); + } + + @Test + public void testStartTraceDefaults() throws Exception { + // Default name and lcsp + runThrowError(addr -> """ + set python print-stack full + python import ghidragdb + ghidra trace connect %s + file bash + ghidra trace start + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + assertEquals("x86:LE:64:default", + tb.trace.getBaseLanguage().getLanguageID().getIdAsString()); + assertEquals("gcc", + tb.trace.getBaseCompilerSpec().getCompilerSpecID().getIdAsString()); + } + } + + @Test + public void testStartTraceDefaultNoFile() throws Exception { + runThrowError(addr -> """ + set python print-stack full + python import ghidragdb + ghidra trace connect %s + ghidra trace start + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/noname")) { + assertThat(mdo.get(), instanceOf(Trace.class)); + } + } + + @Test + public void testStartTraceCustomize() throws Exception { + runThrowError(addr -> """ + set python print-stack full + python import ghidragdb + ghidra trace connect %s + file bash + set ghidra-language Toy:BE:64:default + set ghidra-compiler default + ghidra trace start myToy + quit + """.formatted(addr)); + DomainFile dfMyToy = env.getProject().getProjectData().getFile("/New Traces/myToy"); + assertNotNull(dfMyToy); + try (ManagedDomainObject mdo = new ManagedDomainObject(dfMyToy, false, false, monitor)) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + assertEquals("Toy:BE:64:default", + tb.trace.getBaseLanguage().getLanguageID().getIdAsString()); + assertEquals("default", + tb.trace.getBaseCompilerSpec().getCompilerSpecID().getIdAsString()); + } + } + + @Test + public void testStopTrace() throws Exception { + // TODO: This test assumes gdb and the target file bash are x86-64 + runThrowError(addr -> """ + set python print-stack full + python import ghidragdb + ghidra trace connect %s + file bash + ghidra trace start + ghidra trace stop + quit + """.formatted(addr)); + DomainFile dfBash = env.getProject().getProjectData().getFile("/New Traces/gdb/bash"); + assertNotNull(dfBash); + // TODO: Given the 'quit' command, I'm not sure this assertion is checking anything. + assertFalse(dfBash.isOpen()); + } + + @Test + public void testInfo() throws Exception { + AtomicReference refAddr = new AtomicReference<>(); + String out = runThrowError(addr -> { + refAddr.set(addr); + return """ + set python print-stack full + file bash + echo \\n + python import ghidragdb + echo \\n---Import---\\n + ghidra trace info + ghidra trace connect %s + echo \\n---Connect---\\n + ghidra trace info + ghidra trace start + echo \\n---Start---\\n + ghidra trace info + ghidra trace stop + echo \\n---Stop---\\n + ghidra trace info + ghidra trace disconnect + echo \\n---Disconnect---\\n + ghidra trace info + quit + """.formatted(addr); + }); + + assertEquals(""" + Not connected to Ghidra""", + extractOutSection(out, "---Import---")); + assertEquals(""" + Connected to Ghidra at %s + No trace""".formatted(refAddr.get()), + extractOutSection(out, "---Connect---")); + assertEquals(""" + Connected to Ghidra at %s + Trace active""".formatted(refAddr.get()), + extractOutSection(out, "---Start---")); + assertEquals(""" + Connected to Ghidra at %s + No trace""".formatted(refAddr.get()), + extractOutSection(out, "---Stop---")); + assertEquals(""" + Not connected to Ghidra""", + extractOutSection(out, "---Disconnect---")); + } + + @Test + public void testLcsp() throws Exception { + // TODO: This test assumes x86-64 on test system + String out = runThrowError(""" + set python print-stack full + python import ghidragdb + echo \\n---Import---\\n + ghidra trace lcsp + echo \\n---\\n + file bash + echo \\n---File---\\n + ghidra trace lcsp + set ghidra-language Toy:BE:64:default + echo \\n---Language---\\n + ghidra trace lcsp + set ghidra-compiler posStack + echo \\n---Compiler---\\n + ghidra trace lcsp + quit + """); + assertEquals(""" + Selected Ghidra language: DATA:LE:64:default + Selected Ghidra compiler: pointer64""", + extractOutSection(out, "---Import---")); + assertEquals(""" + Selected Ghidra language: x86:LE:64:default + Selected Ghidra compiler: gcc""", + extractOutSection(out, "---File---")); + assertEquals(""" + Selected Ghidra language: Toy:BE:64:default + Selected Ghidra compiler: default""", + extractOutSection(out, "---Language---")); + assertEquals(""" + Selected Ghidra language: Toy:BE:64:default + Selected Ghidra compiler: posStack""", + extractOutSection(out, "---Compiler---")); + } + + @Test + public void testSave() throws Exception { + traceManager.setSaveTracesByDefault(false); + + // For sanity check, verify failing to save drops data + runThrowError(addr -> """ + set python print-stack full + python import ghidragdb + ghidra trace connect %s + file bash + ghidra trace start no-save + ghidra trace tx-start "Create snapshot" + ghidra trace new-snap "Scripted snapshot" + ghidra trace tx-commit + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/no-save")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + assertEquals(0, tb.trace.getTimeManager().getAllSnapshots().size()); + } + + runThrowError(addr -> """ + set python print-stack full + python import ghidragdb + ghidra trace connect %s + file bash + ghidra trace start save + ghidra trace tx-start "Create snapshot" + ghidra trace new-snap "Scripted snapshot" + ghidra trace tx-commit + ghidra trace save + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/save")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + assertEquals(1, tb.trace.getTimeManager().getAllSnapshots().size()); + } + } + + @Test + public void testSnapshot() throws Exception { + runThrowError(addr -> """ + set python print-stack full + python import ghidragdb + ghidra trace connect %s + file bash + ghidra trace start + ghidra trace tx-start "Create snapshot" + ghidra trace new-snap "Scripted snapshot" + ghidra trace tx-commit + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + TraceSnapshot snapshot = Unique.assertOne(tb.trace.getTimeManager().getAllSnapshots()); + assertEquals(0, snapshot.getKey()); + assertEquals("Scripted snapshot", snapshot.getDescription()); + } + } + + @Test + public void testPutmem() throws Exception { + String out = runThrowError(addr -> """ + set python print-stack full + python import ghidragdb + ghidra trace connect %s + file bash + start + ghidra trace start + ghidra trace tx-start "Create snapshot" + ghidra trace new-snap "Scripted snapshot" + ghidra trace putmem &main 10 + ghidra trace tx-commit + echo \\n---Dump---\\n + x/10bx main + echo \\n--- + kill + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + long snap = Unique.assertOne(tb.trace.getTimeManager().getAllSnapshots()).getKey(); + + MemDump dump = parseHexDump(extractOutSection(out, "---Dump---")); + ByteBuffer buf = ByteBuffer.allocate(dump.data().length); + tb.trace.getMemoryManager().getBytes(snap, tb.addr(dump.address()), buf); + + assertArrayEquals(dump.data(), buf.array()); + } + } + + @Test + public void testPutmemInferior2() throws Exception { + String out = runThrowError(addr -> """ + set python print-stack full + python import ghidragdb + ghidra trace connect %s + add-inferior + inferior 2 + file bash + start + ghidra trace start + ghidra trace tx-start "Create snapshot" + ghidra trace new-snap "Scripted snapshot" + ghidra trace putmem &main 10 + ghidra trace tx-commit + echo \\n---Dump---\\n + x/10bx main + echo \\n--- + kill + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + AddressSpace ram2 = tb.trace.getBaseAddressFactory().getAddressSpace("ram2"); + assertNotNull(ram2); + long snap = Unique.assertOne(tb.trace.getTimeManager().getAllSnapshots()).getKey(); + + MemDump dump = parseHexDump(extractOutSection(out, "---Dump---")); + ByteBuffer buf = ByteBuffer.allocate(dump.data().length); + tb.trace.getMemoryManager().getBytes(snap, ram2.getAddress(dump.address()), buf); + + assertArrayEquals(dump.data(), buf.array()); + } + } + + @Test + public void testPutmemState() throws Exception { + String out = runThrowError(addr -> """ + set python print-stack full + python import ghidragdb + ghidra trace connect %s + file bash + start + ghidra trace start + ghidra trace tx-start "Create snapshot" + ghidra trace new-snap "Scripted snapshot" + ghidra trace putmem-state &main 10 error + ghidra trace tx-commit + echo \\n---Start---\\n + print/x &main + echo \\n--- + kill + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + long snap = Unique.assertOne(tb.trace.getTimeManager().getAllSnapshots()).getKey(); + + String eval = extractOutSection(out, "---Start---"); + Address addr = tb.addr(Long.decode(eval.split("=")[1].trim())); + + Entry entry = + tb.trace.getMemoryManager().getMostRecentStateEntry(snap, addr); + assertEquals(Map.entry(new ImmutableTraceAddressSnapRange( + new AddressRangeImpl(addr, 10), Lifespan.at(0)), TraceMemoryState.ERROR), entry); + } + } + + @Test + public void testDelmem() throws Exception { + String out = runThrowError(addr -> """ + set python print-stack full + python import ghidragdb + ghidra trace connect %s + file bash + start + ghidra trace start + ghidra trace tx-start "Create snapshot" + ghidra trace new-snap "Scripted snapshot" + ghidra trace putmem &main 10 + ghidra trace delmem &main 5 + ghidra trace tx-commit + echo \\n---Dump---\\n + x/10bx main + echo \\n--- + kill + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + long snap = Unique.assertOne(tb.trace.getTimeManager().getAllSnapshots()).getKey(); + + MemDump dump = parseHexDump(extractOutSection(out, "---Dump---")); + Arrays.fill(dump.data(), 0, 5, (byte) 0); + ByteBuffer buf = ByteBuffer.allocate(dump.data().length); + tb.trace.getMemoryManager().getBytes(snap, tb.addr(dump.address()), buf); + + assertArrayEquals(dump.data(), buf.array()); + } + } + + @Test + public void testPutreg() throws Exception { + String count = IntStream.iterate(0, i -> i < 32, i -> i + 1) + .mapToObj(Integer::toString) + .collect(Collectors.joining(",", "{", "}")); + runThrowError(addr -> """ + set python print-stack full + python import ghidragdb + ghidra trace connect %s + file bash + start + ghidra trace start + set $ymm0.v32_int8 = %s + set $st0 = 1.5 + ghidra trace tx-start "Create snapshot" + ghidra trace new-snap "Scripted snapshot" + ghidra trace putreg + ghidra trace tx-commit + kill + quit + """.formatted(addr, count)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + long snap = Unique.assertOne(tb.trace.getTimeManager().getAllSnapshots()).getKey(); + AddressSpace t1f0 = tb.trace.getBaseAddressFactory() + .getAddressSpace("Inferiors[1].Threads[1].Stack[0].Registers"); + TraceMemorySpace regs = tb.trace.getMemoryManager().getMemorySpace(t1f0, false); + RegisterValue ymm0 = regs.getValue(snap, tb.reg("ymm0")); + // GDB treats registers in arch's endian + assertEquals("1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100", + ymm0.getUnsignedValue().toString(16)); + + TraceData st0; + try (Transaction tx = tb.trace.openTransaction("Float80 unit")) { + TraceCodeSpace code = tb.trace.getCodeManager().getCodeSpace(t1f0, true); + st0 = code.definedData() + .create(Lifespan.nowOn(0), tb.reg("st0"), Float10DataType.dataType); + } + + assertEquals("1.5", st0.getDefaultValueRepresentation()); + } + } + + @Test + public void testDelreg() throws Exception { + String count = IntStream.iterate(0, i -> i < 32, i -> i + 1) + .mapToObj(Integer::toString) + .collect(Collectors.joining(",", "{", "}")); + runThrowError(addr -> """ + set python print-stack full + python import ghidragdb + ghidra trace connect %s + file bash + start + ghidra trace start + set $ymm0.v32_int8 = %s + set $st0 = 1.5 + ghidra trace tx-start "Create snapshot" + ghidra trace new-snap "Scripted snapshot" + ghidra trace putreg + ghidra trace delreg + ghidra trace tx-commit + kill + quit + """.formatted(addr, count)); + // The spaces will be left over, but the values should be zeroed + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + long snap = Unique.assertOne(tb.trace.getTimeManager().getAllSnapshots()).getKey(); + AddressSpace t1f0 = tb.trace.getBaseAddressFactory() + .getAddressSpace("Inferiors[1].Threads[1].Stack[0].Registers"); + TraceMemorySpace regs = tb.trace.getMemoryManager().getMemorySpace(t1f0, false); + RegisterValue ymm0 = regs.getValue(snap, tb.reg("ymm0")); + assertEquals("0", ymm0.getUnsignedValue().toString(16)); + + TraceData st0; + try (Transaction tx = tb.trace.openTransaction("Float80 unit")) { + TraceCodeSpace code = tb.trace.getCodeManager().getCodeSpace(t1f0, true); + st0 = code.definedData() + .create(Lifespan.nowOn(0), tb.reg("st0"), Float10DataType.dataType); + } + + assertEquals("0.0", st0.getDefaultValueRepresentation()); + } + } + + @Test + public void testCreateObj() throws Exception { + String out = runThrowError(addr -> """ + set python print-stack full + python import ghidragdb + ghidra trace connect %s + ghidra trace start + ghidra trace tx-start "Create Object" + echo \\n---Id---\\n + ghidra trace create-obj Test.Objects[1] + echo \\n--- + ghidra trace tx-commit + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/noname")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + TraceObject object = tb.trace.getObjectManager() + .getObjectByCanonicalPath(TraceObjectKeyPath.parse("Test.Objects[1]")); + assertNotNull(object); + String created = extractOutSection(out, "---Id---"); + long id = Long.parseLong(created.split("id=")[1].split(",")[0]); + assertEquals(object.getKey(), id); + } + } + + @Test + public void testInsertObj() throws Exception { + String out = runThrowError(addr -> """ + set python print-stack full + python import ghidragdb + ghidra trace connect %s + ghidra trace start + ghidra trace tx-start "Create Object" + ghidra trace create-obj Test.Objects[1] + echo \\n---Lifespan---\\n + ghidra trace insert-obj Test.Objects[1] + echo \\n--- + ghidra trace tx-commit + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/noname")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + TraceObject object = tb.trace.getObjectManager() + .getObjectByCanonicalPath(TraceObjectKeyPath.parse("Test.Objects[1]")); + assertNotNull(object); + Lifespan life = Unique.assertOne(object.getLife().spans()); + assertEquals(Lifespan.nowOn(0), life); + String lifeStr = extractOutSection(out, "---Lifespan---"); + assertEquals("Inserted object: lifespan=[0,+inf)", lifeStr); + } + } + + @Test + public void testRemoveObj() throws Exception { + runThrowError(addr -> """ + set python print-stack full + python import ghidragdb + ghidra trace connect %s + ghidra trace start + ghidra trace tx-start "Create Object" + ghidra trace create-obj Test.Objects[1] + ghidra trace insert-obj Test.Objects[1] + ghidra trace set-snap 1 + ghidra trace remove-obj Test.Objects[1] + ghidra trace tx-commit + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/noname")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + TraceObject object = tb.trace.getObjectManager() + .getObjectByCanonicalPath(TraceObjectKeyPath.parse("Test.Objects[1]")); + assertNotNull(object); + Lifespan life = Unique.assertOne(object.getLife().spans()); + assertEquals(Lifespan.at(0), life); + } + } + + @SuppressWarnings("unchecked") + protected T runTestSetValue(String extra, String gdbExpr, String gtype) + throws Exception { + String expPrint = DummyProc.which("expPrint"); + runThrowError(addr -> """ + set python print-stack full + python import ghidragdb + ghidra trace connect %s + file %s + start + ghidra trace start + ghidra trace tx-start "Create Object" + ghidra trace create-obj Test.Objects[1] + ghidra trace insert-obj Test.Objects[1] + %s + ghidra trace set-value Test.Objects[1] test "%s" %s + ghidra trace tx-commit + kill + quit + """.formatted(addr, expPrint, extra, gdbExpr, gtype)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/expPrint")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + TraceObject object = tb.trace.getObjectManager() + .getObjectByCanonicalPath(TraceObjectKeyPath.parse("Test.Objects[1]")); + assertNotNull(object); + TraceObjectValue value = object.getValue(0, "test"); + return value == null ? null : (T) value.getValue(); + } + } + + @Test + public void testSetValueNull() throws Exception { + assertNull(runTestSetValue("", "(void)0", "")); + } + + @Test + public void testSetValueBool() throws Exception { + // C++ required for bool + assertEquals(Boolean.TRUE, runTestSetValue("set language c++", "(bool)1", "")); + } + + @Test + public void testSetValueByte() throws Exception { + assertEquals(Byte.valueOf((byte) 1), runTestSetValue("", "(char)1", "")); + } + + @Test + public void testSetValueChar() throws Exception { + assertEquals(Character.valueOf('A'), runTestSetValue("", "'A'", "CHAR")); + } + + @Test + public void testSetValueShort() throws Exception { + assertEquals(Short.valueOf((short) 1), runTestSetValue("", "(short)1", "")); + } + + @Test + public void testSetValueInt() throws Exception { + assertEquals(Integer.valueOf(1), runTestSetValue("", "(int)1", "")); + } + + @Test + public void testSetValueLong() throws Exception { + assertEquals(Long.valueOf(1), runTestSetValue("", "(long long)1", "")); + } + + @Test + public void testSetValueString() throws Exception { + assertEquals("Hello World!", runTestSetValue("", "\\\"Hello World!\\\"", "")); + } + + @Test + public void testSetValueStringWide() throws Exception { + // C++ required for wchar_t + assertEquals("Hello World!", + runTestSetValue("set language c++", "L\\\"Hello World!\\\"", "")); + } + + @Test + public void testSetValueBoolArr() throws Exception { + // C++ required for bool, true, false + assertArrayEquals(new boolean[] { true, false }, + runTestSetValue("set language c++", "{ true, false }", "")); + } + + @Test + public void testSetValueByteArrUsingString() throws Exception { + // Because explicit array type is chosen, we get null terminator + assertArrayEquals(new byte[] { 'H', 0, 'W', 0 }, + runTestSetValue("", "\\\"H\\\\0W\\\"", "BYTE_ARR")); + } + + @Test + public void testSetValueByteArrUsingArray() throws Exception { + assertArrayEquals(new byte[] { 'H', 0, 'W' }, + runTestSetValue("", "(char[3]){'H', 0, 'W'}", "BYTE_ARR")); + } + + @Test + public void testSetValueCharArrUsingString() throws Exception { + // Because explicit array type is chosen, we get null terminator + assertArrayEquals(new char[] { 'H', 0, 'W', 0 }, + runTestSetValue("", "\\\"H\\\\0W\\\"", "CHAR_ARR")); + } + + @Test + public void testSetValueCharArrUsingArray() throws Exception { + assertArrayEquals(new char[] { 'H', 0, 'W' }, + runTestSetValue("", "(char[3]){'H', 0, 'W'}", "CHAR_ARR")); + } + + @Test + public void testSetValueShortArrUsingString() throws Exception { + // Because explicit array type is chosen, we get null terminator + assertArrayEquals(new short[] { 'H', 0, 'W', 0 }, + runTestSetValue("set language c++", "L\\\"H\\\\0W\\\"", "SHORT_ARR")); + } + + @Test + public void testSetValueShortArrUsingArray() throws Exception { + assertArrayEquals(new short[] { 'H', 0, 'W' }, + runTestSetValue("", "(short[3]){'H', 0, 'W'}", "SHORT_ARR")); + } + + @Test + public void testSetValueIntArrayUsingString() throws Exception { + // Because explicit array type is chosen, we get null terminator + assertArrayEquals(new int[] { 'H', 0, 'W', 0 }, + runTestSetValue("set language c++", "L\\\"H\\\\0W\\\"", "INT_ARR")); + } + + @Test + public void testSetValueIntArrUsingArray() throws Exception { + assertArrayEquals(new int[] { 1, 2, 3, 4 }, + runTestSetValue("", "{1, 2, 3, 4}", "")); + } + + @Test + public void testSetValueLongArr() throws Exception { + assertArrayEquals(new long[] { 1, 2, 3, 4 }, + runTestSetValue("", "{1LL, 2LL, 3LL, 4LL}", "")); + } + + // Skip String[]. Trouble is expressing them in GDB.... + + @Test + public void testSetValueAddress() throws Exception { + Address address = runTestSetValue("", "(void*)0xdeadbeef", ""); + // Don't have the address factory to create expected address + assertEquals(0xdeadbeefL, address.getOffset()); + assertEquals("ram", address.getAddressSpace().getName()); + } + + @Test + public void testSetValueObject() throws Exception { + TraceObject object = runTestSetValue("", "Test.Objects[1]", "OBJECT"); + assertEquals("Test.Objects[1]", object.getCanonicalPath().toString()); + } + + @Test + public void testRetainValues() throws Exception { + runThrowError(addr -> """ + set python print-stack full + python import ghidragdb + ghidra trace connect %s + file bash + set language c++ + start + ghidra trace start + ghidra trace tx-start "Create Object" + ghidra trace create-obj Test.Objects[1] + ghidra trace insert-obj Test.Objects[1] + ghidra trace set-value Test.Objects[1] [1] '"A"' + ghidra trace set-value Test.Objects[1] [2] '"B"' + ghidra trace set-value Test.Objects[1] [3] '"C"' + ghidra trace set-snap 10 + ghidra trace retain-values Test.Objects[1] [1] [3] + ghidra trace tx-commit + kill + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + TraceObject object = tb.trace.getObjectManager() + .getObjectByCanonicalPath(TraceObjectKeyPath.parse("Test.Objects[1]")); + assertNotNull(object); + assertEquals(Map.ofEntries( + Map.entry("[1]", Lifespan.nowOn(0)), + Map.entry("[2]", Lifespan.span(0, 9)), + Map.entry("[3]", Lifespan.nowOn(0))), + object.getValues() + .stream() + .collect(Collectors.toMap(v -> v.getEntryKey(), v -> v.getLifespan()))); + } + } + + @Test + public void testGetObj() throws Exception { + String out = runThrowError(addr -> """ + set python print-stack full + python import ghidragdb + ghidra trace connect %s + ghidra trace start + ghidra trace tx-start "Create Object" + echo \\n---Id---\\n + ghidra trace create-obj Test.Objects[1] + echo \\n--- + ghidra trace tx-commit + echo \\n---GetObject---\\n + ghidra trace get-obj Test.Objects[1] + echo \\n--- + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/noname")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + TraceObject object = tb.trace.getObjectManager() + .getObjectByCanonicalPath(TraceObjectKeyPath.parse("Test.Objects[1]")); + assertNotNull(object); + String getObject = extractOutSection(out, "---GetObject---"); + assertEquals("1\tTest.Objects[1]", getObject); + } + } + + @Test + public void testGetValues() throws Exception { + String expPrint = DummyProc.which("expPrint"); + String out = runThrowError(addr -> """ + set python print-stack full + python import ghidragdb + ghidra trace connect %s + file %s + set language c++ + start + ghidra trace start + ghidra trace tx-start "Create Object" + ghidra trace create-obj Test.Objects[1] + ghidra trace insert-obj Test.Objects[1] + ghidra trace set-value Test.Objects[1] vnull (void)0 + ghidra trace set-value Test.Objects[1] vbool true + ghidra trace set-value Test.Objects[1] vbyte (char)1 + ghidra trace set-value Test.Objects[1] vchar "'A'" CHAR + ghidra trace set-value Test.Objects[1] vshort (short)2 + ghidra trace set-value Test.Objects[1] vint 3 + ghidra trace set-value Test.Objects[1] vlong 4LL + ghidra trace set-value Test.Objects[1] vstring '"Hello"' + ghidra trace set-value Test.Objects[1] vboolarr '{true, false}' + ghidra trace set-value Test.Objects[1] vbytearr '(char[3]){1, 2, 3}' BYTE_ARR + ghidra trace set-value Test.Objects[1] vchararr '"Hello"' CHAR_ARR + ghidra trace set-value Test.Objects[1] vshortarr '(short[3]){1, 2, 3}' + ghidra trace set-value Test.Objects[1] vintarr '{1, 2, 3}' + ghidra trace set-value Test.Objects[1] vlongarr '{1LL, 2LL, 3LL}' + ghidra trace set-value Test.Objects[1] vaddr (void*)0xdeadbeef + ghidra trace set-value Test.Objects[1] vobj Test.Objects[1] OBJECT + ghidra trace tx-commit + echo \\n---GetValues---\\n + ghidra trace get-values Test.Objects[1]. + echo \\n--- + kill + quit + """.formatted(addr, expPrint)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/expPrint")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + assertEquals(""" + Parent Key Span Value Type + Test.Objects[1] vbool [0,+inf) True BOOL + Test.Objects[1] vboolarr [0,+inf) [True, False] BOOL_ARR + Test.Objects[1] vbyte [0,+inf) 1 BYTE + Test.Objects[1] vbytearr [0,+inf) b'\\x01\\x02\\x03' BYTE_ARR + Test.Objects[1] vchar [0,+inf) 'A' CHAR + Test.Objects[1] vchararr [0,+inf) 'Hello\\x00' CHAR_ARR + Test.Objects[1] vint [0,+inf) 3 INT + Test.Objects[1] vintarr [0,+inf) [1, 2, 3] INT_ARR + Test.Objects[1] vlong [0,+inf) 4 LONG + Test.Objects[1] vlongarr [0,+inf) [1, 2, 3] LONG_ARR + Test.Objects[1] vobj [0,+inf) Test.Objects[1] OBJECT + Test.Objects[1] vshort [0,+inf) 2 SHORT + Test.Objects[1] vshortarr [0,+inf) [1, 2, 3] SHORT_ARR + Test.Objects[1] vstring [0,+inf) 'Hello' STRING + Test.Objects[1] vaddr [0,+inf) ram:deadbeef ADDRESS""", + extractOutSection(out, "---GetValues---")); + } + } + + @Test + public void testGetValuesRng() throws Exception { + String out = runThrowError(addr -> """ + set python print-stack full + python import ghidragdb + ghidra trace connect %s + file bash + set language c++ + start + ghidra trace start + ghidra trace tx-start "Create Object" + ghidra trace create-obj Test.Objects[1] + ghidra trace insert-obj Test.Objects[1] + ghidra trace set-value Test.Objects[1] vaddr (void*)0xdeadbeef + ghidra trace tx-commit + echo \\n---GetValues---\\n + ghidra trace get-values-rng (void*)0xdeadbeef 10 + echo \\n--- + kill + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + assertEquals(""" + Parent Key Span Value Type + Test.Objects[1] vaddr [0,+inf) ram:deadbeef ADDRESS""", + extractOutSection(out, "---GetValues---")); + } + } + + @Test + public void testActivateObject() throws Exception { + runThrowError(addr -> """ + set python print-stack full + python import ghidragdb + ghidra trace connect %s + file bash + set language c++ + start + ghidra trace start + ghidra trace tx-start "Create Object" + ghidra trace create-obj Test.Objects[1] + ghidra trace insert-obj Test.Objects[1] + ghidra trace tx-commit + ghidra trace activate Test.Objects[1] + kill + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + assertSame(mdo.get(), traceManager.getCurrentTrace()); + assertEquals("Test.Objects[1]", + traceManager.getCurrentObject().getCanonicalPath().toString()); + } + } + + @Test + public void testDisassemble() throws Exception { + String out = runThrowError(addr -> """ + set python print-stack full + python import ghidragdb + ghidra trace connect %s + file bash + set language c++ + start + ghidra trace start + ghidra trace tx-start "Tx" + ghidra trace putmem &main 10 + echo \\n---Disassemble---\\n + ghidra trace disassemble &main + echo \\n--- + ghidra trace tx-commit + kill + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + // Not concerned about specifics, so long as disassembly occurs + long total = 0; + for (CodeUnit cu : tb.trace.getCodeManager().definedUnits().get(0, true)) { + total += cu.getLength(); + } + assertEquals("Disassembled %d bytes".formatted(total), + extractOutSection(out, "---Disassemble---")); + } + } + + @Test + public void testPutInferiors() throws Exception { + runThrowError(addr -> """ + set python print-stack full + python import ghidragdb + ghidra trace connect %s + add-inferior + ghidra trace start + ghidra trace tx-start "Tx" + ghidra trace put-inferiors + ghidra trace tx-commit + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/noname")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + // Would be nice to control / validate the specifics + Collection inferiors = tb.trace.getObjectManager() + .getValuePaths(Lifespan.at(0), PathPredicates.parse("Inferiors[]")) + .map(p -> p.getDestination(null)) + .toList(); + assertEquals(2, inferiors.size()); + } + } + + @Test + public void testPutAvailable() throws Exception { + runThrowError(addr -> """ + set python print-stack full + python import ghidragdb + ghidra trace connect %s + add-inferior + ghidra trace start + ghidra trace tx-start "Tx" + ghidra trace put-available + ghidra trace tx-commit + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/noname")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + // Would be nice to control / validate the specifics + Collection available = tb.trace.getObjectManager() + .getValuePaths(Lifespan.at(0), PathPredicates.parse("Available[]")) + .map(p -> p.getDestination(null)) + .toList(); + assertThat(available.size(), greaterThan(2)); + } + } + + @Test + public void testPutBreakpoints() throws Exception { + runThrowError(addr -> """ + set python print-stack full + python import ghidragdb + ghidra trace connect %s + file bash + start + ghidra trace start + ghidra trace tx-start "Tx" + break main + hbreak *main+10 + watch -l *((char*)(&main+20)) + rwatch -l *((char(*)[8])(&main+30)) + awatch -l *((char(*)[5])(&main+40)) + ghidra trace put-breakpoints + ghidra trace tx-commit + kill + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + List infBreakLocVals = tb.trace.getObjectManager() + .getValuePaths(Lifespan.at(0), + PathPredicates.parse("Inferiors[1].Breakpoints[]")) + .map(p -> p.getLastEntry()) + .toList(); + assertEquals(5, infBreakLocVals.size()); + AddressRange rangeMain = + infBreakLocVals.get(0).getChild().getValue(0, "_range").castValue(); + Address main = rangeMain.getMinAddress(); + + // The temporary breakpoint uses up number 1 + assertBreakLoc(infBreakLocVals.get(0), "[2.1]", main, 1, + Set.of(TraceBreakpointKind.SW_EXECUTE), + "main"); + assertBreakLoc(infBreakLocVals.get(1), "[3.1]", main.add(10), 1, + Set.of(TraceBreakpointKind.HW_EXECUTE), + "*main+10"); + assertBreakLoc(infBreakLocVals.get(2), "[4.1]", main.add(20), 1, + Set.of(TraceBreakpointKind.WRITE), + "-location *((char*)(&main+20))"); + assertBreakLoc(infBreakLocVals.get(3), "[5.1]", main.add(30), 8, + Set.of(TraceBreakpointKind.READ), + "-location *((char(*)[8])(&main+30))"); + assertBreakLoc(infBreakLocVals.get(4), "[6.1]", main.add(40), 5, + Set.of(TraceBreakpointKind.READ, TraceBreakpointKind.WRITE), + "-location *((char(*)[5])(&main+40))"); + } + } + + @Test + public void testPutEnvironment() throws Exception { + runThrowError(addr -> """ + set python print-stack full + python import ghidragdb + ghidra trace connect %s + file bash + start + ghidra trace start + ghidra trace tx-start "Tx" + ghidra trace put-environment + ghidra trace tx-commit + kill + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + // Assumes GDB on Linux amd64 + TraceObject env = Objects.requireNonNull(tb.obj("Inferiors[1].Environment")); + assertEquals("gdb", env.getValue(0, "_debugger").getValue()); + assertEquals("i386:x86-64", env.getValue(0, "_arch").getValue()); + assertEquals("GNU/Linux", env.getValue(0, "_os").getValue()); + assertEquals("little", env.getValue(0, "_endian").getValue()); + } + } + + @Test + public void testPutRegions() throws Exception { + runThrowError(addr -> """ + set python print-stack full + python import ghidragdb + ghidra trace connect %s + file bash + start + ghidra trace start + ghidra trace tx-start "Tx" + ghidra trace put-regions + ghidra trace tx-commit + kill + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + // Would be nice to control / validate the specifics + Collection all = + tb.trace.getMemoryManager().getAllRegions(); + assertThat(all.size(), greaterThan(2)); + } + } + + @Test + public void testPutModules() throws Exception { + runThrowError(addr -> """ + set python print-stack full + python import ghidragdb + ghidra trace connect %s + file bash + start + ghidra trace start + ghidra trace tx-start "Tx" + ghidra trace put-modules + ghidra trace tx-commit + kill + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + // Would be nice to control / validate the specifics + Collection all = tb.trace.getModuleManager().getAllModules(); + TraceModule modBash = + Unique.assertOne(all.stream().filter(m -> m.getName().contains("bash"))); + assertNotEquals(tb.addr(0), Objects.requireNonNull(modBash.getBase())); + } + } + + @Test + public void testPutThreads() throws Exception { + runThrowError(addr -> """ + set python print-stack full + python import ghidragdb + ghidra trace connect %s + file bash + start + ghidra trace start + ghidra trace tx-start "Tx" + ghidra trace put-threads + ghidra trace tx-commit + kill + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + // Would be nice to control / validate the specifics + Unique.assertOne(tb.trace.getThreadManager().getAllThreads()); + } + } + + @Test + public void testPutFrames() throws Exception { + runThrowError(addr -> """ + set python print-stack full + python import ghidragdb + ghidra trace connect %s + file bash + start + break read + continue + ghidra trace start + ghidra trace tx-start "Tx" + ghidra trace put-frames + ghidra trace tx-commit + kill + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + // Would be nice to control / validate the specifics + List stack = tb.trace.getObjectManager() + .getValuePaths(Lifespan.at(0), + PathPredicates.parse("Inferiors[1].Threads[1].Stack[]")) + .map(p -> p.getDestination(null)) + .toList(); + assertThat(stack.size(), greaterThan(2)); + } + } +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/agent/gdb/rmi/GdbHooksTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/agent/gdb/rmi/GdbHooksTest.java new file mode 100644 index 0000000000..0ef3cfa4e5 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/agent/gdb/rmi/GdbHooksTest.java @@ -0,0 +1,425 @@ +/* ### + * 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 agent.gdb.rmi; + +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.Assert.*; + +import java.nio.ByteBuffer; +import java.util.List; + +import org.junit.Ignore; +import org.junit.Test; + +import agent.gdb.model.GdbLinuxSpecimen; +import ghidra.app.plugin.core.debug.utils.ManagedDomainObject; +import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; +import ghidra.dbg.testutil.DummyProc; +import ghidra.dbg.util.PathPattern; +import ghidra.dbg.util.PathPredicates; +import ghidra.program.model.address.AddressSpace; +import ghidra.trace.database.ToyDBTraceBuilder; +import ghidra.trace.model.Trace; +import ghidra.trace.model.memory.TraceMemorySpace; +import ghidra.trace.model.target.TraceObject; +import ghidra.trace.model.time.TraceSnapshot; + +public class GdbHooksTest extends AbstractGdbTraceRmiTest { + private static final long RUN_TIMEOUT_MS = 20000; + private static final long RETRY_MS = 500; + + record GdbAndTrace(GdbAndHandler conn, ManagedDomainObject mdo) implements AutoCloseable { + public void execute(String cmd) { + conn.execute(cmd); + } + + public String executeCapture(String cmd) { + return conn.executeCapture(cmd); + } + + @Override + public void close() throws Exception { + conn.close(); + mdo.close(); + } + } + + @SuppressWarnings("resource") + protected GdbAndTrace startAndSyncGdb() throws Exception { + GdbAndHandler conn = startAndConnectGdb(); + try { + // TODO: Why does using 'set arch' cause a hang at quit? + conn.execute(""" + set ghidra-language x86:LE:64:default + ghidra trace start + ghidra trace sync-enable"""); + ManagedDomainObject mdo = waitDomainObject("/New Traces/gdb/noname"); + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + return new GdbAndTrace(conn, mdo); + } + catch (Exception e) { + conn.close(); + throw e; + } + } + + @Test + public void testOnNewInferior() throws Exception { + try (GdbAndTrace conn = startAndSyncGdb()) { + conn.execute("add-inferior"); + waitForPass(() -> assertEquals(2, tb.objValues(0, "Inferiors[]").size())); + } + } + + protected String getIndex(TraceObject object, String pattern) { + if (object == null) { + return null; + } + PathPattern pat = PathPredicates.parse(pattern).getSingletonPattern(); + if (pat.countWildcards() != 1) { + throw new IllegalArgumentException("Exactly one wildcard required"); + } + List path = object.getCanonicalPath().getKeyList(); + if (path.size() < pat.asPath().size()) { + return null; + } + List matched = pat.matchKeys(path.subList(0, pat.asPath().size())); + if (matched == null) { + return null; + } + return matched.get(0); + } + + protected String inferiorIndex(TraceObject object) { + return getIndex(object, "Inferiors[]"); + } + + @Test + public void testOnInferiorSelected() throws Exception { + try (GdbAndTrace conn = startAndSyncGdb()) { + traceManager.openTrace(tb.trace); + // Both inferiors must have sync enabled + conn.execute(""" + add-inferior + inferior 2 + ghidra trace sync-enable"""); + + conn.execute("inferior 1"); + waitForPass(() -> assertEquals("1", inferiorIndex(traceManager.getCurrentObject()))); + + conn.execute("inferior 2"); + waitForPass(() -> assertEquals("2", inferiorIndex(traceManager.getCurrentObject()))); + + conn.execute("inferior 1"); + waitForPass(() -> assertEquals("1", inferiorIndex(traceManager.getCurrentObject()))); + } + } + + @Test + public void testOnInferiorDeleted() throws Exception { + try (GdbAndTrace conn = startAndSyncGdb()) { + conn.execute("add-inferior"); + waitForPass(() -> assertEquals(2, tb.objValues(0, "Inferiors[]").size())); + + conn.execute("remove-inferior 2"); + waitForPass(() -> assertEquals(1, tb.objValues(0, "Inferiors[]").size())); + } + } + + protected long lastSnap(GdbAndTrace conn) { + return conn.conn.handler().getLastSnapshot(tb.trace); + } + + @Test + public void testOnNewThread() throws Exception { + try (GdbAndTrace conn = startAndSyncGdb()) { + conn.execute(""" + file %s + break work + start""".formatted(GdbLinuxSpecimen.CLONE_EXIT.getCommandLine())); + waitForPass(() -> { + TraceObject inf = tb.obj("Inferiors[1]"); + assertNotNull(inf); + assertEquals("STOPPED", tb.objValue(inf, lastSnap(conn), "_state")); + }, RUN_TIMEOUT_MS, RETRY_MS); + waitForPass(() -> assertEquals(1, + tb.objValues(lastSnap(conn), "Inferiors[1].Threads[]").size()), + RUN_TIMEOUT_MS, RETRY_MS); + + conn.execute("continue"); + waitForPass(() -> assertEquals(2, + tb.objValues(lastSnap(conn), "Inferiors[1].Threads[]").size()), + RUN_TIMEOUT_MS, RETRY_MS); + } + } + + protected String threadIndex(TraceObject object) { + return getIndex(object, "Inferiors[1].Threads[]"); + } + + @Test + public void testOnThreadSelected() throws Exception { + String cloneExit = DummyProc.which("expCloneExit"); + try (GdbAndTrace conn = startAndSyncGdb()) { + traceManager.openTrace(tb.trace); + + conn.execute(""" + file %s + break work + run""".formatted(cloneExit)); + waitForPass(() -> { + TraceObject inf = tb.obj("Inferiors[1]"); + assertNotNull(inf); + assertEquals("STOPPED", tb.objValue(inf, lastSnap(conn), "_state")); + }, RUN_TIMEOUT_MS, RETRY_MS); + waitForPass(() -> assertEquals(2, + tb.objValues(lastSnap(conn), "Inferiors[1].Threads[]").size()), + RUN_TIMEOUT_MS, RETRY_MS); + + // Now the real test + conn.execute("thread 1"); + waitForPass(() -> assertEquals("1", threadIndex(traceManager.getCurrentObject()))); + + conn.execute("thread 2"); + waitForPass(() -> assertEquals("2", threadIndex(traceManager.getCurrentObject()))); + + conn.execute("thread 1"); + waitForPass(() -> assertEquals("1", threadIndex(traceManager.getCurrentObject()))); + } + } + + protected String frameIndex(TraceObject object) { + return getIndex(object, "Inferiors[1].Threads[1].Stack[]"); + } + + @Test + public void testOnFrameSelected() throws Exception { + String stack = DummyProc.which("expStack"); + try (GdbAndTrace conn = startAndSyncGdb()) { + traceManager.openTrace(tb.trace); + + conn.execute(""" + file %s + break break_here + run""".formatted(stack)); + waitForPass(() -> assertThat( + tb.objValues(lastSnap(conn), "Inferiors[1].Threads[1].Stack[]").size(), + greaterThan(2)), + RUN_TIMEOUT_MS, RETRY_MS); + + conn.execute("frame 1"); + waitForPass(() -> assertEquals("1", frameIndex(traceManager.getCurrentObject())), + RUN_TIMEOUT_MS, RETRY_MS); + + conn.execute("frame 0"); + waitForPass(() -> assertEquals("0", frameIndex(traceManager.getCurrentObject())), + RUN_TIMEOUT_MS, RETRY_MS); + } + } + + @Test + @Ignore + public void testOnSyscallMemory() throws Exception { + // TODO: Need a specimen + // FWIW, I've already seen this getting exercised in other tests. + } + + @Test + public void testOnMemoryChanged() throws Exception { + try (GdbAndTrace conn = startAndSyncGdb()) { + conn.execute(""" + file bash + start"""); + + long address = Long.decode(conn.executeCapture("print/x &main").split("\\s+")[2]); + conn.execute("set *((char*) &main) = 0x7f"); + waitForPass(() -> { + ByteBuffer buf = ByteBuffer.allocate(1); + tb.trace.getMemoryManager().getBytes(lastSnap(conn), tb.addr(address), buf); + assertEquals(0x7f, buf.get(0)); + }, RUN_TIMEOUT_MS, RETRY_MS); + } + } + + @Test + public void testOnRegisterChanged() throws Exception { + try (GdbAndTrace conn = startAndSyncGdb()) { + conn.execute(""" + file bash + start"""); + + TraceObject thread = waitForValue(() -> tb.obj("Inferiors[1].Threads[1]")); + waitForPass( + () -> assertEquals("STOPPED", tb.objValue(thread, lastSnap(conn), "_state")), + RUN_TIMEOUT_MS, RETRY_MS); + + conn.execute("set $rax = 0x1234"); + AddressSpace space = tb.trace.getBaseAddressFactory() + .getAddressSpace("Inferiors[1].Threads[1].Stack[0].Registers"); + TraceMemorySpace regs = tb.trace.getMemoryManager().getMemorySpace(space, false); + waitForPass(() -> assertEquals("1234", + regs.getValue(lastSnap(conn), tb.reg("RAX")).getUnsignedValue().toString(16))); + } + } + + @Test + public void testOnCont() throws Exception { + try (GdbAndTrace conn = startAndSyncGdb()) { + conn.execute(""" + file bash + run"""); + + TraceObject inf = waitForValue(() -> tb.obj("Inferiors[1]")); + TraceObject thread = waitForValue(() -> tb.obj("Inferiors[1].Threads[1]")); + waitForPass(() -> { + assertEquals("RUNNING", tb.objValue(inf, lastSnap(conn), "_state")); + assertEquals("RUNNING", tb.objValue(thread, lastSnap(conn), "_state")); + }, RUN_TIMEOUT_MS, RETRY_MS); + } + } + + @Test + public void testOnStop() throws Exception { + try (GdbAndTrace conn = startAndSyncGdb()) { + conn.execute(""" + file bash + start"""); + + TraceObject inf = waitForValue(() -> tb.obj("Inferiors[1]")); + TraceObject thread = waitForValue(() -> tb.obj("Inferiors[1].Threads[1]")); + waitForPass(() -> { + assertEquals("STOPPED", tb.objValue(inf, lastSnap(conn), "_state")); + assertEquals("STOPPED", tb.objValue(thread, lastSnap(conn), "_state")); + }, RUN_TIMEOUT_MS, RETRY_MS); + } + } + + @Test + public void testOnExited() throws Exception { + try (GdbAndTrace conn = startAndSyncGdb()) { + conn.execute(""" + file bash + set args -c "exit 1" + run"""); + + waitForPass(() -> { + TraceSnapshot snapshot = + tb.trace.getTimeManager().getSnapshot(lastSnap(conn), false); + assertNotNull(snapshot); + assertEquals("Exited with code 1", snapshot.getDescription()); + + TraceObject inf1 = tb.obj("Inferiors[1]"); + assertNotNull(inf1); + Object val = tb.objValue(inf1, lastSnap(conn), "_exit_code"); + assertThat(val, instanceOf(Number.class)); + assertEquals(1, ((Number) val).longValue()); + }, RUN_TIMEOUT_MS, RETRY_MS); + } + } + + /** + * Test on_clear_objfiles, on_new_objfile, on_free_objfile. + * + *

+ * Technically, this probably doesn't hit on_free_objfile, but all three just call + * modules_changed, so I'm not concerned. + */ + @Test + public void testOnEventsObjfiles() throws Exception { + String print = DummyProc.which("expPrint"); + String modPrint = "Inferiors[1].Modules[%s]".formatted(print); + try (GdbAndTrace conn = startAndSyncGdb()) { + conn.execute(""" + file %s + start""".formatted(print)); + waitForPass(() -> assertEquals(1, tb.objValues(lastSnap(conn), modPrint).size()), + RUN_TIMEOUT_MS, RETRY_MS); + + conn.execute("continue"); + waitState(1, () -> lastSnap(conn), TargetExecutionState.TERMINATED); + /** + * Termination does not clear objfiles. Not until we run a new target. + */ + conn.execute(""" + file bash + set args -c "exit 1" + run"""); + waitForPass(() -> assertEquals(0, tb.objValues(lastSnap(conn), modPrint).size()), + RUN_TIMEOUT_MS, RETRY_MS); + } + } + + @Test + public void testOnBreakpointCreated() throws Exception { + String print = DummyProc.which("expPrint"); + try (GdbAndTrace conn = startAndSyncGdb()) { + conn.execute("file " + print); + assertEquals(0, tb.objValues(lastSnap(conn), "Breakpoints[]").size()); + + conn.execute("break main"); + waitForPass(() -> { + List brks = tb.objValues(lastSnap(conn), "Breakpoints[]"); + assertEquals(1, brks.size()); + return (TraceObject) brks.get(0); + }); + } + } + + @Test + public void testOnBreakpointModified() throws Exception { + String print = DummyProc.which("expPrint"); + try (GdbAndTrace conn = startAndSyncGdb()) { + conn.execute("file " + print); + assertEquals(0, tb.objValues(lastSnap(conn), "Breakpoints[]").size()); + + conn.execute("break main"); + TraceObject brk = waitForPass(() -> { + List brks = tb.objValues(lastSnap(conn), "Breakpoints[]"); + assertEquals(1, brks.size()); + return (TraceObject) brks.get(0); + }); + assertEquals(null, tb.objValue(brk, lastSnap(conn), "Commands")); + + conn.execute(""" + commands %s + echo test + end""".formatted(brk.getCanonicalPath().index())); + waitForPass( + () -> assertEquals("echo test\n", tb.objValue(brk, lastSnap(conn), "Commands"))); + } + } + + @Test + public void testOnBreakpointDeleted() throws Exception { + String print = DummyProc.which("expPrint"); + try (GdbAndTrace conn = startAndSyncGdb()) { + conn.execute("file " + print); + assertEquals(0, tb.objValues(lastSnap(conn), "Breakpoints[]").size()); + + conn.execute("break main"); + TraceObject brk = waitForPass(() -> { + List brks = tb.objValues(lastSnap(conn), "Breakpoints[]"); + assertEquals(1, brks.size()); + return (TraceObject) brks.get(0); + }); + + conn.execute("delete %s".formatted(brk.getCanonicalPath().index())); + waitForPass( + () -> assertEquals(0, tb.objValues(lastSnap(conn), "Breakpoints[]").size())); + } + } +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/agent/gdb/rmi/GdbMethodsTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/agent/gdb/rmi/GdbMethodsTest.java new file mode 100644 index 0000000000..aca5980085 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/agent/gdb/rmi/GdbMethodsTest.java @@ -0,0 +1,1254 @@ +/* ### + * 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 agent.gdb.rmi; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.junit.Test; + +import db.Transaction; +import generic.Unique; +import ghidra.app.plugin.core.debug.service.rmi.trace.RemoteMethod; +import ghidra.app.plugin.core.debug.service.rmi.trace.ValueDecoder; +import ghidra.app.plugin.core.debug.utils.ManagedDomainObject; +import ghidra.dbg.testutil.DummyProc; +import ghidra.dbg.util.PathPattern; +import ghidra.dbg.util.PathPredicates; +import ghidra.program.model.address.*; +import ghidra.program.model.data.Float10DataType; +import ghidra.program.model.lang.Register; +import ghidra.program.model.lang.RegisterValue; +import ghidra.trace.database.ToyDBTraceBuilder; +import ghidra.trace.model.Lifespan; +import ghidra.trace.model.Trace; +import ghidra.trace.model.breakpoint.TraceBreakpointKind; +import ghidra.trace.model.listing.TraceCodeSpace; +import ghidra.trace.model.listing.TraceData; +import ghidra.trace.model.memory.TraceMemoryRegion; +import ghidra.trace.model.memory.TraceMemorySpace; +import ghidra.trace.model.modules.TraceModule; +import ghidra.trace.model.target.TraceObject; +import ghidra.trace.model.target.TraceObjectValue; + +public class GdbMethodsTest extends AbstractGdbTraceRmiTest { + + @Test + public void testExecuteCapture() throws Exception { + try (GdbAndHandler conn = startAndConnectGdb()) { + RemoteMethod execute = conn.getMethod("execute"); + assertEquals(false, + execute.parameters().get("to_string").defaultValue().get(ValueDecoder.DEFAULT)); + assertEquals("test", execute.invoke(Map.of("cmd", "echo test", "to_string", true))); + } + } + + @Test + public void testExecute() throws Exception { + try (GdbAndHandler conn = startAndConnectGdb()) { + RemoteMethod execute = conn.getMethod("execute"); + execute.invoke(Map.of("cmd", """ + file bash + start + ghidra trace start + kill""")); + } + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + // Just confirm it's present + } + } + + @Test + public void testRefreshAvailable() throws Exception { + try (GdbAndHandler conn = startAndConnectGdb()) { + conn.execute(""" + ghidra trace start + ghidra trace tx-open "Fake" 'ghidra trace create-obj Available'"""); + RemoteMethod refreshAvailable = conn.getMethod("refresh_available"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/noname")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + TraceObject available = Objects.requireNonNull(tb.obj("Available")); + + refreshAvailable.invoke(Map.of("node", available)); + + // Would be nice to control / validate the specifics + List list = tb.trace.getObjectManager() + .getValuePaths(Lifespan.at(0), PathPredicates.parse("Available[]")) + .map(p -> p.getDestination(null)) + .toList(); + assertThat(list.size(), greaterThan(2)); + } + } + } + + @Test + public void testRefreshBreakpoints() throws Exception { + try (GdbAndHandler conn = startAndConnectGdb()) { + conn.execute(""" + file bash + ghidra trace start + %s + ghidra trace tx-open "Fake" 'ghidra trace create-obj Breakpoints' + start""" + .formatted(INSTRUMENT_STOPPED)); + RemoteMethod refreshBreakpoints = conn.getMethod("refresh_breakpoints"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + + conn.execute(""" + break main + hbreak *main+10 + watch -l *((char*)(&main+20)) + rwatch -l *((char(*)[8])(&main+30)) + awatch -l *((char(*)[5])(&main+40))"""); + TraceObject breakpoints = Objects.requireNonNull(tb.obj("Breakpoints")); + refreshBreakpoints.invoke(Map.of("node", breakpoints)); + + List infBreakLocVals = tb.trace.getObjectManager() + .getValuePaths(Lifespan.at(0), + PathPredicates.parse("Inferiors[1].Breakpoints[]")) + .map(p -> p.getLastEntry()) + .toList(); + assertEquals(5, infBreakLocVals.size()); + AddressRange rangeMain = + infBreakLocVals.get(0).getChild().getValue(0, "_range").castValue(); + Address main = rangeMain.getMinAddress(); + + // The temporary breakpoint uses up number 1 + assertBreakLoc(infBreakLocVals.get(0), "[2.1]", main, 1, + Set.of(TraceBreakpointKind.SW_EXECUTE), + "main"); + assertBreakLoc(infBreakLocVals.get(1), "[3.1]", main.add(10), 1, + Set.of(TraceBreakpointKind.HW_EXECUTE), + "*main+10"); + assertBreakLoc(infBreakLocVals.get(2), "[4.1]", main.add(20), 1, + Set.of(TraceBreakpointKind.WRITE), + "-location *((char*)(&main+20))"); + assertBreakLoc(infBreakLocVals.get(3), "[5.1]", main.add(30), 8, + Set.of(TraceBreakpointKind.READ), + "-location *((char(*)[8])(&main+30))"); + assertBreakLoc(infBreakLocVals.get(4), "[6.1]", main.add(40), 5, + Set.of(TraceBreakpointKind.READ, TraceBreakpointKind.WRITE), + "-location *((char(*)[5])(&main+40))"); + } + } + } + + @Test + public void testRefreshInfBreakpoints() throws Exception { + try (GdbAndHandler conn = startAndConnectGdb()) { + conn.execute(""" + file bash + ghidra trace start + %s + ghidra trace tx-open "Fake" 'ghidra trace create-obj Inferiors[1].Breakpoints' + start""" + .formatted(INSTRUMENT_STOPPED)); + RemoteMethod refreshInfBreakpoints = conn.getMethod("refresh_inf_breakpoints"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + + TraceObject locations = Objects.requireNonNull(tb.obj("Inferiors[1].Breakpoints")); + conn.execute(""" + break main + hbreak *main+10 + watch -l *((char*)(&main+20)) + rwatch -l *((char(*)[8])(&main+30)) + awatch -l *((char(*)[5])(&main+40))"""); + refreshInfBreakpoints.invoke(Map.of("node", locations)); + + List infBreakLocVals = tb.trace.getObjectManager() + .getValuePaths(Lifespan.at(0), + PathPredicates.parse("Inferiors[1].Breakpoints[]")) + .map(p -> p.getLastEntry()) + .toList(); + assertEquals(5, infBreakLocVals.size()); + AddressRange rangeMain = + infBreakLocVals.get(0).getChild().getValue(0, "_range").castValue(); + Address main = rangeMain.getMinAddress(); + + // The temporary breakpoint uses up number 1 + assertBreakLoc(infBreakLocVals.get(0), "[2.1]", main, 1, + Set.of(TraceBreakpointKind.SW_EXECUTE), + "main"); + assertBreakLoc(infBreakLocVals.get(1), "[3.1]", main.add(10), 1, + Set.of(TraceBreakpointKind.HW_EXECUTE), + "*main+10"); + assertBreakLoc(infBreakLocVals.get(2), "[4.1]", main.add(20), 1, + Set.of(TraceBreakpointKind.WRITE), + "-location *((char*)(&main+20))"); + assertBreakLoc(infBreakLocVals.get(3), "[5.1]", main.add(30), 8, + Set.of(TraceBreakpointKind.READ), + "-location *((char(*)[8])(&main+30))"); + assertBreakLoc(infBreakLocVals.get(4), "[6.1]", main.add(40), 5, + Set.of(TraceBreakpointKind.READ, TraceBreakpointKind.WRITE), + "-location *((char(*)[5])(&main+40))"); + } + } + } + + @Test + public void testRefreshInferiors() throws Exception { + try (GdbAndHandler conn = startAndConnectGdb()) { + conn.execute(""" + add-inferior + ghidra trace start + ghidra trace tx-open "Fake" 'ghidra trace create-obj Inferiors'"""); + RemoteMethod refreshInferiors = conn.getMethod("refresh_inferiors"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/noname")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + TraceObject inferiors = Objects.requireNonNull(tb.obj("Inferiors")); + + refreshInferiors.invoke(Map.of("node", inferiors)); + + // Would be nice to control / validate the specifics + List list = tb.trace.getObjectManager() + .getValuePaths(Lifespan.at(0), PathPredicates.parse("Inferiors[]")) + .map(p -> p.getDestination(null)) + .toList(); + assertEquals(2, list.size()); + } + } + } + + @Test + public void testRefreshEnvironment() throws Exception { + try (GdbAndHandler conn = startAndConnectGdb()) { + String path = "Inferiors[1].Environment"; + conn.execute(""" + file bash + start + ghidra trace start + ghidra trace tx-open "Fake" 'ghidra trace create-obj %s'""".formatted(path)); + RemoteMethod refreshEnvironment = conn.getMethod("refresh_environment"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + TraceObject env = Objects.requireNonNull(tb.obj(path)); + + refreshEnvironment.invoke(Map.of("node", env)); + + // Assumes GDB on Linux amd64 + assertEquals("gdb", env.getValue(0, "_debugger").getValue()); + assertEquals("i386:x86-64", env.getValue(0, "_arch").getValue()); + assertEquals("GNU/Linux", env.getValue(0, "_os").getValue()); + assertEquals("little", env.getValue(0, "_endian").getValue()); + } + } + } + + @Test + public void testRefreshThreads() throws Exception { + try (GdbAndHandler conn = startAndConnectGdb()) { + String path = "Inferiors[1].Threads"; + conn.execute(""" + file bash + start + ghidra trace start + ghidra trace tx-open "Fake" 'ghidra trace create-obj %s'""".formatted(path)); + RemoteMethod refreshThreads = conn.getMethod("refresh_threads"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + TraceObject threads = Objects.requireNonNull(tb.obj(path)); + + refreshThreads.invoke(Map.of("node", threads)); + + // Would be nice to control / validate the specifics + Unique.assertOne(tb.trace.getThreadManager().getAllThreads()); + } + } + } + + @Test + public void testRefreshStack() throws Exception { + try (GdbAndHandler conn = startAndConnectGdb()) { + String path = "Inferiors[1].Threads[1].Stack"; + conn.execute(""" + file bash + ghidra trace start + %s + ghidra trace tx-open "Fake" 'ghidra trace create-obj %s' + break read + run""" + .formatted(INSTRUMENT_STOPPED, path)); + RemoteMethod refreshStack = conn.getMethod("refresh_stack"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + + TraceObject stack = Objects.requireNonNull(tb.obj(path)); + refreshStack.invoke(Map.of("node", stack)); + + // Would be nice to control / validate the specifics + List list = tb.trace.getObjectManager() + .getValuePaths(Lifespan.at(0), + PathPredicates.parse("Inferiors[1].Threads[1].Stack[]")) + .map(p -> p.getDestination(null)) + .toList(); + assertThat(list.size(), greaterThan(2)); + } + } + } + + @Test + public void testRefreshRegisters() throws Exception { + String count = IntStream.iterate(0, i -> i < 32, i -> i + 1) + .mapToObj(Integer::toString) + .collect(Collectors.joining(",", "{", "}")); + try (GdbAndHandler conn = startAndConnectGdb()) { + String path = "Inferiors[1].Threads[1].Stack[0].Registers"; + conn.execute(""" + file bash + ghidra trace start + %s + ghidra trace tx-open "Fake" 'ghidra trace create-obj %s' + start""" + .formatted(INSTRUMENT_STOPPED, path)); + RemoteMethod refreshRegisters = conn.getMethod("refresh_registers"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + + conn.execute(""" + set $ymm0.v32_int8 = %s + set $st0 = 1.5 + """.formatted(count)); + + TraceObject registers = Objects.requireNonNull(tb.obj(path)); + refreshRegisters.invoke(Map.of("node", registers)); + + long snap = 0; + AddressSpace t1f0 = tb.trace.getBaseAddressFactory() + .getAddressSpace("Inferiors[1].Threads[1].Stack[0].Registers"); + TraceMemorySpace regs = tb.trace.getMemoryManager().getMemorySpace(t1f0, false); + RegisterValue ymm0 = regs.getValue(snap, tb.reg("ymm0")); + // GDB treats registers in arch's endian + assertEquals("1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100", + ymm0.getUnsignedValue().toString(16)); + + TraceData st0; + try (Transaction tx = tb.trace.openTransaction("Float80 unit")) { + TraceCodeSpace code = tb.trace.getCodeManager().getCodeSpace(t1f0, true); + st0 = code.definedData() + .create(Lifespan.nowOn(0), tb.reg("st0"), Float10DataType.dataType); + } + + assertEquals("1.5", st0.getDefaultValueRepresentation()); + } + } + } + + @Test + public void testRefreshMappings() throws Exception { + try (GdbAndHandler conn = startAndConnectGdb()) { + String path = "Inferiors[1].Memory"; + conn.execute(""" + file bash + start + ghidra trace start + ghidra trace tx-open "Fake" 'ghidra trace create-obj %s'""".formatted(path)); + RemoteMethod refreshMappings = conn.getMethod("refresh_mappings"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + TraceObject memory = Objects.requireNonNull(tb.obj(path)); + + refreshMappings.invoke(Map.of("node", memory)); + + // Would be nice to control / validate the specifics + Collection all = + tb.trace.getMemoryManager().getAllRegions(); + assertThat(all.size(), greaterThan(2)); + } + } + } + + @Test + public void testRefreshModules() throws Exception { + try (GdbAndHandler conn = startAndConnectGdb()) { + String path = "Inferiors[1].Modules"; + conn.execute(""" + file bash + start + ghidra trace start + ghidra trace tx-open "Fake" 'ghidra trace create-obj %s'""".formatted(path)); + RemoteMethod refreshModules = conn.getMethod("refresh_modules"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + TraceObject modules = Objects.requireNonNull(tb.obj(path)); + + refreshModules.invoke(Map.of("node", modules)); + + // Would be nice to control / validate the specifics + Collection all = tb.trace.getModuleManager().getAllModules(); + TraceModule modBash = + Unique.assertOne(all.stream().filter(m -> m.getName().contains("bash"))); + assertNotEquals(tb.addr(0), Objects.requireNonNull(modBash.getBase())); + } + } + } + + @Test + public void testActivateInferior() throws Exception { + try (GdbAndHandler conn = startAndConnectGdb()) { + conn.execute(""" + add-inferior + ghidra trace start + ghidra trace tx-open Init 'ghidra trace put-inferiors'"""); + RemoteMethod activateInferior = conn.getMethod("activate_inferior"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/noname")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + + List list = tb.trace.getObjectManager() + .getValuePaths(Lifespan.at(0), PathPredicates.parse("Inferiors[]")) + .map(p -> p.getDestination(null)) + .toList(); + assertEquals(2, list.size()); + + for (TraceObject inf : list) { + activateInferior.invoke(Map.of("inferior", inf)); + String out = conn.executeCapture("inferior"); + String num = inf.getCanonicalPath().index(); + assertThat(out, containsString("Current inferior is %s".formatted(num))); + } + } + } + } + + @Test + public void testActivateThread() throws Exception { + try (GdbAndHandler conn = startAndConnectGdb()) { + conn.execute(""" + add-inferior + ghidra trace start + file bash + start + ghidra trace tx-open Start1 'ghidra trace put-threads' + add-inferior + inferior 2 + file bash + start + ghidra trace tx-open Start2 'ghidra trace put-threads'"""); + RemoteMethod activateThread = conn.getMethod("activate_thread"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/noname")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + + PathPattern pattern = + PathPredicates.parse("Inferiors[].Threads[]").getSingletonPattern(); + List list = tb.trace.getObjectManager() + .getValuePaths(Lifespan.at(0), pattern) + .map(p -> p.getDestination(null)) + .toList(); + assertEquals(2, list.size()); + + for (TraceObject t : list) { + activateThread.invoke(Map.of("thread", t)); + String out = conn.executeCapture("thread"); + List indices = pattern.matchKeys(t.getCanonicalPath().getKeyList()); + assertThat(out, containsString( + "Current thread is %s.%s".formatted(indices.get(0), indices.get(1)))); + } + } + } + } + + @Test + public void testActivateFrame() throws Exception { + try (GdbAndHandler conn = startAndConnectGdb()) { + conn.execute(""" + file bash + ghidra trace start + %s + break read + run""" + .formatted(INSTRUMENT_STOPPED)); + RemoteMethod activateFrame = conn.getMethod("activate_frame"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + + conn.execute("ghidra trace tx-open Init 'ghidra trace put-frames'"); + + List list = tb.trace.getObjectManager() + .getValuePaths(Lifespan.at(0), + PathPredicates.parse("Inferiors[1].Threads[1].Stack[]")) + .map(p -> p.getDestination(null)) + .toList(); + assertThat(list.size(), greaterThan(2)); + + for (TraceObject f : list) { + activateFrame.invoke(Map.of("frame", f)); + String out = conn.executeCapture("frame"); + String level = f.getCanonicalPath().index(); + assertThat(out, containsString("#%s".formatted(level))); + } + } + } + } + + @Test + public void testDeleteInferior() throws Exception { + try (GdbAndHandler conn = startAndConnectGdb()) { + conn.execute(""" + add-inferior + ghidra trace start + ghidra trace tx-open Init 'ghidra trace put-inferiors'"""); + RemoteMethod deleteInferior = conn.getMethod("delete_inferior"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/noname")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + + TraceObject inf2 = Objects.requireNonNull(tb.obj("Inferiors[2]")); + deleteInferior.invoke(Map.of("inferior", inf2)); + + String out = conn.executeCapture("info inferiors"); + assertThat(out, not(containsString("2"))); + assertEquals(2, out.strip().split("\n").length); // Header + 1 inferior + } + } + } + + @Test + public void testAttachObj() throws Exception { + String sleep = DummyProc.which("expTraceableSleep"); + try (DummyProc proc = DummyProc.run(sleep)) { + try (GdbAndHandler conn = startAndConnectGdb()) { + conn.execute(""" + ghidra trace start + ghidra trace tx-open Init 'ghidra trace put-available' + ghidra trace tx-open Init 'ghidra trace put-inferiors'"""); + RemoteMethod attachObj = conn.getMethod("attach_obj"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/noname")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + TraceObject inf = Objects.requireNonNull(tb.obj("Inferiors[1]")); + TraceObject target = + Objects.requireNonNull(tb.obj("Available[%d]".formatted(proc.pid))); + attachObj.invoke(Map.of("inferior", inf, "target", target)); + + String out = conn.executeCapture("info inferiors"); + assertThat(out, containsString("process %d".formatted(proc.pid))); + assertThat(out, containsString("expTraceableSleep")); + } + } + } + } + + @Test + public void testAttachPid() throws Exception { + String sleep = DummyProc.which("expTraceableSleep"); + try (DummyProc proc = DummyProc.run(sleep)) { + try (GdbAndHandler conn = startAndConnectGdb()) { + conn.execute(""" + ghidra trace start + ghidra trace tx-open Init 'ghidra trace put-inferiors'"""); + RemoteMethod attachPid = conn.getMethod("attach_pid"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/noname")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + TraceObject inf = Objects.requireNonNull(tb.obj("Inferiors[1]")); + attachPid.invoke(Map.of("inferior", inf, "pid", proc.pid)); + + String out = conn.executeCapture("info inferiors"); + assertThat(out, containsString("process %d".formatted(proc.pid))); + assertThat(out, containsString("expTraceableSleep")); + } + } + } + } + + @Test + public void testDetach() throws Exception { + String sleep = DummyProc.which("expTraceableSleep"); + try (DummyProc proc = DummyProc.run(sleep)) { + try (GdbAndHandler conn = startAndConnectGdb()) { + conn.execute(""" + ghidra trace start + %s + ghidra trace tx-open Init 'ghidra trace put-inferiors' + attach %d""" + .formatted(INSTRUMENT_STOPPED, proc.pid)); + RemoteMethod detach = conn.getMethod("detach"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/noname")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + + TraceObject inf = Objects.requireNonNull(tb.obj("Inferiors[1]")); + detach.invoke(Map.of("inferior", inf)); + + String out = conn.executeCapture("info inferiors"); + assertThat(out, containsString("")); + } + } + } + } + + @Test + public void testLaunchMain() throws Exception { + try (GdbAndHandler conn = startAndConnectGdb()) { + conn.execute(""" + ghidra trace start + %s + ghidra trace tx-open Init 'ghidra trace put-inferiors'""" + .formatted(INSTRUMENT_STOPPED)); + RemoteMethod launchMain = conn.getMethod("launch_main"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/noname")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + + TraceObject inf = Objects.requireNonNull(tb.obj("Inferiors[1]")); + launchMain.invoke(Map.ofEntries( + Map.entry("inferior", inf), + Map.entry("file", "bash"))); + waitStopped(); + + String out = conn.executeCapture("info inferiors"); + assertThat(out, containsString("bash")); + } + } + } + + @Test + public void testLaunchLoader() throws Exception { + try (GdbAndHandler conn = startAndConnectGdb()) { + conn.execute(""" + ghidra trace start + %s + ghidra trace tx-open Init 'ghidra trace put-inferiors'""" + .formatted(INSTRUMENT_STOPPED)); + RemoteMethod launchLoader = conn.getMethod("launch_loader"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/noname")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + + TraceObject inf = Objects.requireNonNull(tb.obj("Inferiors[1]")); + launchLoader.invoke(Map.ofEntries( + Map.entry("inferior", inf), + Map.entry("file", "bash"))); + waitStopped(); + + String out = conn.executeCapture("frame"); + assertThat(out, containsString("ld-linux")); + } + } + } + + @Test + public void testLaunchRun() throws Exception { + try (GdbAndHandler conn = startAndConnectGdb()) { + conn.execute(""" + ghidra trace start + %s + %s + ghidra trace tx-open Init 'ghidra trace put-inferiors'""" + .formatted(INSTRUMENT_STOPPED, INSTRUMENT_RUNNING)); + RemoteMethod launchRun = conn.getMethod("launch_run"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/noname")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + + TraceObject inf = Objects.requireNonNull(tb.obj("Inferiors[1]")); + launchRun.invoke(Map.ofEntries( + Map.entry("inferior", inf), + Map.entry("file", "bash"))); + waitRunning(); + Thread.sleep(100); // Give it plenty of time to block on read + + conn.execute("interrupt"); + waitStopped(); + + String out = conn.executeCapture("frame"); + assertThat(out, containsString("read")); + } + } + } + + @Test + public void testKill() throws Exception { + try (GdbAndHandler conn = startAndConnectGdb()) { + conn.execute(""" + file bash + ghidra trace start + %s + ghidra trace tx-open Init 'ghidra trace put-inferiors' + start""" + .formatted(INSTRUMENT_STOPPED)); + RemoteMethod kill = conn.getMethod("kill"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + + TraceObject inf = Objects.requireNonNull(tb.obj("Inferiors[1]")); + kill.invoke(Map.of("inferior", inf)); + + String out = conn.executeCapture("info inferiors"); + assertThat(out, containsString("")); + } + } + } + + @Test + public void testResumeInterrupt5() throws Exception { + try (GdbAndHandler conn = startAndConnectGdb()) { + RemoteMethod resume = conn.getMethod("resume"); + RemoteMethod interrupt = conn.getMethod("interrupt"); + conn.execute(""" + define do-put-before + ghidra trace new-snap Before + ghidra trace put-inferiors + ghidra trace putreg + end + define do-put-after + ghidra trace new-snap After + ghidra trace putreg + end + file bash + ghidra trace start + %s + %s + start""" + .formatted(INSTRUMENT_STOPPED, INSTRUMENT_RUNNING)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + conn.execute("ghidra trace tx-open Before do-put-before"); + + TraceObject inf1 = Objects.requireNonNull(tb.obj("Inferiors[1]")); + + for (int i = 0; i < 5; i++) { + resume.invoke(Map.of("inferior", inf1)); + waitRunning(); + + interrupt.invoke(Map.of()); + waitStopped(); + } + + conn.execute("ghidra trace tx-open After do-put-after"); + + Register pc = tb.language.getProgramCounter(); + AddressSpace t1s = tb.trace.getBaseAddressFactory() + .getAddressSpace("Inferiors[1].Threads[1].Stack[0].Registers"); + TraceMemorySpace regs = tb.trace.getMemoryManager().getMemorySpace(t1s, false); + RegisterValue pc0 = regs.getValue(0, pc); + RegisterValue pc1 = regs.getValue(1, pc); + assertNotEquals(pc0, pc1); + } + } + } + + @Test + public void testStepInto() throws Exception { + try (GdbAndHandler conn = startAndConnectGdb()) { + conn.execute(""" + file bash + ghidra trace start + %s + start""" + .formatted(INSTRUMENT_STOPPED)); + RemoteMethod stepInto = conn.getMethod("step_into"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + conn.execute("ghidra trace tx-open Init 'ghidra trace put-threads'"); + + TraceObject thread = Objects.requireNonNull(tb.obj("Inferiors[1].Threads[1]")); + + while (!conn.executeCapture("x/1i $pc").contains("call")) { + stepInto.invoke(Map.of("thread", thread)); + } + + String dis2 = conn.executeCapture("x/2i $pc"); + long pcNext = Long.decode(dis2.strip().split("\n")[1].strip().split("\\s+")[0]); + + stepInto.invoke(Map.of("thread", thread)); + long pc = Long.decode(conn.executeCapture("print/x $pc").split("\\s+")[2]); + assertNotEquals(pcNext, pc); + } + } + } + + @Test + public void testStepOver() throws Exception { + try (GdbAndHandler conn = startAndConnectGdb()) { + conn.execute(""" + file bash + ghidra trace start + %s + start""" + .formatted(INSTRUMENT_STOPPED)); + RemoteMethod step_over = conn.getMethod("step_over"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + conn.execute("ghidra trace tx-open Init 'ghidra trace put-threads'"); + + TraceObject thread = Objects.requireNonNull(tb.obj("Inferiors[1].Threads[1]")); + + while (!conn.executeCapture("x/1i $pc").contains("call")) { + step_over.invoke(Map.of("thread", thread)); + } + + String dis2 = conn.executeCapture("x/2i $pc"); + long pcNext = Long.decode(dis2.strip().split("\n")[1].strip().split("\\s+")[0]); + + step_over.invoke(Map.of("thread", thread)); + long pc = Long.decode(conn.executeCapture("print/x $pc").split("\\s+")[2]); + assertEquals(pcNext, pc); + } + } + } + + @Test + public void testStepOut() throws Exception { + try (GdbAndHandler conn = startAndConnectGdb()) { + conn.execute(""" + file bash + ghidra trace start + %s + start""" + .formatted(INSTRUMENT_STOPPED)); + RemoteMethod stepOut = conn.getMethod("step_out"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + conn.execute("ghidra trace tx-open Init 'ghidra trace put-threads'"); + + int initDepth = conn.executeCapture("bt").split("\n").length; + while (conn.executeCapture("bt").split("\n").length <= initDepth) { + conn.execute("stepi"); + } + + // TODO: Not likely, but the return could block on a syscall + TraceObject thread = Objects.requireNonNull(tb.obj("Inferiors[1].Threads[1]")); + stepOut.invoke(Map.of("thread", thread)); + assertEquals(initDepth, conn.executeCapture("bt").split("\n").length); + } + } + } + + @Test + public void testStepAdvance() throws Exception { + try (GdbAndHandler conn = startAndConnectGdb()) { + conn.execute(""" + file bash + ghidra trace start + %s + start""" + .formatted(INSTRUMENT_STOPPED)); + RemoteMethod stepAdvance = conn.getMethod("step_advance"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + conn.execute("ghidra trace tx-open Init 'ghidra trace put-threads'"); + + TraceObject thread = Objects.requireNonNull(tb.obj("Inferiors[1].Threads[1]")); + String dis3 = conn.executeCapture("x/3i $pc"); + // TODO: Examine for control transfer? + long pcTarget = Long.decode(dis3.strip().split("\n")[2].strip().split("\\s+")[0]); + + stepAdvance.invoke(Map.of("thread", thread, "address", tb.addr(pcTarget))); + + long pc = Long.decode(conn.executeCapture("print/x $pc").split("\\s+")[2]); + assertEquals(pcTarget, pc); + } + } + } + + @Test + public void testStepReturn() throws Exception { + try (GdbAndHandler conn = startAndConnectGdb()) { + conn.execute(""" + file bash + ghidra trace start + %s + start""" + .formatted(INSTRUMENT_STOPPED)); + RemoteMethod stepReturn = conn.getMethod("step_return"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + conn.execute("ghidra trace tx-open Init 'ghidra trace put-threads'"); + + int initDepth = conn.executeCapture("bt").split("\n").length; + while (conn.executeCapture("bt").split("\n").length <= initDepth) { + conn.execute("stepi"); + } + + TraceObject thread = Objects.requireNonNull(tb.obj("Inferiors[1].Threads[1]")); + stepReturn.invoke(Map.of("thread", thread)); + assertEquals(initDepth, conn.executeCapture("bt").split("\n").length); + } + } + } + + @Test + public void testBreakSwExecuteAddress() throws Exception { + try (GdbAndHandler conn = startAndConnectGdb()) { + conn.execute(""" + file bash + ghidra trace start + %s + start""" + .formatted(INSTRUMENT_STOPPED)); + RemoteMethod breakSwExecuteAddress = conn.getMethod("break_sw_execute_address"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + + TraceObject inf = Objects.requireNonNull(tb.obj("Inferiors[1]")); + long address = Long.decode(conn.executeCapture("print/x &main").split("\\s+")[2]); + breakSwExecuteAddress.invoke(Map.of("inferior", inf, "address", tb.addr(address))); + + String out = conn.executeCapture("info break"); + assertThat(out, containsString("
")); + } + } + } + + @Test + public void testBreakSwExecuteExpression() throws Exception { + try (GdbAndHandler conn = startAndConnectGdb()) { + conn.execute(""" + file bash + ghidra trace start + %s + start""" + .formatted(INSTRUMENT_STOPPED)); + RemoteMethod breakSwExecuteExpression = conn.getMethod("break_sw_execute_expression"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + + breakSwExecuteExpression.invoke(Map.of("expression", "main")); + + String out = conn.executeCapture("info break"); + assertThat(out, containsString("
")); + } + } + } + + @Test + public void testBreakHwExecuteAddress() throws Exception { + try (GdbAndHandler conn = startAndConnectGdb()) { + conn.execute(""" + file bash + ghidra trace start + %s + start""" + .formatted(INSTRUMENT_STOPPED)); + RemoteMethod breakHwExecuteAddress = conn.getMethod("break_hw_execute_address"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + + TraceObject inf = Objects.requireNonNull(tb.obj("Inferiors[1]")); + long address = Long.decode(conn.executeCapture("print/x &main").split("\\s+")[2]); + breakHwExecuteAddress.invoke(Map.of("inferior", inf, "address", tb.addr(address))); + + String out = conn.executeCapture("info break"); + assertThat(out, containsString("
")); + assertThat(out, containsString("hw breakpoint")); + } + } + } + + @Test + public void testBreakHwExecuteExpression() throws Exception { + try (GdbAndHandler conn = startAndConnectGdb()) { + conn.execute(""" + file bash + ghidra trace start + %s + start""" + .formatted(INSTRUMENT_STOPPED)); + RemoteMethod breakHwExecuteExpression = conn.getMethod("break_hw_execute_expression"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + + breakHwExecuteExpression.invoke(Map.of("expression", "main")); + + String out = conn.executeCapture("info break"); + assertThat(out, containsString("
")); + assertThat(out, containsString("hw breakpoint")); + } + } + } + + @Test + public void testBreakReadRange() throws Exception { + try (GdbAndHandler conn = startAndConnectGdb()) { + conn.execute(""" + file bash + ghidra trace start + %s + start""" + .formatted(INSTRUMENT_STOPPED)); + RemoteMethod breakReadRange = conn.getMethod("break_read_range"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + + TraceObject inf = Objects.requireNonNull(tb.obj("Inferiors[1]")); + long address = Long.decode(conn.executeCapture("print/x &main").split("\\s+")[2]); + AddressRange range = tb.range(address, address + 3); // length 4 + breakReadRange.invoke(Map.of("inferior", inf, "range", range)); + + String out = conn.executeCapture("info break"); + assertThat(out, containsString("0x%x".formatted(address))); + assertThat(out, containsString("[4]")); + assertThat(out, containsString("read watchpoint")); + } + } + } + + @Test + public void testBreakReadExpression() throws Exception { + try (GdbAndHandler conn = startAndConnectGdb()) { + conn.execute(""" + file bash + ghidra trace start + %s + start""" + .formatted(INSTRUMENT_STOPPED)); + RemoteMethod breakReadExpression = conn.getMethod("break_read_expression"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + + breakReadExpression.invoke(Map.of("expression", "main")); + + String out = conn.executeCapture("info break"); + assertThat(out, containsString("main")); + assertThat(out, containsString("read watchpoint")); + } + } + } + + @Test + public void testBreakWriteRange() throws Exception { + try (GdbAndHandler conn = startAndConnectGdb()) { + conn.execute(""" + file bash + ghidra trace start + %s + start""" + .formatted(INSTRUMENT_STOPPED)); + RemoteMethod breakWriteRange = conn.getMethod("break_write_range"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + + TraceObject inf = Objects.requireNonNull(tb.obj("Inferiors[1]")); + long address = Long.decode(conn.executeCapture("print/x &main").split("\\s+")[2]); + AddressRange range = tb.range(address, address + 3); // length 4 + breakWriteRange.invoke(Map.of("inferior", inf, "range", range)); + + String out = conn.executeCapture("info break"); + assertThat(out, containsString("0x%x".formatted(address))); + assertThat(out, containsString("[4]")); + assertThat(out, containsString("hw watchpoint")); + } + } + } + + @Test + public void testBreakWriteExpression() throws Exception { + try (GdbAndHandler conn = startAndConnectGdb()) { + conn.execute(""" + file bash + ghidra trace start + %s + start""" + .formatted(INSTRUMENT_STOPPED)); + RemoteMethod breakWriteExpression = conn.getMethod("break_write_expression"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + + breakWriteExpression.invoke(Map.of("expression", "main")); + + String out = conn.executeCapture("info break"); + assertThat(out, containsString("main")); + assertThat(out, containsString("hw watchpoint")); + } + } + } + + @Test + public void testBreakAccessRange() throws Exception { + try (GdbAndHandler conn = startAndConnectGdb()) { + conn.execute(""" + file bash + ghidra trace start + %s + start""" + .formatted(INSTRUMENT_STOPPED)); + RemoteMethod breakAccessRange = conn.getMethod("break_access_range"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + + TraceObject inf = Objects.requireNonNull(tb.obj("Inferiors[1]")); + long address = Long.decode(conn.executeCapture("print/x &main").split("\\s+")[2]); + AddressRange range = tb.range(address, address + 3); // length 4 + breakAccessRange.invoke(Map.of("inferior", inf, "range", range)); + + String out = conn.executeCapture("info break"); + assertThat(out, containsString("0x%x".formatted(address))); + assertThat(out, containsString("[4]")); + assertThat(out, containsString("acc watchpoint")); + } + } + } + + @Test + public void testBreakAccessExpression() throws Exception { + try (GdbAndHandler conn = startAndConnectGdb()) { + conn.execute(""" + file bash + ghidra trace start + %s + start""" + .formatted(INSTRUMENT_STOPPED)); + RemoteMethod breakAccessExpression = conn.getMethod("break_access_expression"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + + breakAccessExpression.invoke(Map.of("expression", "main")); + + String out = conn.executeCapture("info break"); + assertThat(out, containsString("main")); + assertThat(out, containsString("acc watchpoint")); + } + } + } + + @Test + public void testBreakEvent() throws Exception { + try (GdbAndHandler conn = startAndConnectGdb()) { + conn.execute(""" + file bash + ghidra trace start + %s + start""" + .formatted(INSTRUMENT_STOPPED)); + RemoteMethod breakEvent = conn.getMethod("break_event"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + + breakEvent.invoke(Map.of("spec", "load")); + + String out = conn.executeCapture("info break"); + assertThat(out, containsString("load of library")); + assertThat(out, containsString("catchpoint")); + } + } + } + + @Test + public void testToggleBreakpoint() throws Exception { + try (GdbAndHandler conn = startAndConnectGdb()) { + conn.execute(""" + file bash + ghidra trace start + %s + break main + run""" + .formatted(INSTRUMENT_STOPPED)); + RemoteMethod toggleBreakpoint = conn.getMethod("toggle_breakpoint"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + conn.execute("ghidra trace tx-open Init 'ghidra trace put-breakpoints'"); + TraceObject bpt = Objects.requireNonNull(tb.obj("Breakpoints[1]")); + + toggleBreakpoint.invoke(Map.of("breakpoint", bpt, "enabled", false)); + + String out = conn.executeCapture("info break"); + Tabular table = Tabular.parse(out); + assertEquals("n", table.findRow("Num", "1").getCell("Enb")); + } + } + } + + @Test + public void testToggleBreakpointLocation() throws Exception { + try (GdbAndHandler conn = startAndConnectGdb()) { + conn.execute(""" + file bash + ghidra trace start + %s + break main + run""" + .formatted(INSTRUMENT_STOPPED)); + RemoteMethod toggleBreakpointLocation = conn.getMethod("toggle_breakpoint_location"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + conn.execute("ghidra trace tx-open Init 'ghidra trace put-breakpoints'"); + // NB. Requires canonical path. Inf[1].Brk[1] is a link + TraceObject loc = Objects.requireNonNull(tb.obj("Breakpoints[1][1]")); + + toggleBreakpointLocation.invoke(Map.of("location", loc, "enabled", false)); + + String out = conn.executeCapture("info break"); + Tabular table = Tabular.parse(out); + + Row locRow = table.findRow("Num", "1.1"); + if (locRow != null) { + /** + * Earlier versions split the breakpoint's only location out, so the location + * can be disabled while the breakpoint itself remains "enabled." + */ + assertEquals("n", locRow.getCell("Enb")); + } + else { + /** + * Later versions recognize that disabling the only location disables the whole + * breakpoint. + */ + assertEquals("n", table.findRow("Num", "1").getCell("Enb")); + } + } + } + } + + @Test + public void testDeleteBreakpoint() throws Exception { + try (GdbAndHandler conn = startAndConnectGdb()) { + conn.execute(""" + file bash + ghidra trace start + %s + break main + run""" + .formatted(INSTRUMENT_STOPPED)); + RemoteMethod deleteBreakpoint = conn.getMethod("delete_breakpoint"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + conn.execute("ghidra trace tx-open Init 'ghidra trace put-breakpoints'"); + TraceObject bpt = Objects.requireNonNull(tb.obj("Breakpoints[1]")); + + deleteBreakpoint.invoke(Map.of("breakpoint", bpt)); + + String out = conn.executeCapture("info break"); + assertEquals(1, out.strip().split("\n").length); // Header only + } + } + } +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/agent/lldb/rmi/AbstractLldbTraceRmiTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/agent/lldb/rmi/AbstractLldbTraceRmiTest.java new file mode 100644 index 0000000000..5b2e5d436a --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/agent/lldb/rmi/AbstractLldbTraceRmiTest.java @@ -0,0 +1,509 @@ +/* ### + * 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 agent.lldb.rmi; + +import static org.hamcrest.Matchers.startsWith; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.SocketTimeoutException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.junit.Before; +import org.junit.BeforeClass; + +import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest; +import ghidra.app.plugin.core.debug.service.rmi.trace.RemoteAsyncResult; +import ghidra.app.plugin.core.debug.service.rmi.trace.RemoteMethod; +import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiAcceptor; +import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiHandler; +import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiPlugin; +import ghidra.app.plugin.core.debug.utils.ManagedDomainObject; +import ghidra.app.services.TraceRmiService; +import ghidra.dbg.testutil.DummyProc; +import ghidra.framework.TestApplicationUtils; +import ghidra.framework.main.ApplicationLevelOnlyPlugin; +import ghidra.framework.model.DomainFile; +import ghidra.framework.plugintool.Plugin; +import ghidra.framework.plugintool.util.*; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressRangeImpl; +import ghidra.trace.model.Lifespan; +import ghidra.trace.model.breakpoint.TraceBreakpointKind; +import ghidra.trace.model.breakpoint.TraceBreakpointKind.TraceBreakpointKindSet; +import ghidra.trace.model.target.TraceObject; +import ghidra.trace.model.target.TraceObjectValue; +import ghidra.util.Msg; +import ghidra.util.NumericUtilities; + +public abstract class AbstractLldbTraceRmiTest extends AbstractGhidraHeadedDebuggerGUITest { + // Connecting should be the first thing the script does, so use a tight timeout. + protected static final int CONNECT_TIMEOUT_MS = 3000; + protected static final int TIMEOUT_SECONDS = 300; + protected static final int QUIT_TIMEOUT_MS = 1000; + public static final String INSTRUMENT_STOPPED = + """ + ghidra_trace_txopen "Fake" 'ghidra_trace_create_obj Processes[1]' + define do-set-stopped + ghidra_trace_set_value Processes[1] _state '"STOPPED"' + end + define set-stopped + ghidra_trace_txopen Stopped do-set-stopped + end + #lldb.debugger.HandleCommand('target stop-hook add -P ghidralldb.hooks.StopHook') + #python lldb.events.stop.connect(lambda e: lldb.execute("set-stopped"))"""; + public static final String INSTRUMENT_RUNNING = + """ + ghidra_trace_txopen "Fake" 'ghidra_trace_create_obj Processes[1]' + define do-set-running + ghidra_trace_set_value Processes[1] _state '"RUNNING"' + end + define set-running + ghidra_trace_txopen Running do-set-running + end + #lldb.debugger.HandleCommand('target stop-hook add -P ghidralldb.hooks.StopHook') + #python lldb.events.cont.connect(lambda e: lldb.execute("set-running"))"""; + + protected TraceRmiService traceRmi; + private Path lldbPath; + private Path outFile; + private Path errFile; + + @BeforeClass + public static void setupPython() throws Throwable { + new ProcessBuilder("gradle", "Debugger-agent-lldb:installPyPackage") + .directory(TestApplicationUtils.getInstallationDirectory()) + .inheritIO() + .start() + .waitFor(); + } + + @Before + public void setupTraceRmi() throws Throwable { + traceRmi = addPlugin(tool, TraceRmiPlugin.class); + + lldbPath = Paths.get(DummyProc.which("lldb")); + outFile = Files.createTempFile("lldbout", null); + errFile = Files.createTempFile("lldberr", null); + } + + protected void addAllDebuggerPlugins() throws PluginException { + PluginsConfiguration plugConf = new PluginsConfiguration() { + @Override + protected boolean accepts(Class pluginClass) { + return !ApplicationLevelOnlyPlugin.class.isAssignableFrom(pluginClass); + } + }; + + for (PluginDescription pd : plugConf + .getPluginDescriptions(PluginPackage.getPluginPackage("Debugger"))) { + addPlugin(tool, pd.getPluginClass()); + } + } + + protected static String addrToStringForLldb(InetAddress address) { + if (address.isAnyLocalAddress()) { + return "127.0.0.1"; // Can't connect to 0.0.0.0 as such. Choose localhost. + } + return address.getHostAddress(); + } + + protected static String sockToStringForLldb(SocketAddress address) { + if (address instanceof InetSocketAddress tcp) { + return addrToStringForLldb(tcp.getAddress()) + ":" + tcp.getPort(); + } + throw new AssertionError("Unhandled address type " + address); + } + + protected record LldbResult(boolean timedOut, int exitCode, String stdout, String stderr) { + protected String handle() { + if (!"".equals(stderr) || (0 != exitCode && 143 != exitCode)) { + throw new LldbError(exitCode, stdout, stderr); + } + return stdout; + } + } + + protected record ExecInLldb(Process lldb, CompletableFuture future) { + } + + @SuppressWarnings("resource") // Do not close stdin + protected ExecInLldb execInLldb(String script) throws IOException { + ProcessBuilder pb = new ProcessBuilder(lldbPath.toString()); + // If commands come from file, LLDB will quit after EOF. + Msg.info(this, "outFile: " + outFile); + Msg.info(this, "errFile: " + errFile); + pb.redirectInput(ProcessBuilder.Redirect.PIPE); + pb.redirectOutput(outFile.toFile()); + pb.redirectError(errFile.toFile()); + Process lldbProc = pb.start(); + OutputStream stdin = lldbProc.getOutputStream(); + stdin.write(script.getBytes()); + stdin.flush(); + return new ExecInLldb(lldbProc, CompletableFuture.supplyAsync(() -> { + try { + if (!lldbProc.waitFor(TIMEOUT_SECONDS, TimeUnit.SECONDS)) { + Msg.error(this, "Timed out waiting for LLDB"); + lldbProc.destroyForcibly(); + lldbProc.waitFor(TIMEOUT_SECONDS, TimeUnit.SECONDS); + return new LldbResult(true, -1, Files.readString(outFile), + Files.readString(errFile)); + } + Msg.info(this, "LLDB exited with code " + lldbProc.exitValue()); + return new LldbResult(false, lldbProc.exitValue(), Files.readString(outFile), + Files.readString(errFile)); + } + catch (Exception e) { + return ExceptionUtils.rethrow(e); + } + finally { + lldbProc.destroyForcibly(); + } + })); + } + + public static class LldbError extends RuntimeException { + public final int exitCode; + public final String stdout; + public final String stderr; + + public LldbError(int exitCode, String stdout, String stderr) { + super(""" + exitCode=%d: + ----stdout---- + %s + ----stderr---- + %s + """.formatted(exitCode, stdout, stderr)); + this.exitCode = exitCode; + this.stdout = stdout; + this.stderr = stderr; + } + } + + protected String runThrowError(String script) throws Exception { + CompletableFuture result = execInLldb(script).future; + return result.get(TIMEOUT_SECONDS, TimeUnit.SECONDS).handle(); + } + + protected record LldbAndHandler(ExecInLldb exec, TraceRmiHandler handler) + implements AutoCloseable { + protected RemoteMethod getMethod(String name) { + return Objects.requireNonNull(handler.getMethods().get(name)); + } + + public void execute(String cmd) { + RemoteMethod execute = getMethod("execute"); + execute.invoke(Map.of("cmd", cmd)); + } + + public RemoteAsyncResult executeAsync(String cmd) { + RemoteMethod execute = getMethod("execute"); + return execute.invokeAsync(Map.of("cmd", cmd)); + } + + public String executeCapture(String cmd) { + RemoteMethod execute = getMethod("execute"); + return (String) execute.invoke(Map.of("cmd", cmd, "to_string", true)); + } + + @Override + public void close() throws Exception { + Msg.info(this, "Cleaning up lldb"); + exec.lldb().destroy(); + try { + LldbResult r = exec.future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + r.handle(); + waitForPass(() -> assertTrue(handler.isClosed())); + } + finally { + exec.lldb.destroyForcibly(); + } + } + } + + protected LldbAndHandler startAndConnectLldb(Function scriptSupplier) + throws Exception { + TraceRmiAcceptor acceptor = traceRmi.acceptOne(null); + ExecInLldb exec = + execInLldb(scriptSupplier.apply(sockToStringForLldb(acceptor.getAddress()))); + acceptor.setTimeout(CONNECT_TIMEOUT_MS); + try { + TraceRmiHandler handler = acceptor.accept(); + return new LldbAndHandler(exec, handler); + } + catch (SocketTimeoutException e) { + exec.lldb.destroyForcibly(); + exec.future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS).handle(); + throw e; + } + } + + protected LldbAndHandler startAndConnectLldb() throws Exception { + return startAndConnectLldb(addr -> """ + script import ghidralldb + ghidra_trace_connect %s + """.formatted(addr)); + } + + @SuppressWarnings("resource") + protected String runThrowError(Function scriptSupplier) + throws Exception { + LldbAndHandler conn = startAndConnectLldb(scriptSupplier); + LldbResult r = conn.exec.future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + String stdout = r.handle(); + waitForPass(() -> assertTrue(conn.handler.isClosed())); + return stdout; + } + + protected void waitStopped() { + TraceObject proc = Objects.requireNonNull(tb.objAny("Processes[]", Lifespan.at(0))); + waitForPass(() -> assertEquals("STOPPED", tb.objValue(proc, 0, "_state"))); + waitTxDone(); + } + + protected void waitRunning() { + TraceObject proc = Objects.requireNonNull(tb.objAny("Processes[]", Lifespan.at(0))); + waitForPass(() -> assertEquals("RUNNING", tb.objValue(proc, 0, "_state"))); + waitTxDone(); + } + + protected String extractOutSection(String out, String head) { + String[] split = out.split("\n"); + String xout = ""; + for (String s : split) { + if (!s.startsWith("(lldb)") && !s.equals("")) { + xout += s + "\n"; + } + } + return xout.split(head)[1].split("---")[0].replace("(lldb)", "").trim(); + } + + record MemDump(long address, byte[] data) { + } + + protected MemDump parseHexDump(String dump) throws IOException { + // First, get the address. Assume contiguous, so only need top line. + List lines = List.of(dump.split("\n")); + List toksLine0 = List.of(lines.get(0).split("\\s+")); + assertThat(toksLine0.get(0), startsWith("0x")); + String addrstr = toksLine0.get(0); + if (addrstr.contains(":")) { + addrstr = addrstr.substring(0, addrstr.indexOf(":")); + } + long address = Long.decode(addrstr); + + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + for (String l : lines) { + List parts = List.of(l.split(":")); + assertEquals(2, parts.size()); + String hex = parts.get(1).replaceAll("\\s*0x", ""); + byte[] lineData = NumericUtilities.convertStringToBytes(hex); + assertNotNull("Converted to null: " + hex, parts.get(1)); + buf.write(lineData); + } + return new MemDump(address, buf.toByteArray()); + } + + record RegDump() { + } + + protected RegDump parseRegDump(String dump) { + return new RegDump(); + } + + protected ManagedDomainObject openDomainObject(String path) throws Exception { + DomainFile df = env.getProject().getProjectData().getFile(path); + assertNotNull(df); + return new ManagedDomainObject(df, false, false, monitor); + } + + protected ManagedDomainObject waitDomainObject(String path) throws Exception { + DomainFile df; + long start = System.currentTimeMillis(); + while (true) { + df = env.getProject().getProjectData().getFile(path); + if (df != null) { + return new ManagedDomainObject(df, false, false, monitor); + } + Thread.sleep(1000); + if (System.currentTimeMillis() - start > 30000) { + throw new TimeoutException("30 seconds expired waiting for domain file"); + } + } + } + + protected void assertBreakLoc(TraceObjectValue locVal, String key, Address addr, int len, + Set kinds, String expression) throws Exception { + assertEquals(key, locVal.getEntryKey()); + TraceObject loc = locVal.getChild(); + TraceObject spec = loc.getCanonicalParent(0).getParent(); + assertEquals(new AddressRangeImpl(addr, len), loc.getValue(0, "_range").getValue()); + assertEquals(TraceBreakpointKindSet.encode(kinds), spec.getValue(0, "_kinds").getValue()); + assertTrue(spec.getValue(0, "_expression").getValue().toString().contains(expression)); + } + + protected void assertWatchLoc(TraceObjectValue locVal, String key, Address addr, int len, + Set kinds, String expression) throws Exception { + assertEquals(key, locVal.getEntryKey()); + TraceObject loc = locVal.getChild(); + assertEquals(new AddressRangeImpl(addr, len), loc.getValue(0, "_range").getValue()); + assertEquals(TraceBreakpointKindSet.encode(kinds), loc.getValue(0, "_kinds").getValue()); + } + + protected void waitTxDone() { + waitFor(() -> tb.trace.getCurrentTransactionInfo() == null); + } + + private record Cut(String head, int begin, int end) { + String parseCell(String line) { + int begin = Math.min(line.length(), this.begin); + int end = Math.min(line.length(), this.end); + /** + * NOTE: Do not assert previous char is space. + * + * When breakpoints table spells out locations, Address and What cells are indented and + * no longer align with their column headers. + */ + return line.substring(begin, end).trim(); + } + } + + protected record Row(Map cells) { + private static Row parse(List cuts, String line) { + return new Row( + cuts.stream().collect(Collectors.toMap(Cut::head, c -> c.parseCell(line)))); + } + + public String getCell(String head) { + return cells.get(head); + } + } + + protected record Tabular(List headings, List rows) { + static final Pattern SPACES = Pattern.compile(" *"); + static final Pattern WORDS = Pattern.compile("\\w+"); + + private static List findCuts(String header) { + List result = new ArrayList<>(); + Matcher spaceMatcher = SPACES.matcher(header); + Matcher wordMatcher = WORDS.matcher(header); + int start = 0; + while (start < header.length()) { + if (!spaceMatcher.find(start)) { + throw new AssertionError(); + } + start = spaceMatcher.end(); + if (start >= header.length()) { + break; + } + if (!wordMatcher.find(start)) { + throw new AssertionError(); + } + result.add(new Cut(wordMatcher.group(), wordMatcher.start(), wordMatcher.end())); + start = wordMatcher.end(); + } + return result; + } + + private static List adjustCuts(List cuts) { + List result = new ArrayList<>(); + for (int i = 0; i < cuts.size(); i++) { + Cut cut = cuts.get(i); + int j = i + 1; + int end = j < cuts.size() ? cuts.get(j).begin : Integer.MAX_VALUE; + result.add(new Cut(cut.head, cut.begin, end)); + } + return result; + } + + /** + * Parse a table. + * + *

+ * This is far from perfect, but good enough for making assertions in tests. For example, in + * the breakpoints table, lldb may insert an extra informational line under a breakpoint + * row. This line will get mangled and parsed as if it were an entry. However, it's "Num" + * cell will be empty, so they will not likely interfere. + * + * @param out the output in tabular form + * @return the table object, more or less + */ + public static Tabular parse(String out) { + List lines = List.of(out.split("\n")); + if (lines.isEmpty()) { + throw new AssertionError("Output is not tabular"); + } + List cuts = adjustCuts(findCuts(lines.get(0))); + return new Tabular(cuts.stream().map(Cut::head).toList(), + lines.stream().skip(1).map(l -> Row.parse(cuts, l)).toList()); + } + + public Row findRow(String head, String contents) { + return rows.stream() + .filter(r -> Objects.equals(contents, r.getCell(head))) + .findFirst() + .orElse(null); + } + } + + public static void waitForPass(Runnable runnable, long timeoutMs, long retryDelayMs) { + long start = System.currentTimeMillis(); + AssertionError lastError = null; + while (System.currentTimeMillis() - start < timeoutMs) { + try { + runnable.run(); + return; + } + catch (AssertionError e) { + lastError = e; + } + try { + Thread.sleep(retryDelayMs); + } + catch (InterruptedException e) { + // Retry sooner, I guess. + } + } + if (lastError == null) { + throw new AssertionError("Timed out before first try?"); + } + throw lastError; + } +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/agent/lldb/rmi/LldbCommandsTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/agent/lldb/rmi/LldbCommandsTest.java new file mode 100644 index 0000000000..c48302fe0b --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/agent/lldb/rmi/LldbCommandsTest.java @@ -0,0 +1,1279 @@ +/* ### + * 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 agent.lldb.rmi; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.junit.Test; + +import generic.Unique; +import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiAcceptor; +import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiHandler; +import ghidra.app.plugin.core.debug.utils.ManagedDomainObject; +import ghidra.dbg.util.PathPredicates; +import ghidra.framework.model.DomainFile; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressRange; +import ghidra.program.model.address.AddressRangeImpl; +import ghidra.program.model.address.AddressSpace; +import ghidra.program.model.lang.RegisterValue; +import ghidra.program.model.listing.CodeUnit; +import ghidra.trace.database.ToyDBTraceBuilder; +import ghidra.trace.model.ImmutableTraceAddressSnapRange; +import ghidra.trace.model.Lifespan; +import ghidra.trace.model.Trace; +import ghidra.trace.model.TraceAddressSnapRange; +import ghidra.trace.model.breakpoint.TraceBreakpointKind; +import ghidra.trace.model.memory.TraceMemoryRegion; +import ghidra.trace.model.memory.TraceMemorySpace; +import ghidra.trace.model.memory.TraceMemoryState; +import ghidra.trace.model.modules.TraceModule; +import ghidra.trace.model.target.TraceObject; +import ghidra.trace.model.target.TraceObjectKeyPath; +import ghidra.trace.model.target.TraceObjectValue; +import ghidra.trace.model.time.TraceSnapshot; +import ghidra.util.Msg; + +public class LldbCommandsTest extends AbstractLldbTraceRmiTest { + + //@Test + public void testManual() throws Exception { + TraceRmiAcceptor acceptor = traceRmi.acceptOne(null); + Msg.info(this, + "Use: ghidra_trace_connect " + sockToStringForLldb(acceptor.getAddress())); + TraceRmiHandler handler = acceptor.accept(); + Msg.info(this, "Connected: " + sockToStringForLldb(handler.getRemoteAddress())); + handler.waitClosed(); + Msg.info(this, "Closed"); + } + + @Test + public void testConnectErrorNoArg() throws Exception { + try { + runThrowError(""" + script import ghidralldb + ghidra_trace_connect + quit + """); + fail(); + } + catch (LldbError e) { + assertThat(e.stderr, containsString("'ghidra_trace_connect'")); + assertThat(e.stderr, containsString("'address'")); + } + } + + @Test + public void testConnect() throws Exception { + runThrowError(addr -> """ + script import ghidralldb + ghidra_trace_connect %s + quit + """.formatted(addr)); + } + + @Test + public void testDisconnect() throws Exception { + runThrowError(addr -> """ + script import ghidralldb + ghidra_trace_connect %s + ghidra_trace_disconnect + quit + """.formatted(addr)); + } + + @Test + public void testStartTraceDefaults() throws Exception { + // Default name and lcsp + runThrowError(addr -> """ + script import ghidralldb + ghidra_trace_connect %s + file bash + ghidra_trace_start + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + assertEquals("x86:LE:64:default", + tb.trace.getBaseLanguage().getLanguageID().getIdAsString()); + assertEquals("gcc", + tb.trace.getBaseCompilerSpec().getCompilerSpecID().getIdAsString()); + } + } + + @Test + public void testStartTraceDefaultNoFile() throws Exception { + runThrowError(addr -> """ + script import ghidralldb + ghidra_trace_connect %s + ghidra_trace_start + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/noname")) { + assertThat(mdo.get(), instanceOf(Trace.class)); + } + } + + @Test + public void testStartTraceCustomize() throws Exception { + runThrowError(addr -> """ + script import ghidralldb + ghidra_trace_connect %s + file bash + script ghidralldb.util.set_convenience_variable('ghidra-language','Toy:BE:64:default') + script ghidralldb.util.set_convenience_varaible('ghidra-compiler','default') + ghidra_trace_start myToy + quit + """.formatted(addr)); + DomainFile dfMyToy = env.getProject().getProjectData().getFile("/New Traces/myToy"); + assertNotNull(dfMyToy); + try (ManagedDomainObject mdo = new ManagedDomainObject(dfMyToy, false, false, monitor)) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + assertEquals("Toy:BE:64:default", + tb.trace.getBaseLanguage().getLanguageID().getIdAsString()); + assertEquals("default", + tb.trace.getBaseCompilerSpec().getCompilerSpecID().getIdAsString()); + } + } + + @Test + public void testStopTrace() throws Exception { + // TODO: This test assumes lldb and the target file bash are x86-64 + runThrowError(addr -> """ + script import ghidralldb + ghidra_trace_connect %s + file bash + ghidra_trace_start + ghidra_trace_stop + quit + """.formatted(addr)); + DomainFile dfBash = env.getProject().getProjectData().getFile("/New Traces/lldb/bash"); + assertNotNull(dfBash); + // TODO: Given the 'quit' command, I'm not sure this assertion is checking anything. + assertFalse(dfBash.isOpen()); + } + + @Test + public void testInfo() throws Exception { + AtomicReference refAddr = new AtomicReference<>(); + String out = runThrowError(addr -> { + refAddr.set(addr); + return """ + file bash + script import ghidralldb + _mark_ ---Import--- + ghidra_trace_info + ghidra_trace_connect %s + _mark_ ---Connect--- + ghidra_trace_info + ghidra_trace_start + _mark_ ---Start--- + ghidra_trace_info + ghidra_trace_stop + _mark_ ---Stop--- + ghidra_trace_info + ghidra_trace_disconnect + _mark_ ---Disconnect--- + ghidra_trace_info + quit + """.formatted(addr); + }); + + assertEquals(""" + Not connected to Ghidra""", + extractOutSection(out, "---Import---")); + assertEquals(""" + Connected to Ghidra at %s + No trace""".formatted(refAddr.get()), + extractOutSection(out, "---Connect---")); + assertEquals(""" + Connected to Ghidra at %s + Trace active""".formatted(refAddr.get()), + extractOutSection(out, "---Start---")); + assertEquals(""" + Connected to Ghidra at %s + No trace""".formatted(refAddr.get()), + extractOutSection(out, "---Stop---")); + assertEquals(""" + Not connected to Ghidra""", + extractOutSection(out, "---Disconnect---")); + } + + @Test + public void testLcsp() throws Exception { + // TODO: This test assumes x86-64 on test system + String out = runThrowError(""" + script import ghidralldb + _mark_ ---Import--- + ghidra_trace_info_lcsp + _mark_ --- + file bash + _mark_ ---File--- + ghidra_trace_info_lcsp + script ghidralldb.util.set_convenience_variable('ghidra-language','Toy:BE:64:default') + _mark_ ---Language--- + ghidra_trace_info_lcsp + script ghidralldb.util.set_convenience_variable('ghidra-compiler','posStack') + _mark_ ---Compiler--- + ghidra_trace_info_lcsp + quit + """); + +// assertEquals(""" +// Selected Ghidra language: DATA:LE:64:default +// Selected Ghidra compiler: pointer64""", +// extractOutSection(out, "---Import---")); + assertEquals(""" + Selected Ghidra language: x86:LE:64:default + Selected Ghidra compiler: gcc""", + extractOutSection(out, "---File---")); + assertEquals(""" + Selected Ghidra language: Toy:BE:64:default + Selected Ghidra compiler: default""", + extractOutSection(out, "---Language---")); + assertEquals(""" + Selected Ghidra language: Toy:BE:64:default + Selected Ghidra compiler: posStack""", + extractOutSection(out, "---Compiler---")); + } + + @Test + public void testSave() throws Exception { + traceManager.setSaveTracesByDefault(false); + + // For sanity check, verify failing to save drops data + runThrowError(addr -> """ + script import ghidralldb + ghidra_trace_connect %s + file bash + ghidra_trace_start no-save + ghidra_trace_txstart "Create snapshot" + ghidra_trace_new_snap "Scripted snapshot" + ghidra_trace_txcommit + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/no-save")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + assertEquals(0, tb.trace.getTimeManager().getAllSnapshots().size()); + } + + runThrowError(addr -> """ + script import ghidralldb + ghidra_trace_connect %s + file bash + ghidra_trace_start save + ghidra_trace_txstart "Create snapshot" + ghidra_trace_new_snap "Scripted snapshot" + ghidra_trace_txcommit + ghidra_trace_save + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/save")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + assertEquals(1, tb.trace.getTimeManager().getAllSnapshots().size()); + } + } + + @Test + public void testSnapshot() throws Exception { + runThrowError(addr -> """ + script import ghidralldb + ghidra_trace_connect %s + file bash + ghidra_trace_start + ghidra_trace_txstart "Create snapshot" + ghidra_trace_new_snap "Scripted snapshot" + ghidra_trace_txcommit + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + TraceSnapshot snapshot = Unique.assertOne(tb.trace.getTimeManager().getAllSnapshots()); + assertEquals(0, snapshot.getKey()); + assertEquals("\"Scripted snapshot\"", snapshot.getDescription()); + } + } + + @Test + public void testPutmem() throws Exception { + String out = runThrowError(addr -> """ + script import ghidralldb + ghidra_trace_connect %s + file bash + process launch --stop-at-entry + ghidra_trace_start + ghidra_trace_txstart "Create snapshot" + ghidra_trace_new_snap "Scripted snapshot" + ghidra_trace_putmem `(void(*)())main` 10 + ghidra_trace_txcommit + _mark_ ---Dump--- + x/10bx `(void(*)())main` + _mark_ ---") + kill + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + long snap = Unique.assertOne(tb.trace.getTimeManager().getAllSnapshots()).getKey(); + + MemDump dump = parseHexDump(extractOutSection(out, "---Dump---")); + ByteBuffer buf = ByteBuffer.allocate(dump.data().length); + tb.trace.getMemoryManager().getBytes(snap, tb.addr(dump.address()), buf); + + assertArrayEquals(dump.data(), buf.array()); + } + } + + // Not sure this is a meaningful test anymore + @Test + public void testPutmemProcess2() throws Exception { + String out = runThrowError(addr -> """ + script import ghidralldb + ghidra_trace_connect %s + file bash + process launch --stop-at-entry + ghidra_trace_start + ghidra_trace_txstart "Create snapshot" + ghidra_trace_new_snap "Scripted snapshot" + ghidra_trace_putmem `(void(*)())main` 10 + ghidra_trace_txcommit + _mark_ ---Dump--- + x/10bx `(void(*)())main` + _mark_ ---") + kill + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + AddressSpace ram2 = tb.trace.getBaseAddressFactory().getAddressSpace("ram"); + assertNotNull(ram2); + long snap = Unique.assertOne(tb.trace.getTimeManager().getAllSnapshots()).getKey(); + + MemDump dump = parseHexDump(extractOutSection(out, "---Dump---")); + ByteBuffer buf = ByteBuffer.allocate(dump.data().length); + tb.trace.getMemoryManager().getBytes(snap, ram2.getAddress(dump.address()), buf); + + assertArrayEquals(dump.data(), buf.array()); + } + } + + @Test + public void testPutmemState() throws Exception { + String out = runThrowError(addr -> """ + settings set interpreter.echo-commands false + script import ghidralldb + ghidra_trace_connect %s + file bash + process launch --stop-at-entry + ghidra_trace_start + ghidra_trace_txstart "Create snapshot" + ghidra_trace_new_snap "Scripted snapshot" + ghidra_trace_putmem_state `(void(*)())main` 10 error + ghidra_trace_txcommit + _mark_ ---Start--- + print/x (void(*)())main + _mark_ ---") + kill + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + long snap = Unique.assertOne(tb.trace.getTimeManager().getAllSnapshots()).getKey(); + + String eval = extractOutSection(out, "---Start---"); + String addrstr = eval.split("=")[1].trim(); + if (addrstr.contains(" ")) { + addrstr= addrstr.substring(0, addrstr.indexOf(" ")); + } + Address addr = tb.addr(Long.decode(addrstr)); + + Entry entry = + tb.trace.getMemoryManager().getMostRecentStateEntry(snap, addr); + assertEquals(Map.entry(new ImmutableTraceAddressSnapRange( + new AddressRangeImpl(addr, 10), Lifespan.at(0)), TraceMemoryState.ERROR), entry); + } + } + + @Test + public void testDelmem() throws Exception { + String out = runThrowError(addr -> """ + script import ghidralldb + ghidra_trace_connect %s + file bash + process launch --stop-at-entry + ghidra_trace_start + ghidra_trace_txstart "Create snapshot" + ghidra_trace_new_snap "Scripted snapshot" + ghidra_trace_putmem `(void(*)())main` 10 + ghidra_trace_delmem `(void(*)())main` 5 + ghidra_trace_txcommit + _mark_ ---Dump--- + x/10bx (void(*)())main + _mark_ ---") + kill + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + long snap = Unique.assertOne(tb.trace.getTimeManager().getAllSnapshots()).getKey(); + + MemDump dump = parseHexDump(extractOutSection(out, "---Dump---")); + Arrays.fill(dump.data(), 0, 5, (byte) 0); + ByteBuffer buf = ByteBuffer.allocate(dump.data().length); + tb.trace.getMemoryManager().getBytes(snap, tb.addr(dump.address()), buf); + + assertArrayEquals(dump.data(), buf.array()); + } + } + + @Test + public void testPutreg() throws Exception { + String count = IntStream.iterate(0, i -> i < 32, i -> i + 1) + .mapToObj(Integer::toString) + .collect(Collectors.joining(",", "{", "}")); + runThrowError(addr -> """ + script import ghidralldb + ghidra_trace_connect %s + file bash + process launch --stop-at-entry + ghidra_trace_start + expr $rax = 0xdeadbeef + #expr $ymm0 = %s + expr $st0 = 1.5 + ghidra_trace_txstart "Create snapshot" + ghidra_trace_new_snap "Scripted snapshot" + ghidra_trace_putreg + ghidra_trace_txcommit + kill + quit + """.formatted(addr, count)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + long snap = Unique.assertOne(tb.trace.getTimeManager().getAllSnapshots()).getKey(); + List regVals = tb.trace.getObjectManager() + .getValuePaths(Lifespan.at(0), + PathPredicates.parse("Processes[].Threads[].Stack[].Registers")) + .map(p -> p.getLastEntry()) + .toList(); + TraceObjectValue tobj = regVals.get(0); + AddressSpace t1f0 = tb.trace.getBaseAddressFactory() + .getAddressSpace(tobj.getCanonicalPath().toString()); + TraceMemorySpace regs = tb.trace.getMemoryManager().getMemorySpace(t1f0, false); + + RegisterValue rax = regs.getValue(snap, tb.reg("rax")); + assertEquals("deadbeef", rax.getUnsignedValue().toString(16)); + +// RegisterValue ymm0 = regs.getValue(snap, tb.reg("ymm0")); +// // LLDB treats registers in arch's endian +// assertEquals("1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100", +// ymm0.getUnsignedValue().toString(16)); + +// TraceData st0; +// try (Transaction tx = tb.trace.openTransaction("Float80 unit")) { +// TraceCodeSpace code = tb.trace.getCodeManager().getCodeSpace(t1f0, true); +// st0 = code.definedData() +// .create(Lifespan.nowOn(0), tb.reg("st0"), Float10DataType.dataType); +// } +// assertEquals("1.5", st0.getDefaultValueRepresentation()); + } + } + + @Test + public void testDelreg() throws Exception { + String count = IntStream.iterate(0, i -> i < 32, i -> i + 1) + .mapToObj(Integer::toString) + .collect(Collectors.joining(",", "{", "}")); + runThrowError(addr -> """ + script import ghidralldb + ghidra_trace_connect %s + file bash + process launch --stop-at-entry + ghidra_trace_start + #expr $ymm0 = %s + expr $st0 = 1.5 + ghidra_trace_txstart "Create snapshot" + ghidra_trace_new_snap "Scripted snapshot" + ghidra_trace_putreg + ghidra_trace_delreg + ghidra_trace_txcommit + kill + quit + """.formatted(addr, count)); + // The spaces will be left over, but the values should be zeroed + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + long snap = Unique.assertOne(tb.trace.getTimeManager().getAllSnapshots()).getKey(); + List regVals = tb.trace.getObjectManager() + .getValuePaths(Lifespan.at(0), + PathPredicates.parse("Processes[].Threads[].Stack[].Registers")) + .map(p -> p.getLastEntry()) + .toList(); + TraceObjectValue tobj = regVals.get(0); + AddressSpace t1f0 = tb.trace.getBaseAddressFactory() + .getAddressSpace(tobj.getCanonicalPath().toString()); + TraceMemorySpace regs = tb.trace.getMemoryManager().getMemorySpace(t1f0, false); + + RegisterValue rax = regs.getValue(snap, tb.reg("rax")); + assertEquals("0", rax.getUnsignedValue().toString(16)); + +// RegisterValue ymm0 = regs.getValue(snap, tb.reg("ymm0")); +// assertEquals("0", ymm0.getUnsignedValue().toString(16)); + +// TraceData st0; +// try (Transaction tx = tb.trace.openTransaction("Float80 unit")) { +// TraceCodeSpace code = tb.trace.getCodeManager().getCodeSpace(t1f0, true); +// st0 = code.definedData() +// .create(Lifespan.nowOn(0), tb.reg("st0"), Float10DataType.dataType); +// } +// assertEquals("0.0", st0.getDefaultValueRepresentation()); + } + } + + @Test + public void testCreateObj() throws Exception { + String out = runThrowError(addr -> """ + script import ghidralldb + ghidra_trace_connect %s + ghidra_trace_start + ghidra_trace_txstart "Create Object" + _mark_ ---Id--- + ghidra_trace_create_obj Test.Objects[1] + _mark_ ---") + ghidra_trace_txcommit + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/noname")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + TraceObject object = tb.trace.getObjectManager() + .getObjectByCanonicalPath(TraceObjectKeyPath.parse("Test.Objects[1]")); + assertNotNull(object); + String created = extractOutSection(out, "---Id---"); + long id = Long.parseLong(created.split("id=")[1].split(",")[0]); + assertEquals(object.getKey(), id); + } + } + + @Test + public void testInsertObj() throws Exception { + String out = runThrowError(addr -> """ + script import ghidralldb + ghidra_trace_connect %s + ghidra_trace_start + ghidra_trace_txstart "Create Object" + ghidra_trace_create_obj Test.Objects[1] + _mark_ ---Lifespan--- + ghidra_trace_insert_obj Test.Objects[1] + _mark_ ---") + ghidra_trace_txcommit + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/noname")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + TraceObject object = tb.trace.getObjectManager() + .getObjectByCanonicalPath(TraceObjectKeyPath.parse("Test.Objects[1]")); + assertNotNull(object); + Lifespan life = Unique.assertOne(object.getLife().spans()); + assertEquals(Lifespan.nowOn(0), life); + String lifeStr = extractOutSection(out, "---Lifespan---"); + assertEquals("Inserted object: lifespan=[0,+inf)", lifeStr); + } + } + + @Test + public void testRemoveObj() throws Exception { + runThrowError(addr -> """ + script import ghidralldb + ghidra_trace_connect %s + file bash + process launch --stop-at-entry + ghidra_trace_start + ghidra_trace_txstart "Create Object" + ghidra_trace_create_obj Test.Objects[1] + ghidra_trace_insert_obj Test.Objects[1] + ghidra_trace_set_snap 1 + ghidra_trace_remove_obj Test.Objects[1] + ghidra_trace_txcommit + kill + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + TraceObject object = tb.trace.getObjectManager() + .getObjectByCanonicalPath(TraceObjectKeyPath.parse("Test.Objects[1]")); + assertNotNull(object); + Lifespan life = Unique.assertOne(object.getLife().spans()); + assertEquals(Lifespan.at(0), life); + } + } + + @SuppressWarnings("unchecked") + protected T runTestSetValue(String extra, String lldbExpr, String gtype) + throws Exception { + runThrowError(addr -> """ + script import ghidralldb + ghidra_trace_connect %s + file bash + process launch --stop-at-entry + ghidra_trace_start + ghidra_trace_txstart "Create Object" + ghidra_trace_create_obj Test.Objects[1] + ghidra_trace_insert_obj Test.Objects[1] + %s + ghidra_trace_set_value Test.Objects[1] test %s %s + ghidra_trace_txcommit + kill + quit + """.formatted(addr, extra, lldbExpr, gtype)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + TraceObject object = tb.trace.getObjectManager() + .getObjectByCanonicalPath(TraceObjectKeyPath.parse("Test.Objects[1]")); + assertNotNull(object); + TraceObjectValue value = object.getValue(0, "test"); + return value == null ? null : (T) value.getValue(); + } + } + + // NB: Fails in gdb tests as well + //@Test + public void testSetValueNull() throws Exception { + assertNull(runTestSetValue("", "(void)null", "")); + } + + @Test + public void testSetValueBool() throws Exception { + // C++ required for bool + assertEquals(Boolean.TRUE, runTestSetValue("#set language c++", "(bool)1", "")); + } + + @Test + public void testSetValueByte() throws Exception { + assertEquals(Byte.valueOf((byte) 1), runTestSetValue("", "(char)1", "")); + } + + @Test + public void testSetValueChar() throws Exception { + assertEquals(Character.valueOf('A'), runTestSetValue("", "'A'", "CHAR")); + } + + @Test + public void testSetValueShort() throws Exception { + assertEquals(Short.valueOf((short) 1), runTestSetValue("", "(short)1", "")); + } + + @Test + public void testSetValueInt() throws Exception { + assertEquals(Integer.valueOf(1), runTestSetValue("", "(int)1", "")); + } + + @Test + public void testSetValueLong() throws Exception { + assertEquals(Long.valueOf(1), runTestSetValue("", "(long)1", "")); + } + + @Test + public void testSetValueString() throws Exception { + assertEquals("\"Hello World!\"", runTestSetValue("", "\"Hello World!\"", "")); + } + + @Test + public void testSetValueStringWide() throws Exception { + assertEquals("L\"Hello World!\"", runTestSetValue("", "L\"Hello World!\"", "")); + } + + @Test + public void testSetValueBoolArr() throws Exception { + // C++ required for bool, true, false + assertArrayEquals(new boolean[] { true, false }, + runTestSetValue("expr bool $x[2]={ true, false }", "$x", "")); + } + + @Test + public void testSetValueByteArrUsingString() throws Exception { + // Because explicit array type is chosen, we get null terminator + assertArrayEquals(new byte[] { 'H', 0, 'W', 0 }, + runTestSetValue("expr char $x[]=\"H\\0W\"", "$x", "BYTE_ARR")); + } + + @Test + public void testSetValueByteArrUsingArray() throws Exception { + assertArrayEquals(new byte[] { 'H', 0, 'W' }, + runTestSetValue("expr char $x[]={'H', 0, 'W'}", "$x", "BYTE_ARR")); + } + + @Test + public void testSetValueCharArrUsingString() throws Exception { + // Because explicit array type is chosen, we get null terminator + assertArrayEquals(new char[] { 'H', 0, 'W', 0 }, + runTestSetValue("expr char $x[]=\"H\\0W\"", "$x", "CHAR_ARR")); + } + + @Test + public void testSetValueCharArrUsingArray() throws Exception { + assertArrayEquals(new char[] { 'H', 0, 'W' }, + runTestSetValue("expr char $x[]={'H', 0, 'W'}", "$x", "CHAR_ARR")); + } + + @Test + public void testSetValueShortArrUsingString() throws Exception { + // Because explicit array type is chosen, we get null terminator + assertArrayEquals(new short[] { 'H', 0, 'W', 0 }, + runTestSetValue("expr wchar_t $x[]=L\"H\\0W\"", "$x", "SHORT_ARR")); + } + + @Test + public void testSetValueShortArrUsingArray() throws Exception { + assertArrayEquals(new short[] { 'H', 0, 'W' }, + runTestSetValue("expr short $x[]={'H', 0, 'W'}", "$x", "SHORT_ARR")); + } + + @Test + public void testSetValueIntArrayUsingMixedArray() throws Exception { + // Because explicit array type is chosen, we get null terminator + assertArrayEquals(new int[] { 'H', 0, 'W' }, + runTestSetValue("expr int $x[]={'H', 0, 'W'}", "$x", "INT_ARR")); + } + + @Test + public void testSetValueIntArrUsingArray() throws Exception { + assertArrayEquals(new int[] { 1, 2, 3, 4 }, + runTestSetValue("expr int $x[]={1,2,3,4}", "$x", "")); + } + + @Test + public void testSetValueLongArr() throws Exception { + assertArrayEquals(new long[] { 1, 2, 3, 4 }, + runTestSetValue("expr long long $x[]={1LL,2LL,3LL,4LL}", "$x", "")); + } + + // Skip String[]. Trouble is expressing them in LLDB.... + + @Test + public void testSetValueAddress() throws Exception { + Address address = runTestSetValue("", "(void*)0xdeadbeef", ""); + // Don't have the address factory to create expected address + assertEquals(0xdeadbeefL, address.getOffset()); + assertEquals("ram", address.getAddressSpace().getName()); + } + + @Test + public void testSetValueObject() throws Exception { + TraceObject object = runTestSetValue("", "Test.Objects[1]", "OBJECT"); + assertEquals("Test.Objects[1]", object.getCanonicalPath().toString()); + } + + @Test + public void testRetainValues() throws Exception { + runThrowError(addr -> """ + script import ghidralldb + ghidra_trace_connect %s + file bash + #set language c++ + process launch --stop-at-entry + ghidra_trace_start + ghidra_trace_txstart "Create Object" + ghidra_trace_create_obj Test.Objects[1] + ghidra_trace_insert_obj Test.Objects[1] + ghidra_trace_set_value Test.Objects[1] [1] '"A"' + ghidra_trace_set_value Test.Objects[1] [2] '"B"' + ghidra_trace_set_value Test.Objects[1] [3] '"C"' + ghidra_trace_set_snap 10 + ghidra_trace_retain_values Test.Objects[1] [1] [3] + ghidra_trace_txcommit + kill + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + TraceObject object = tb.trace.getObjectManager() + .getObjectByCanonicalPath(TraceObjectKeyPath.parse("Test.Objects[1]")); + assertNotNull(object); + assertEquals(Map.ofEntries( + Map.entry("[1]", Lifespan.nowOn(0)), + Map.entry("[2]", Lifespan.span(0, 9)), + Map.entry("[3]", Lifespan.nowOn(0))), + object.getValues() + .stream() + .collect(Collectors.toMap(v -> v.getEntryKey(), v -> v.getLifespan()))); + } + } + + @Test + public void testGetObj() throws Exception { + String out = runThrowError(addr -> """ + script import ghidralldb + ghidra_trace_connect %s + ghidra_trace_start + ghidra_trace_txstart "Create Object" + _mark_ ---Id--- + ghidra_trace_create_obj Test.Objects[1] + _mark_ ---") + ghidra_trace_txcommit + _mark_ ---GetObject--- + ghidra_trace_get_obj Test.Objects[1] + _mark_ ---") + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/noname")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + TraceObject object = tb.trace.getObjectManager() + .getObjectByCanonicalPath(TraceObjectKeyPath.parse("Test.Objects[1]")); + assertNotNull(object); + String getObject = extractOutSection(out, "---GetObject---"); + assertEquals("1\tTest.Objects[1]", getObject); + } + } + + @Test + public void testGetValues() throws Exception { + String out = runThrowError(addr -> """ + script import ghidralldb + ghidra_trace_connect %s + file bash + process launch --stop-at-entry + ghidra_trace_start + ghidra_trace_txstart "Create Object" + ghidra_trace_create_obj Test.Objects[1] + ghidra_trace_insert_obj Test.Objects[1] + #ghidra_trace_set_value Test.Objects[1] vnull (void)null + ghidra_trace_set_value Test.Objects[1] vbool true + ghidra_trace_set_value Test.Objects[1] vbyte (char)1 + ghidra_trace_set_value Test.Objects[1] vchar 'A' CHAR + ghidra_trace_set_value Test.Objects[1] vshort (short)2 + ghidra_trace_set_value Test.Objects[1] vint 3 + ghidra_trace_set_value Test.Objects[1] vlong 4LL + ghidra_trace_set_value Test.Objects[1] vstring "Hello" + expr bool $vboolarr[] = {true, false} + ghidra_trace_set_value Test.Objects[1] vboolarr $vboolarr + expr char $vbytearr[] = {1, 2, 3} + ghidra_trace_set_value Test.Objects[1] vbytearr $vbytearr BYTE_ARR + expr char $vchararr[] = "Hello" + ghidra_trace_set_value Test.Objects[1] vchararr $vchararr CHAR_ARR + expr short $vshortarr[] = {1, 2, 3} + ghidra_trace_set_value Test.Objects[1] vshortarr $vshortarr + expr int $vintarr[] = {1, 2, 3} + ghidra_trace_set_value Test.Objects[1] vintarr $vintarr + expr long $vlongarr[] = {1LL, 2LL, 3LL} + ghidra_trace_set_value Test.Objects[1] vlongarr $vlongarr + ghidra_trace_set_value Test.Objects[1] vaddr (void*)0xdeadbeef + ghidra_trace_set_value Test.Objects[1] vobj Test.Objects[1] OBJECT + ghidra_trace_txcommit + _mark_ ---GetValues--- + ghidra_trace_get_values Test.Objects[1]. + _mark_ ---") + kill + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + assertEquals(""" + Parent Key Span Value Type + Test.Objects[1] vbool [0,+inf) True BOOL + Test.Objects[1] vboolarr [0,+inf) [True, False] BOOL_ARR + Test.Objects[1] vbyte [0,+inf) 1 BYTE + Test.Objects[1] vbytearr [0,+inf) b'\\x01\\x02\\x03' BYTE_ARR + Test.Objects[1] vchar [0,+inf) 'A' CHAR + Test.Objects[1] vchararr [0,+inf) 'Hello\\x00' CHAR_ARR + Test.Objects[1] vint [0,+inf) 3 INT + Test.Objects[1] vintarr [0,+inf) [1, 2, 3] INT_ARR + Test.Objects[1] vlong [0,+inf) 4 LONG + Test.Objects[1] vlongarr [0,+inf) [1, 2, 3] LONG_ARR + Test.Objects[1] vobj [0,+inf) Test.Objects[1] OBJECT + Test.Objects[1] vshort [0,+inf) 2 SHORT + Test.Objects[1] vshortarr [0,+inf) [1, 2, 3] SHORT_ARR + Test.Objects[1] vstring [0,+inf) '"Hello"' STRING + Test.Objects[1] vaddr [0,+inf) ram:deadbeef ADDRESS""".replaceAll(" ", "").replaceAll("\n", ""), + extractOutSection(out, "---GetValues---").replaceAll(" ", "").replaceAll("\n", "")); + } + } + + @Test + public void testGetValuesRng() throws Exception { + String out = runThrowError(addr -> """ + script import ghidralldb + ghidra_trace_connect %s + file bash + #set language c++ + process launch --stop-at-entry + ghidra_trace_start + ghidra_trace_txstart "Create Object" + ghidra_trace_create_obj Test.Objects[1] + ghidra_trace_insert_obj Test.Objects[1] + ghidra_trace_set_value Test.Objects[1] vaddr (void*)0xdeadbeef + ghidra_trace_txcommit + _mark_ ---GetValues--- + ghidra_trace_get_values_rng (void*)0xdeadbeef 10 + _mark_ ---") + kill + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + assertEquals(""" + Parent + Key + Span + Value + Type + Test.Objects[1] + vaddr + [0,+inf) + ram:deadbeef + ADDRESS""".replaceAll(" ", ""), + extractOutSection(out, "---GetValues---").replaceAll(" ", "")); + } + } + + //@Test + public void testActivateObject() throws Exception { + runThrowError(addr -> """ + script import ghidralldb + ghidra_trace_connect %s + file bash + #set language c++ + process launch --stop-at-entry + ghidra_trace_start + ghidra_trace_txstart "Create Object" + ghidra_trace_create_obj Test.Objects[1] + ghidra_trace_insert_obj Test.Objects[1] + ghidra_trace_txcommit + ghidra_trace_activate Test.Objects[1] + kill + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + assertSame(mdo.get(), traceManager.getCurrentTrace()); + assertEquals("Test.Objects[1]", + traceManager.getCurrentObject().getCanonicalPath().toString()); + } + } + + @Test + public void testDisassemble() throws Exception { + String out = runThrowError(addr -> """ + script import ghidralldb + ghidra_trace_connect %s + file bash + #set language c++ + process launch --stop-at-entry + ghidra_trace_start + ghidra_trace_txstart "Tx" + ghidra_trace_putmem `(void(*)())main` 10 + _mark_ ---Disassemble--- + ghidra_trace_disassemble `(void(*)())main` + _mark_ ---") + ghidra_trace_txcommit + kill + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + // Not concerned about specifics, so long as disassembly occurs + long total = 0; + for (CodeUnit cu : tb.trace.getCodeManager().definedUnits().get(0, true)) { + total += cu.getLength(); + } + assertEquals("Disassembled %d bytes".formatted(total), + extractOutSection(out, "---Disassemble---")); + } + } + + @Test + public void testPutProcesses() throws Exception { + runThrowError(addr -> """ + script import ghidralldb + ghidra_trace_connect %s + ghidra_trace_start + ghidra_trace_txstart "Tx" + ghidra_trace_put_processes + ghidra_trace_txcommit + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/noname")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + // Would be nice to control / validate the specifics + Collection processes = tb.trace.getObjectManager() + .getValuePaths(Lifespan.at(0), PathPredicates.parse("Processes[]")) + .map(p -> p.getDestination(null)) + .toList(); + assertEquals(1, processes.size()); + } + } + + @Test + public void testPutAvailable() throws Exception { + runThrowError(addr -> """ + script import ghidralldb + ghidra_trace_connect %s + ghidra_trace_start + ghidra_trace_txstart "Tx" + ghidra_trace_put_available + ghidra_trace_txcommit + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/noname")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + // Would be nice to control / validate the specifics + Collection available = tb.trace.getObjectManager() + .getValuePaths(Lifespan.at(0), PathPredicates.parse("Available[]")) + .map(p -> p.getDestination(null)) + .toList(); + assertThat(available.size(), greaterThan(2)); + } + } + + @Test + public void testPutBreakpoints() throws Exception { + runThrowError(addr -> """ + script import ghidralldb + ghidra_trace_connect %s + file bash + process launch --stop-at-entry + ghidra_trace_start + ghidra_trace_txstart "Tx" + breakpoint set --name main + breakpoint set -H --name main + ghidra_trace_put_breakpoints + ghidra_trace_txcommit + kill + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + List procBreakLocVals = tb.trace.getObjectManager() + .getValuePaths(Lifespan.at(0), + PathPredicates.parse("Processes[].Breakpoints[]")) + .map(p -> p.getLastEntry()) + .toList(); + assertEquals(2, procBreakLocVals.size()); + AddressRange rangeMain = + procBreakLocVals.get(0).getChild().getValue(0, "_range").castValue(); + Address main = rangeMain.getMinAddress(); + + assertBreakLoc(procBreakLocVals.get(0), "[1.1]", main, 1, + Set.of(TraceBreakpointKind.SW_EXECUTE), + "main"); + assertBreakLoc(procBreakLocVals.get(1), "[2.1]", main, 1, + Set.of(TraceBreakpointKind.HW_EXECUTE), + "main"); + } + } + + @Test + public void testPutWatchpoints() throws Exception { + runThrowError(addr -> """ + script import ghidralldb + ghidra_trace_connect %s + file bash + process launch --stop-at-entry + ghidra_trace_start + ghidra_trace_txstart "Tx" + watchpoint set expression -- `(void(*)())main` + watchpoint set expression -w read -- `(void(*)())main`+-0x20 + watchpoint set expression -w read_write -- `(void(*)())main`+0x30 + ghidra_trace_put_watchpoints + ghidra_trace_txcommit + kill + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + List procWatchLocVals = tb.trace.getObjectManager() + .getValuePaths(Lifespan.at(0), + PathPredicates.parse("Processes[].Watchpoints[]")) + .map(p -> p.getLastEntry()) + .toList(); + assertEquals(3, procWatchLocVals.size()); + AddressRange rangeMain0 = + procWatchLocVals.get(0).getChild().getValue(0, "_range").castValue(); + Address main0 = rangeMain0.getMinAddress(); + AddressRange rangeMain1 = + procWatchLocVals.get(1).getChild().getValue(0, "_range").castValue(); + Address main1 = rangeMain1.getMinAddress(); + AddressRange rangeMain2 = + procWatchLocVals.get(2).getChild().getValue(0, "_range").castValue(); + Address main2 = rangeMain2.getMinAddress(); + + assertWatchLoc(procWatchLocVals.get(0), "[1]", main0, (int) rangeMain0.getLength(), + Set.of(TraceBreakpointKind.WRITE), "main"); + assertWatchLoc(procWatchLocVals.get(1), "[2]", main1, (int) rangeMain1.getLength(), + Set.of(TraceBreakpointKind.READ), "main"); + assertWatchLoc(procWatchLocVals.get(2), "[3]", main2, (int) rangeMain2.getLength(), + Set.of(TraceBreakpointKind.READ, TraceBreakpointKind.WRITE), "main"); + } + } + + @Test + public void testPutEnvironment() throws Exception { + runThrowError(addr -> """ + script import ghidralldb + ghidra_trace_connect %s + file bash + process launch --stop-at-entry + ghidra_trace_start + ghidra_trace_txstart "Tx" + ghidra_trace_put_environment + ghidra_trace_txcommit + kill + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + // Assumes LLDB on Linux amd64 + TraceObject env = Objects.requireNonNull(tb.objAny("Processes[].Environment", Lifespan.at(0))); + assertEquals("lldb", env.getValue(0, "_debugger").getValue()); + assertEquals("x86_64", env.getValue(0, "_arch").getValue()); + assertEquals("linux", env.getValue(0, "_os").getValue()); + assertEquals("little", env.getValue(0, "_endian").getValue()); + } + } + + @Test + public void testPutRegions() throws Exception { + runThrowError(addr -> """ + script import ghidralldb + ghidra_trace_connect %s + file bash + process launch --stop-at-entry + ghidra_trace_start + ghidra_trace_txstart "Tx" + ghidra_trace_put_regions + ghidra_trace_txcommit + kill + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + // Would be nice to control / validate the specifics + Collection all = + tb.trace.getMemoryManager().getAllRegions(); + assertThat(all.size(), greaterThan(2)); + } + } + + @Test + public void testPutModules() throws Exception { + runThrowError(addr -> """ + script import ghidralldb + ghidra_trace_connect %s + file bash + process launch --stop-at-entry + ghidra_trace_start + ghidra_trace_txstart "Tx" + ghidra_trace_put_modules + ghidra_trace_txcommit + kill + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + // Would be nice to control / validate the specifics + Collection all = tb.trace.getModuleManager().getAllModules(); + TraceModule modBash = + Unique.assertOne(all.stream().filter(m -> m.getName().contains("bash"))); + assertNotEquals(tb.addr(0), Objects.requireNonNull(modBash.getBase())); + } + } + + @Test + public void testPutThreads() throws Exception { + runThrowError(addr -> """ + script import ghidralldb + ghidra_trace_connect %s + file bash + process launch --stop-at-entry + ghidra_trace_start + ghidra_trace_txstart "Tx" + ghidra_trace_put_threads + ghidra_trace_txcommit + kill + quit + """.formatted(addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + // Would be nice to control / validate the specifics + Unique.assertOne(tb.trace.getThreadManager().getAllThreads()); + } + } + + @Test + public void testPutFrames() throws Exception { + try (LldbAndHandler conn = startAndConnectLldb()) { + conn.execute("file bash"); + conn.execute("ghidra_trace_start"); + conn.execute("ghidra_trace_txstart 'Tx'"); + conn.execute("ghidra_trace_put_processes"); + conn.execute("ghidra_trace_txcommit"); + conn.execute("ghidra_trace_install_hooks"); + conn.execute("breakpoint set -n read"); + conn.execute("run"); + + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + + waitStopped(); + conn.execute("ghidra_trace_txstart 'Tx'"); + conn.execute("ghidra_trace_put_frames"); + conn.execute("ghidra_trace_txcommit"); + conn.execute("kill"); + conn.execute("quit"); + // Would be nice to control / validate the specifics + List stack = tb.trace.getObjectManager() + .getValuePaths(Lifespan.at(0), + PathPredicates.parse("Processes[].Threads[].Stack[]")) + .map(p -> p.getDestination(null)) + .toList(); + assertThat(stack.size(), greaterThan(2)); + } + } + } + + @Test + public void testMinimal() throws Exception { + Function scriptSupplier = addr -> """ + script import ghidralldb + ghidra_trace_connect %s + """.formatted(addr); + try (LldbAndHandler conn = startAndConnectLldb(scriptSupplier)) { + conn.execute("script print('FINISHED')"); + conn.execute("quit"); + } + } +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/agent/lldb/rmi/LldbHooksTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/agent/lldb/rmi/LldbHooksTest.java new file mode 100644 index 0000000000..2bee916bb4 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/agent/lldb/rmi/LldbHooksTest.java @@ -0,0 +1,407 @@ +/* ### + * 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 agent.lldb.rmi; + +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Objects; + +import org.junit.Ignore; +import org.junit.Test; + +import agent.gdb.model.GdbLinuxSpecimen; +import ghidra.app.plugin.core.debug.utils.ManagedDomainObject; +import ghidra.dbg.util.PathPattern; +import ghidra.dbg.util.PathPredicates; +import ghidra.program.model.address.AddressSpace; +import ghidra.trace.database.ToyDBTraceBuilder; +import ghidra.trace.model.Lifespan; +import ghidra.trace.model.Trace; +import ghidra.trace.model.memory.TraceMemorySpace; +import ghidra.trace.model.target.TraceObject; +import ghidra.trace.model.time.TraceSnapshot; + +public class LldbHooksTest extends AbstractLldbTraceRmiTest { + private static final long RUN_TIMEOUT_MS = 20000; + private static final long RETRY_MS = 500; + + record LldbAndTrace(LldbAndHandler conn, ManagedDomainObject mdo) implements AutoCloseable { + public void execute(String cmd) { + conn.execute(cmd); + } + + public String executeCapture(String cmd) { + return conn.executeCapture(cmd); + } + + @Override + public void close() throws Exception { + conn.close(); + mdo.close(); + } + } + + @SuppressWarnings("resource") + protected LldbAndTrace startAndSyncLldb() throws Exception { + LldbAndHandler conn = startAndConnectLldb(); + try { + // TODO: Why does using 'set arch' cause a hang at quit? + conn.execute("ghidralldb.util.set_convenience_variable('ghidra-language', 'x86:LE:64:default')"); + conn.execute("ghidra_trace_start"); + ManagedDomainObject mdo = waitDomainObject("/New Traces/lldb/noname"); + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + return new LldbAndTrace(conn, mdo); + } + catch (Exception e) { + conn.close(); + throw e; + } + } + + protected long lastSnap(LldbAndTrace conn) { + return conn.conn.handler().getLastSnapshot(tb.trace); + } + + // TODO: This passes if you single-step through it but fails on some transactional stuff if run + //@Test + public void testOnNewThread() throws Exception { + try (LldbAndTrace conn = startAndSyncLldb()) { + + start(conn, "%s".formatted(GdbLinuxSpecimen.CLONE_EXIT.getCommandLine())); + conn.execute("break set -n work"); + waitForPass(() -> { + TraceObject proc = tb.objAny("Processes[]"); + assertNotNull(proc); + assertEquals("STOPPED", tb.objValue(proc, lastSnap(conn), "_state")); + }, RUN_TIMEOUT_MS, RETRY_MS); + + txPut(conn, "threads"); + waitForPass(() -> assertEquals(1, + tb.objValues(lastSnap(conn), "Processes[].Threads[]").size()), + RUN_TIMEOUT_MS, RETRY_MS); + + conn.execute("continue"); + waitStopped(); + txPut(conn, "threads"); + waitForPass(() -> assertEquals(2, + tb.objValues(lastSnap(conn), "Processes[].Threads[]").size()), + RUN_TIMEOUT_MS, RETRY_MS); + } + } + + // TODO: This passes if you single-step through it but fails on some transactional stuff if run + //@Test + public void testOnThreadSelected() throws Exception { + try (LldbAndTrace conn = startAndSyncLldb()) { + traceManager.openTrace(tb.trace); + + start(conn, "%s".formatted(GdbLinuxSpecimen.CLONE_EXIT.getCommandLine())); + conn.execute("break set -n work"); + + waitForPass(() -> { + TraceObject inf = tb.objAny("Processes[]"); + assertNotNull(inf); + assertEquals("STOPPED", tb.objValue(inf, lastSnap(conn), "_state")); + }, RUN_TIMEOUT_MS, RETRY_MS); + txPut(conn, "threads"); + waitForPass(() -> assertEquals(1, + tb.objValues(lastSnap(conn), "Processes[].Threads[]").size()), + RUN_TIMEOUT_MS, RETRY_MS); + + conn.execute("continue"); + waitStopped(); + waitForPass(() -> { + TraceObject inf = tb.objAny("Processes[]"); + assertNotNull(inf); + assertEquals("STOPPED", tb.objValue(inf, lastSnap(conn), "_state")); + }, RUN_TIMEOUT_MS, RETRY_MS); + + waitForPass(() -> assertEquals(2, + tb.objValues(lastSnap(conn), "Processes[].Threads[]").size()), + RUN_TIMEOUT_MS, RETRY_MS); + + // Now the real test + conn.execute("thread select 1"); + conn.execute("frame select 0"); + waitForPass(() -> { + String ti0 = conn.executeCapture("thread info"); + assertTrue(ti0.contains("#1")); + String threadIndex = threadIndex(traceManager.getCurrentObject()); + assertTrue(ti0.contains(threadIndex)); + }, RUN_TIMEOUT_MS, RETRY_MS); + + conn.execute("thread select 2"); + conn.execute("frame select 0"); + waitForPass(() -> { + String ti0 = conn.executeCapture("thread info"); + assertTrue(ti0.contains("#2")); + String threadIndex = threadIndex(traceManager.getCurrentObject()); + assertTrue(ti0.contains(threadIndex)); + }, RUN_TIMEOUT_MS, RETRY_MS); + + conn.execute("thread select 1"); + conn.execute("frame select 0"); + waitForPass(() -> { + String ti0 = conn.executeCapture("thread info"); + assertTrue(ti0.contains("#1")); + String threadIndex = threadIndex(traceManager.getCurrentObject()); + assertTrue(ti0.contains(threadIndex)); + }, RUN_TIMEOUT_MS, RETRY_MS); + } + } + + protected String getIndex(TraceObject object, String pattern, int n) { + if (object == null) { + return null; + } + PathPattern pat = PathPredicates.parse(pattern).getSingletonPattern(); +// if (pat.countWildcards() != 1) { +// throw new IllegalArgumentException("Exactly one wildcard required"); +// } + List path = object.getCanonicalPath().getKeyList(); + if (path.size() < pat.asPath().size()) { + return null; + } + List matched = pat.matchKeys(path.subList(0, pat.asPath().size())); + if (matched == null) { + return null; + } + if (matched.size() <= n) { + return null; + } + return matched.get(n); + } + + protected String threadIndex(TraceObject object) { + return getIndex(object, "Processes[].Threads[]", 1); + } + + protected String frameIndex(TraceObject object) { + return getIndex(object, "Processes[].Threads[].Stack[]", 2); + } + + @Test + public void testOnFrameSelected() throws Exception { + try (LldbAndTrace conn = startAndSyncLldb()) { + traceManager.openTrace(tb.trace); + + start(conn, "bash"); + conn.execute("breakpoint set -n read"); + conn.execute("cont"); + + waitStopped(); + waitForPass(() -> assertThat( + tb.objValues(lastSnap(conn), "Processes[].Threads[].Stack[]").size(), + greaterThan(2)), + RUN_TIMEOUT_MS, RETRY_MS); + + conn.execute("frame select 1"); + waitForPass(() -> assertEquals("1", frameIndex(traceManager.getCurrentObject())), + RUN_TIMEOUT_MS, RETRY_MS); + + conn.execute("frame select 0"); + waitForPass(() -> assertEquals("0", frameIndex(traceManager.getCurrentObject())), + RUN_TIMEOUT_MS, RETRY_MS); + } + } + + @Test + @Ignore + public void testOnSyscallMemory() throws Exception { + // TODO: Need a specimen + // FWIW, I've already seen this getting exercised in other tests. + } + + @Test + public void testOnMemoryChanged() throws Exception { + try (LldbAndTrace conn = startAndSyncLldb()) { + start(conn, "bash"); + + long address = Long.decode(conn.executeCapture("dis -c1 -n main").split("\\s+")[1]); + conn.execute("expr *((char*)(void(*)())main) = 0x7f"); + conn.execute("ghidra_trace_txstart 'Tx'"); + conn.execute("ghidra_trace_putmem `(void(*)())main` 10"); + conn.execute("ghidra_trace_txcommit"); + + waitForPass(() -> { + ByteBuffer buf = ByteBuffer.allocate(10); + tb.trace.getMemoryManager().getBytes(lastSnap(conn), tb.addr(address), buf); + assertEquals(0x7f, buf.get(0)); + }, RUN_TIMEOUT_MS, RETRY_MS); + } + } + + @Test + public void testOnRegisterChanged() throws Exception { + try (LldbAndTrace conn = startAndSyncLldb()) { + start(conn, "bash"); + + conn.execute("expr $rax = 0x1234"); + conn.execute("ghidra_trace_txstart 'Tx'"); + conn.execute("ghidra_trace_putreg"); + conn.execute("ghidra_trace_txcommit"); + + String path = "Processes[].Threads[].Stack[].Registers"; + TraceObject registers = Objects.requireNonNull(tb.objAny(path, Lifespan.at(0))); + AddressSpace space = tb.trace.getBaseAddressFactory() + .getAddressSpace(registers.getCanonicalPath().toString()); + TraceMemorySpace regs = tb.trace.getMemoryManager().getMemorySpace(space, false); + waitForPass(() -> assertEquals("1234", + regs.getValue(lastSnap(conn), tb.reg("RAX")).getUnsignedValue().toString(16))); + } + } + + @Test + public void testOnCont() throws Exception { + try (LldbAndTrace conn = startAndSyncLldb()) { + start(conn, "bash"); + + conn.execute("cont"); + waitRunning(); + + TraceObject proc = waitForValue(() -> tb.objAny("Processes[]")); + waitForPass(() -> { + assertEquals("RUNNING", tb.objValue(proc, lastSnap(conn), "_state")); + }, RUN_TIMEOUT_MS, RETRY_MS); + } + } + + @Test + public void testOnStop() throws Exception { + try (LldbAndTrace conn = startAndSyncLldb()) { + start(conn, "bash"); + + TraceObject inf = waitForValue(() -> tb.objAny("Processes[]")); + waitForPass(() -> { + assertEquals("STOPPED", tb.objValue(inf, lastSnap(conn), "_state")); + }, RUN_TIMEOUT_MS, RETRY_MS); + } + } + + @Test + public void testOnExited() throws Exception { + try (LldbAndTrace conn = startAndSyncLldb()) { + conn.execute("file bash"); + conn.execute("ghidra_trace_sync_enable"); + conn.execute("process launch --stop-at-entry -- -c 'exit 1'"); + txPut(conn, "processes"); + + conn.execute("cont"); + waitRunning(); + waitStopped(); + + waitForPass(() -> { + TraceSnapshot snapshot = + tb.trace.getTimeManager().getSnapshot(lastSnap(conn), false); + assertNotNull(snapshot); + assertEquals("Exited with code 1", snapshot.getDescription()); + + TraceObject proc = tb.objAny("Processes[]"); + assertNotNull(proc); + Object val = tb.objValue(proc, lastSnap(conn), "_exit_code"); + assertThat(val, instanceOf(Number.class)); + assertEquals(1, ((Number) val).longValue()); + }, RUN_TIMEOUT_MS, RETRY_MS); + } + } + + @Test + public void testOnBreakpointCreated() throws Exception { + try (LldbAndTrace conn = startAndSyncLldb()) { + start(conn, "bash"); + assertEquals(0, tb.objValues(lastSnap(conn), "Processes[].Breakpoints[]").size()); + + conn.execute("breakpoint set -n main"); + conn.execute("stepi"); + + waitForPass(() -> { + List brks = tb.objValues(lastSnap(conn), "Processes[].Breakpoints[]"); + assertEquals(1, brks.size()); + return (TraceObject) brks.get(0); + }); + } + } + + @Test + public void testOnBreakpointModified() throws Exception { + try (LldbAndTrace conn = startAndSyncLldb()) { + start(conn, "bash"); + assertEquals(0, tb.objValues(lastSnap(conn), "Breakpoints[]").size()); + + conn.execute("breakpoint set -n main"); + conn.execute("stepi"); + TraceObject brk = waitForPass(() -> { + List brks = tb.objValues(lastSnap(conn), "Breakpoints[]"); + assertEquals(1, brks.size()); + return (TraceObject) brks.get(0); + }); + assertEquals(null, tb.objValue(brk, lastSnap(conn), "Condition")); + conn.execute("breakpoint modify -c 'x>3'"); + conn.execute("stepi"); + // NB: Testing "Commands" requires multi-line input - not clear how to do this + //assertEquals(null, tb.objValue(brk, lastSnap(conn), "Commands")); + //conn.execute("breakpoint command add 'echo test'"); + //conn.execute("DONE"); + + waitForPass( + () -> assertEquals("x>3", tb.objValue(brk, lastSnap(conn), "Condition"))); + } + } + + @Test + public void testOnBreakpointDeleted() throws Exception { + try (LldbAndTrace conn = startAndSyncLldb()) { + start(conn, "bash"); + assertEquals(0, tb.objValues(lastSnap(conn), "Processes[].Breakpoints[]").size()); + + conn.execute("breakpoint set -n main"); + conn.execute("stepi"); + + TraceObject brk = waitForPass(() -> { + List brks = tb.objValues(lastSnap(conn), "Processes[].Breakpoints[]"); + assertEquals(1, brks.size()); + return (TraceObject) brks.get(0); + }); + + conn.execute("breakpoint delete %s".formatted(brk.getCanonicalPath().index())); + conn.execute("stepi"); + + waitForPass( + () -> assertEquals(0, tb.objValues(lastSnap(conn), "Processes[].Breakpoints[]").size())); + } + } + + private void start(LldbAndTrace conn, String obj) { + conn.execute("file "+obj); + conn.execute("ghidra_trace_sync_enable"); + conn.execute("process launch --stop-at-entry"); + txPut(conn, "processes"); + } + + private void txPut(LldbAndTrace conn, String obj) { + conn.execute("ghidra_trace_txstart 'Tx"+obj+"'"); + conn.execute("ghidra_trace_put_"+obj); + conn.execute("ghidra_trace_txcommit"); + } + +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/agent/lldb/rmi/LldbMethodsTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/agent/lldb/rmi/LldbMethodsTest.java new file mode 100644 index 0000000000..ac2f8ffa96 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/agent/lldb/rmi/LldbMethodsTest.java @@ -0,0 +1,1139 @@ +/* ### + * 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 agent.lldb.rmi; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.greaterThan; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import org.junit.Test; + +import generic.Unique; +import ghidra.app.plugin.core.debug.service.rmi.trace.RemoteMethod; +import ghidra.app.plugin.core.debug.service.rmi.trace.ValueDecoder; +import ghidra.app.plugin.core.debug.utils.ManagedDomainObject; +import ghidra.dbg.testutil.DummyProc; +import ghidra.dbg.util.PathPattern; +import ghidra.dbg.util.PathPredicates; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressRange; +import ghidra.program.model.address.AddressSpace; +import ghidra.program.model.lang.RegisterValue; +import ghidra.trace.database.ToyDBTraceBuilder; +import ghidra.trace.model.Lifespan; +import ghidra.trace.model.Trace; +import ghidra.trace.model.breakpoint.TraceBreakpointKind; +import ghidra.trace.model.memory.TraceMemoryRegion; +import ghidra.trace.model.memory.TraceMemorySpace; +import ghidra.trace.model.modules.TraceModule; +import ghidra.trace.model.target.TraceObject; +import ghidra.trace.model.target.TraceObjectValue; + +public class LldbMethodsTest extends AbstractLldbTraceRmiTest { + + @Test + public void testExecuteCapture() throws Exception { + try (LldbAndHandler conn = startAndConnectLldb()) { + RemoteMethod execute = conn.getMethod("execute"); + assertEquals(false, + execute.parameters().get("to_string").defaultValue().get(ValueDecoder.DEFAULT)); + assertEquals("test\n", execute.invoke(Map.of("cmd", "script print('test')", "to_string", true))); + } + } + + @Test + public void testExecute() throws Exception { + try (LldbAndHandler conn = startAndConnectLldb()) { + start(conn, "bash"); + conn.execute("kill"); + } + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + // Just confirm it's present + } + } + + @Test + public void testRefreshAvailable() throws Exception { + try (LldbAndHandler conn = startAndConnectLldb()) { + conn.execute("ghidra_trace_start"); + txCreate(conn, "Available"); + + RemoteMethod refreshAvailable = conn.getMethod("refresh_available"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/noname")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + TraceObject available = Objects.requireNonNull(tb.objAny("Available")); + + refreshAvailable.invoke(Map.of("node", available)); + + // Would be nice to control / validate the specifics + List list = tb.trace.getObjectManager() + .getValuePaths(Lifespan.at(0), PathPredicates.parse("Available[]")) + .map(p -> p.getDestination(null)) + .toList(); + assertThat(list.size(), greaterThan(2)); + } + } + } + + @Test + public void testRefreshBreakpoints() throws Exception { + try (LldbAndHandler conn = startAndConnectLldb()) { + start(conn, "bash"); + txPut(conn, "processes"); + + RemoteMethod refreshBreakpoints = conn.getMethod("refresh_breakpoints"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + //waitStopped(); + + conn.execute("breakpoint set --name main"); + conn.execute("breakpoint set -H --name main"); + txPut(conn, "breakpoints"); + TraceObject breakpoints = Objects.requireNonNull(tb.objAny("Breakpoints")); + refreshBreakpoints.invoke(Map.of("node", breakpoints)); + + List procBreakLocVals = tb.trace.getObjectManager() + .getValuePaths(Lifespan.at(0), + PathPredicates.parse("Processes[].Breakpoints[]")) + .map(p -> p.getLastEntry()) + .toList(); + assertEquals(2, procBreakLocVals.size()); + AddressRange rangeMain = + procBreakLocVals.get(0).getChild().getValue(0, "_range").castValue(); + Address main = rangeMain.getMinAddress(); + + // The temporary breakpoint uses up number 1 + assertBreakLoc(procBreakLocVals.get(0), "[1.1]", main, 1, + Set.of(TraceBreakpointKind.SW_EXECUTE), + "main"); + assertBreakLoc(procBreakLocVals.get(1), "[2.1]", main, 1, + Set.of(TraceBreakpointKind.HW_EXECUTE), + "main"); + } + } + } + + @Test + public void testRefreshProcBreakpoints() throws Exception { + try (LldbAndHandler conn = startAndConnectLldb()) { + start(conn, "bash"); + txPut(conn, "processes"); + txPut(conn, "breakpoints"); + + RemoteMethod refreshProcBreakpoints = conn.getMethod("refresh_proc_breakpoints"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + + TraceObject locations = Objects.requireNonNull(tb.objAny("Processes[].Breakpoints")); + conn.execute("breakpoint set --name main"); + conn.execute("breakpoint set -H --name main"); + refreshProcBreakpoints.invoke(Map.of("node", locations)); + + List procBreakLocVals = tb.trace.getObjectManager() + .getValuePaths(Lifespan.at(0), + PathPredicates.parse("Processes[].Breakpoints[]")) + .map(p -> p.getLastEntry()) + .toList(); + assertEquals(2, procBreakLocVals.size()); + AddressRange rangeMain = + procBreakLocVals.get(0).getChild().getValue(0, "_range").castValue(); + Address main = rangeMain.getMinAddress(); + + assertBreakLoc(procBreakLocVals.get(0), "[1.1]", main, 1, + Set.of(TraceBreakpointKind.SW_EXECUTE), + "main"); + assertBreakLoc(procBreakLocVals.get(1), "[2.1]", main, 1, + Set.of(TraceBreakpointKind.HW_EXECUTE), + "main"); + } + } + } + + @Test + public void testRefreshProcWatchpoints() throws Exception { + try (LldbAndHandler conn = startAndConnectLldb()) { + start(conn, "bash"); + txPut(conn, "all"); + + RemoteMethod refreshProcWatchpoints = conn.getMethod("refresh_proc_watchpoints"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + + TraceObject locations = Objects.requireNonNull(tb.objAny("Processes[].Watchpoints")); + conn.execute("watchpoint set expression -- `(void(*)())main`"); + conn.execute("watchpoint set expression -w read -- `(void(*)())main`+-0x20"); + conn.execute("watchpoint set expression -w read_write -- `(void(*)())main`+0x30"); + refreshProcWatchpoints.invoke(Map.of("node", locations)); + + List procWatchLocVals = tb.trace.getObjectManager() + .getValuePaths(Lifespan.at(0), + PathPredicates.parse("Processes[].Watchpoints[]")) + .map(p -> p.getLastEntry()) + .toList(); + assertEquals(3, procWatchLocVals.size()); + AddressRange rangeMain0 = + procWatchLocVals.get(0).getChild().getValue(0, "_range").castValue(); + Address main0 = rangeMain0.getMinAddress(); + AddressRange rangeMain1 = + procWatchLocVals.get(1).getChild().getValue(0, "_range").castValue(); + Address main1 = rangeMain1.getMinAddress(); + AddressRange rangeMain2 = + procWatchLocVals.get(2).getChild().getValue(0, "_range").castValue(); + Address main2 = rangeMain2.getMinAddress(); + + assertWatchLoc(procWatchLocVals.get(0), "[1]", main0, (int) rangeMain0.getLength(), + Set.of(TraceBreakpointKind.WRITE), + "main"); + assertWatchLoc(procWatchLocVals.get(1), "[2]", main1, (int) rangeMain1.getLength(), + Set.of(TraceBreakpointKind.READ), + "main+0x20"); + assertWatchLoc(procWatchLocVals.get(2), "[3]", main2, (int) rangeMain1.getLength(), + Set.of(TraceBreakpointKind.READ,TraceBreakpointKind.WRITE), + "main+0x30"); + } + } + } + + @Test + public void testRefreshProcesses() throws Exception { + try (LldbAndHandler conn = startAndConnectLldb()) { + conn.execute("ghidra_trace_start"); + txCreate(conn, "Processes"); + txCreate(conn, "Processes[1]"); + + RemoteMethod refreshProcesses = conn.getMethod("refresh_processes"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/noname")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + TraceObject processes = Objects.requireNonNull(tb.objAny("Processes")); + + refreshProcesses.invoke(Map.of("node", processes)); + + // Would be nice to control / validate the specifics + List list = tb.trace.getObjectManager() + .getValuePaths(Lifespan.at(0), PathPredicates.parse("Processes[]")) + .map(p -> p.getDestination(null)) + .toList(); + assertEquals(1, list.size()); + } + } + } + + @Test + public void testRefreshEnvironment() throws Exception { + try (LldbAndHandler conn = startAndConnectLldb()) { + String path = "Processes[].Environment"; + start(conn, "bash"); + txPut(conn, "all"); + + RemoteMethod refreshEnvironment = conn.getMethod("refresh_environment"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + TraceObject env = Objects.requireNonNull(tb.objAny(path)); + + refreshEnvironment.invoke(Map.of("node", env)); + + // Assumes LLDB on Linux amd64 + assertEquals("lldb", env.getValue(0, "_debugger").getValue()); + assertEquals("x86_64", env.getValue(0, "_arch").getValue()); + assertEquals("linux", env.getValue(0, "_os").getValue()); + assertEquals("little", env.getValue(0, "_endian").getValue()); + } + } + } + + @Test + public void testRefreshThreads() throws Exception { + try (LldbAndHandler conn = startAndConnectLldb()) { + String path = "Processes[].Threads"; + start(conn, "bash"); + txCreate(conn, path); + + RemoteMethod refreshThreads = conn.getMethod("refresh_threads"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + TraceObject threads = Objects.requireNonNull(tb.objAny(path)); + + refreshThreads.invoke(Map.of("node", threads)); + + // Would be nice to control / validate the specifics + Unique.assertOne(tb.trace.getThreadManager().getAllThreads()); + } + } + } + + @Test + public void testRefreshStack() throws Exception { + try (LldbAndHandler conn = startAndConnectLldb()) { + String path = "Processes[].Threads[].Stack"; + conn.execute("file bash"); + conn.execute("ghidra_trace_start"); + txPut(conn, "processes"); + breakAt(conn, "read"); + + RemoteMethod refreshStack = conn.getMethod("refresh_stack"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + + txPut(conn, "frames"); + TraceObject stack = Objects.requireNonNull(tb.objAny(path)); + refreshStack.invoke(Map.of("node", stack)); + + // Would be nice to control / validate the specifics + List list = tb.trace.getObjectManager() + .getValuePaths(Lifespan.at(0), + PathPredicates.parse("Processes[].Threads[].Stack[]")) + .map(p -> p.getDestination(null)) + .toList(); + assertTrue(list.size() > 1); + } + } + } + + @Test + public void testRefreshRegisters() throws Exception { + try (LldbAndHandler conn = startAndConnectLldb()) { + String path = "Processes[].Threads[].Stack[].Registers"; + start(conn, "bash"); + conn.execute("ghidra_trace_txstart 'Tx'"); + conn.execute("ghidra_trace_putreg"); + conn.execute("ghidra_trace_txcommit"); + + RemoteMethod refreshRegisters = conn.getMethod("refresh_registers"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + + conn.execute("expr $rax = 0xdeadbeef"); + + TraceObject registers = Objects.requireNonNull(tb.objAny(path, Lifespan.at(0))); + refreshRegisters.invoke(Map.of("node", registers)); + + long snap = 0; + AddressSpace t1f0 = tb.trace.getBaseAddressFactory() + .getAddressSpace(registers.getCanonicalPath().toString()); + TraceMemorySpace regs = tb.trace.getMemoryManager().getMemorySpace(t1f0, false); + RegisterValue rax = regs.getValue(snap, tb.reg("rax")); + // LLDB treats registers in arch's endian + assertEquals("deadbeef", rax.getUnsignedValue().toString(16)); + } + } + } + + @Test + public void testRefreshMappings() throws Exception { + try (LldbAndHandler conn = startAndConnectLldb()) { + String path = "Processes[].Memory"; + start(conn, "bash"); + txCreate(conn, path); + + RemoteMethod refreshMappings = conn.getMethod("refresh_mappings"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + TraceObject memory = Objects.requireNonNull(tb.objAny(path)); + + refreshMappings.invoke(Map.of("node", memory)); + + // Would be nice to control / validate the specifics + Collection all = + tb.trace.getMemoryManager().getAllRegions(); + assertThat(all.size(), greaterThan(2)); + } + } + } + + @Test + public void testRefreshModules() throws Exception { + try (LldbAndHandler conn = startAndConnectLldb()) { + String path = "Processes[].Modules"; + start(conn, "bash"); + txCreate(conn, path); + + RemoteMethod refreshModules = conn.getMethod("refresh_modules"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + TraceObject modules = Objects.requireNonNull(tb.objAny(path)); + + refreshModules.invoke(Map.of("node", modules)); + + // Would be nice to control / validate the specifics + Collection all = tb.trace.getModuleManager().getAllModules(); + TraceModule modBash = + Unique.assertOne(all.stream().filter(m -> m.getName().contains("bash"))); + assertNotEquals(tb.addr(0), Objects.requireNonNull(modBash.getBase())); + } + } + } + + + @Test + public void testActivateThread() throws Exception { + try (LldbAndHandler conn = startAndConnectLldb()) { + // TODO: need to find this file (same issue in LldbHookTests + String dproc = DummyProc.which("expCloneExit"); + conn.execute("file "+dproc); + conn.execute("ghidra_trace_start"); + txPut(conn, "processes"); + breakAt(conn, "work"); + + RemoteMethod activateThread = conn.getMethod("activate_thread"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/expCloneExit")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + + txPut(conn, "threads"); + + PathPattern pattern = + PathPredicates.parse("Processes[].Threads[]").getSingletonPattern(); + List list = tb.trace.getObjectManager() + .getValuePaths(Lifespan.at(0), pattern) + .map(p -> p.getDestination(null)) + .toList(); + assertEquals(2, list.size()); + + for (TraceObject t : list) { + activateThread.invoke(Map.of("thread", t)); + String out = conn.executeCapture("thread info"); + List indices = pattern.matchKeys(t.getCanonicalPath().getKeyList()); + assertThat(out, containsString("tid = %s".formatted(indices.get(1)))); + } + } + } + } + + @Test + public void testActivateFrame() throws Exception { + try (LldbAndHandler conn = startAndConnectLldb()) { + conn.execute("file bash"); + conn.execute("ghidra_trace_start"); + txPut(conn, "processes"); + breakAt(conn, "read"); + + RemoteMethod activateFrame = conn.getMethod("activate_frame"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + + txPut(conn, "frames"); + + List list = tb.trace.getObjectManager() + .getValuePaths(Lifespan.at(0), + PathPredicates.parse("Processes[].Threads[].Stack[]")) + .map(p -> p.getDestination(null)) + .toList(); + //assertThat(list.size(), greaterThan(2)); + + for (TraceObject f : list) { + activateFrame.invoke(Map.of("frame", f)); + String out = conn.executeCapture("frame info"); + String level = f.getCanonicalPath().index(); + assertThat(out, containsString("#%s".formatted(level))); + } + } + } + } + + @Test + public void testRemoveProcess() throws Exception { + try (LldbAndHandler conn = startAndConnectLldb()) { + start(conn, "bash"); + txPut(conn, "processes"); + + RemoteMethod removeProcess = conn.getMethod("remove_process"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + + TraceObject proc2 = Objects.requireNonNull(tb.objAny("Processes[]")); + removeProcess.invoke(Map.of("process", proc2)); + + String out = conn.executeCapture("target list"); + assertThat(out, containsString("No targets")); + } + } + } + + @Test + public void testAttachObj() throws Exception { + String sleep = DummyProc.which("expTraceableSleep"); + try (DummyProc dproc = DummyProc.run(sleep)) { + try (LldbAndHandler conn = startAndConnectLldb()) { + conn.execute("ghidra_trace_start"); + txPut(conn, "available"); + txPut(conn, "processes"); + + RemoteMethod attachObj = conn.getMethod("attach_obj"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/noname")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + TraceObject proc = Objects.requireNonNull(tb.objAny("Processes[]", Lifespan.at(0))); + TraceObject target = + Objects.requireNonNull(tb.obj("Available[%d]".formatted(dproc.pid))); + attachObj.invoke(Map.of("process", proc, "target", target)); + + String out = conn.executeCapture("target list"); + assertThat(out, containsString("pid=%d".formatted(dproc.pid))); + } + } + } + } + + @Test + public void testAttachPid() throws Exception { + String sleep = DummyProc.which("expTraceableSleep"); + try (DummyProc dproc = DummyProc.run(sleep)) { + try (LldbAndHandler conn = startAndConnectLldb()) { + conn.execute("ghidra_trace_start"); + txPut(conn, "processes"); + + RemoteMethod attachPid = conn.getMethod("attach_pid"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/noname")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + TraceObject proc = Objects.requireNonNull(tb.objAny("Processes[]", Lifespan.at(0))); + attachPid.invoke(Map.of("process", proc, "pid", dproc.pid)); + + String out = conn.executeCapture("target list"); + assertThat(out, containsString("pid=%d".formatted(dproc.pid))); + } + } + } + } + + @Test + public void testDetach() throws Exception { + try (LldbAndHandler conn = startAndConnectLldb()) { + start(conn, "bash"); + txPut(conn, "processes"); + //conn.execute("process attach -p %d".formatted(dproc.pid)); + + RemoteMethod detach = conn.getMethod("detach"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + + TraceObject proc = Objects.requireNonNull(tb.objAny("Processes[]")); + detach.invoke(Map.of("process", proc)); + + String out = conn.executeCapture("target list"); + //assertThat(out, containsString("pid=%d".formatted(dproc.pid))); + assertThat(out, containsString("detached")); + } + } + } + + @Test + public void testLaunchEntry() throws Exception { + try (LldbAndHandler conn = startAndConnectLldb()) { + conn.execute("ghidra_trace_start"); + txPut(conn, "processes"); + + RemoteMethod launch = conn.getMethod("launch_loader"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/noname")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + + TraceObject proc = Objects.requireNonNull(tb.objAny("Processes[]")); + launch.invoke(Map.ofEntries( + Map.entry("process", proc), + Map.entry("file", "bash"))); + waitStopped(); + + String out = conn.executeCapture("target list"); + assertThat(out, containsString("bash")); + } + } + } + + @Test //Not clear how to send interrupt + public void testLaunch() throws Exception { + try (LldbAndHandler conn = startAndConnectLldb()) { + conn.execute("ghidra_trace_start"); + txPut(conn, "processes"); + + RemoteMethod launch = conn.getMethod("launch"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/noname")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + + TraceObject proc = Objects.requireNonNull(tb.objAny("Processes[]")); + launch.invoke(Map.ofEntries( + Map.entry("process", proc), + Map.entry("file", "bash"))); + + txPut(conn, "processes"); + + waitRunning(); + Thread.sleep(100); // Give it plenty of time to block on read + + conn.execute("process interrupt"); + txPut(conn, "processes"); + + waitStopped(); + + String out = conn.executeCapture("bt"); + assertThat(out, containsString("read")); + } + } + } + + @Test + public void testKill() throws Exception { + try (LldbAndHandler conn = startAndConnectLldb()) { + start(conn, "bash"); + txPut(conn, "processes"); + + RemoteMethod kill = conn.getMethod("kill"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + + TraceObject proc = Objects.requireNonNull(tb.objAny("Processes[]")); + kill.invoke(Map.of("process", proc)); + + String out = conn.executeCapture("target list"); + assertThat(out, containsString("exited")); + } + } + } + + @Test + public void testStepInto() throws Exception { + try (LldbAndHandler conn = startAndConnectLldb()) { + start(conn, "bash"); + txPut(conn, "processes"); + + RemoteMethod step_into = conn.getMethod("step_into"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + txPut(conn, "threads"); + + TraceObject thread = Objects.requireNonNull(tb.objAny("Processes[].Threads[]")); + + while (!conn.executeCapture("dis -c1 -s '$pc'").contains("call")) { + step_into.invoke(Map.of("thread", thread)); + } + + String dis2 = conn.executeCapture("dis -c2 -s '$pc'"); + // lab0: + // -> addr0 + // + // lab1: + // addr1 + long pcNext = Long.decode(dis2.strip().split("\n")[4].strip().split("\\s+")[0]); + + step_into.invoke(Map.of("thread", thread)); + String disAt = conn.executeCapture("dis -c1 -s '$pc'"); + long pc = Long.decode(disAt.strip().split("\n")[1].strip().split("\\s+")[1]); + assertNotEquals(pcNext, pc); + } + } + } + + @Test + public void testStepOver() throws Exception { + try (LldbAndHandler conn = startAndConnectLldb()) { + conn.execute("file bash"); + conn.execute("ghidra_trace_start"); + txPut(conn, "processes"); + breakAt(conn, "read"); + + RemoteMethod step_over = conn.getMethod("step_over"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + txPut(conn, "threads"); + + TraceObject thread = Objects.requireNonNull(tb.objAny("Processes[].Threads[]")); + + while (!conn.executeCapture("dis -c1 -s '$pc'").contains("call")) { + step_over.invoke(Map.of("thread", thread)); + } + + String dis2 = conn.executeCapture("dis -c2 -s '$pc'"); + // lab0: + // -> addr0 + // addr1 + long pcNext = Long.decode(dis2.strip().split("\n")[2].strip().split("\\s+")[0]); + + step_over.invoke(Map.of("thread", thread)); + String disAt = conn.executeCapture("dis -c1 -s '$pc'"); + long pc = Long.decode(disAt.strip().split("\n")[1].strip().split("\\s+")[1]); + assertEquals(pcNext, pc); + } + } + } + + //@Test Not obvious "thread until -a" works (and definitely requires debug info") + public void testAdvance() throws Exception { + try (LldbAndHandler conn = startAndConnectLldb()) { + start(conn, "bash"); + + RemoteMethod step_ext = conn.getMethod("step_ext"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + //waitStopped(); + txPut(conn, "threads"); + + TraceObject thread = Objects.requireNonNull(tb.objAny("Processes[].Threads[]")); + String dis3 = conn.executeCapture("disassemble -c3 -s '$pc'"); + // TODO: Examine for control transfer? + long pcTarget = Long.decode(dis3.strip().split("\n")[2].strip().split("\\s+")[0]); + + step_ext.invoke(Map.of("thread", thread, "address", tb.addr(pcTarget))); + + String dis1 = conn.executeCapture("disassemble -c1 -s '$pc'"); + long pc = Long.decode(dis1.strip().split("\n")[1].strip().split("\\s+")[1]); + assertEquals(pcTarget, pc); + } + } + } + + @Test + public void testFinish() throws Exception { + try (LldbAndHandler conn = startAndConnectLldb()) { + conn.execute("file bash"); + conn.execute("ghidra_trace_start"); + txPut(conn, "processes"); + breakAt(conn, "read"); + + RemoteMethod activate = conn.getMethod("activate_thread"); + RemoteMethod step_out = conn.getMethod("step_out"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + txPut(conn, "threads"); + + TraceObject thread = Objects.requireNonNull(tb.objAny("Processes[].Threads[]")); + activate.invoke(Map.of("thread", thread)); + + int initDepth = getDepth(conn); + + step_out.invoke(Map.of("thread", thread)); + + int finalDepth = getDepth(conn); + assertEquals(initDepth-1, finalDepth); + } + } + } + + @Test + public void testReturn() throws Exception { + try (LldbAndHandler conn = startAndConnectLldb()) { + conn.execute("file bash"); + conn.execute("ghidra_trace_start"); + txPut(conn, "processes"); + breakAt(conn, "read"); + + RemoteMethod activate = conn.getMethod("activate_thread"); + RemoteMethod ret = conn.getMethod("return"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + txPut(conn, "threads"); + + TraceObject thread = Objects.requireNonNull(tb.objAny("Processes[].Threads[]")); + activate.invoke(Map.of("thread", thread)); + + int initDepth = getDepth(conn); + + ret.invoke(Map.of("thread", thread)); + + int finalDepth = getDepth(conn); + assertEquals(initDepth-1, finalDepth); + } + } + } + + @Test + public void testBreakAddress() throws Exception { + try (LldbAndHandler conn = startAndConnectLldb()) { + start(conn, "bash"); + txPut(conn, "processes"); + + RemoteMethod breakAddress = conn.getMethod("break_address"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + + TraceObject proc = Objects.requireNonNull(tb.objAny("Processes[]")); + long address = Long.decode(conn.executeCapture("dis -c1 -n main").split("\\s+")[1]); + breakAddress.invoke(Map.of("process", proc, "address", tb.addr(address))); + + String out = conn.executeCapture("breakpoint list"); + assertThat(out, containsString("main")); + assertThat(out, containsString(Long.toHexString(address))); + } + } + } + + @Test + public void testBreakExpression() throws Exception { + try (LldbAndHandler conn = startAndConnectLldb()) { + start(conn, "bash"); + txPut(conn, "processes"); + + RemoteMethod breakExpression = conn.getMethod("break_expression"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + + breakExpression.invoke(Map.of("expression", "main")); + + String out = conn.executeCapture("breakpoint list"); + assertThat(out, containsString("main")); + } + } + } + + //@Test stderr getting populated with warning about exhausted hardware breakpoints + // Are hardware breakpoints available on our VMs? + public void testBreakHardwareAddress() throws Exception { + try (LldbAndHandler conn = startAndConnectLldb()) { + start(conn, "bash"); + txPut(conn, "processes"); + + RemoteMethod breakAddress = conn.getMethod("break_hw_address"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + + TraceObject proc = Objects.requireNonNull(tb.objAny("Processes[]")); + long address = Long.decode(conn.executeCapture("dis -c1 -n main").split("\\s+")[1]); + breakAddress.invoke(Map.of("process", proc, "address", tb.addr(address))); + + String out = conn.executeCapture("breakpoint list"); + assertThat(out, containsString(Long.toHexString(address))); + } + } + } + + @Test + public void testBreakHardwareExpression() throws Exception { + try (LldbAndHandler conn = startAndConnectLldb()) { + start(conn, "bash"); + txPut(conn, "processes"); + + RemoteMethod breakExpression = conn.getMethod("break_hw_expression"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + + breakExpression.invoke(Map.of("expression", "`(void(*)())main`")); + + String out = conn.executeCapture("breakpoint list"); + long address = Long.decode(conn.executeCapture("dis -c1 -n main").split("\\s+")[1]); + //NB: a little odd that this isn't in hex + assertThat(out, containsString(Long.toString(address))); + } + } + } + + @Test + public void testBreakReadRange() throws Exception { + try (LldbAndHandler conn = startAndConnectLldb()) { + start(conn, "bash"); + txPut(conn, "processes"); + + RemoteMethod breakRange = conn.getMethod("break_read_range"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + + TraceObject proc = Objects.requireNonNull(tb.objAny("Processes[]")); + long address = Long.decode(conn.executeCapture("dis -c1 -n main").split("\\s+")[1]); + AddressRange range = tb.range(address, address + 3); // length 4 + breakRange.invoke(Map.of("process", proc, "range", range)); + + String out = conn.executeCapture("watchpoint list"); + assertThat(out, containsString("0x%x".formatted(address))); + assertThat(out, containsString("size = 4")); + assertThat(out, containsString("type = r")); + } + } + } + + @Test + public void testBreakReadExpression() throws Exception { + try (LldbAndHandler conn = startAndConnectLldb()) { + start(conn, "bash"); + txPut(conn, "processes"); + + RemoteMethod breakExpression = conn.getMethod("break_read_expression"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + + breakExpression.invoke(Map.of("expression", "`(void(*)())main`")); + long address = Long.decode(conn.executeCapture("dis -c1 -n main").split("\\s+")[1]); + + String out = conn.executeCapture("watchpoint list"); + assertThat(out, containsString(Long.toHexString(address))); + assertThat(out, containsString("type = r")); + } + } + } + + @Test + public void testBreakWriteRange() throws Exception { + try (LldbAndHandler conn = startAndConnectLldb()) { + start(conn, "bash"); + txPut(conn, "processes"); + + RemoteMethod breakRange = conn.getMethod("break_write_range"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + + TraceObject proc = Objects.requireNonNull(tb.objAny("Processes[]")); + long address = Long.decode(conn.executeCapture("dis -c1 -n main").split("\\s+")[1]); + AddressRange range = tb.range(address, address + 3); // length 4 + breakRange.invoke(Map.of("process", proc, "range", range)); + + String out = conn.executeCapture("watchpoint list"); + assertThat(out, containsString("0x%x".formatted(address))); + assertThat(out, containsString("size = 4")); + assertThat(out, containsString("type = w")); + } + } + } + + @Test + public void testBreakWriteExpression() throws Exception { + try (LldbAndHandler conn = startAndConnectLldb()) { + start(conn, "bash"); + txPut(conn, "processes"); + + RemoteMethod breakExpression = conn.getMethod("break_write_expression"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + + breakExpression.invoke(Map.of("expression", "`(void(*)())main`")); + long address = Long.decode(conn.executeCapture("dis -c1 -n main").split("\\s+")[1]); + + String out = conn.executeCapture("watchpoint list"); + assertThat(out, containsString(Long.toHexString(address))); + assertThat(out, containsString("type = w")); + } + } + } + + @Test + public void testBreakAccessRange() throws Exception { + try (LldbAndHandler conn = startAndConnectLldb()) { + start(conn, "bash"); + txPut(conn, "processes"); + + RemoteMethod breakRange = conn.getMethod("break_access_range"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + + TraceObject proc = Objects.requireNonNull(tb.objAny("Processes[]")); + long address = Long.decode(conn.executeCapture("dis -c1 -n main").split("\\s+")[1]); + AddressRange range = tb.range(address, address + 3); // length 4 + breakRange.invoke(Map.of("process", proc, "range", range)); + + String out = conn.executeCapture("watchpoint list"); + assertThat(out, containsString("0x%x".formatted(address))); + assertThat(out, containsString("size = 4")); + assertThat(out, containsString("type = rw")); + } + } + } + + @Test + public void testBreakAccessExpression() throws Exception { + try (LldbAndHandler conn = startAndConnectLldb()) { + start(conn, "bash"); + txPut(conn, "processes"); + + RemoteMethod breakExpression = conn.getMethod("break_access_expression"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + + breakExpression.invoke(Map.of("expression", "`(void(*)())main`")); + long address = Long.decode(conn.executeCapture("dis -c1 -n main").split("\\s+")[1]); + + String out = conn.executeCapture("watchpoint list"); + assertThat(out, containsString(Long.toHexString(address))); + assertThat(out, containsString("type = rw")); + } + } + } + + // NB: not really equivalent to gdb's "catch" but... + @Test + public void testBreakException() throws Exception { + try (LldbAndHandler conn = startAndConnectLldb()) { + start(conn, "bash"); + txPut(conn, "processes"); + + RemoteMethod breakExc = conn.getMethod("break_exception"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + + breakExc.invoke(Map.of("lang", "C++")); + + String out = conn.executeCapture("breakpoint list"); + assertThat(out, containsString("Exception")); + assertThat(out, containsString("__cxa_throw")); + } + } + } + + @Test + public void testToggleBreakpoint() throws Exception { + try (LldbAndHandler conn = startAndConnectLldb()) { + conn.execute("file bash"); + conn.execute("ghidra_trace_start"); + txPut(conn, "processes"); + breakAt(conn, "main"); + + RemoteMethod toggleBreakpoint = conn.getMethod("toggle_breakpoint"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + + txPut(conn, "breakpoints"); + TraceObject bpt = Objects.requireNonNull(tb.objAny("Breakpoints[]")); + + toggleBreakpoint.invoke(Map.of("breakpoint", bpt, "enabled", false)); + + String out = conn.executeCapture("breakpoint list"); + assertThat(out, containsString("disabled")); + } + } + } + + @Test + public void testToggleBreakpointLocation() throws Exception { + try (LldbAndHandler conn = startAndConnectLldb()) { + conn.execute("file bash"); + conn.execute("ghidra_trace_start"); + txPut(conn, "processes"); + breakAt(conn, "main"); + + + RemoteMethod toggleBreakpointLocation = conn.getMethod("toggle_breakpoint_location"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + + txPut(conn, "breakpoints"); + + // NB. Requires canonical path. Inf[].Brk[] is a link + TraceObject loc = Objects.requireNonNull(tb.objAny("Breakpoints[][]")); + + toggleBreakpointLocation.invoke(Map.of("location", loc, "enabled", false)); + + String out = conn.executeCapture("breakpoint list"); + assertThat(out, containsString("disabled")); + } + } + } + + @Test + public void testDeleteBreakpoint() throws Exception { + try (LldbAndHandler conn = startAndConnectLldb()) { + conn.execute("file bash"); + conn.execute("ghidra_trace_start"); + txPut(conn, "processes"); + breakAt(conn, "main"); + + RemoteMethod deleteBreakpoint = conn.getMethod("delete_breakpoint"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + + txPut(conn, "breakpoints"); + TraceObject bpt = Objects.requireNonNull(tb.objAny("Breakpoints[]")); + + deleteBreakpoint.invoke(Map.of("breakpoint", bpt)); + + String out = conn.executeCapture("breakpoint list"); + assertThat(out, containsString("No breakpoints")); + } + } + } + + @Test + public void testDeleteWatchpoint() throws Exception { + try (LldbAndHandler conn = startAndConnectLldb()) { + start(conn, "bash"); + txPut(conn, "processes"); + + RemoteMethod breakExpression = conn.getMethod("break_read_expression"); + RemoteMethod deleteWatchpoint = conn.getMethod("delete_watchpoint"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/lldb/bash")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + + breakExpression.invoke(Map.of("expression", "`(void(*)())main`")); + long address = Long.decode(conn.executeCapture("dis -c1 -n main").split("\\s+")[1]); + + String out = conn.executeCapture("watchpoint list"); + assertThat(out, containsString(Long.toHexString(address))); + + txPut(conn, "watchpoints"); + TraceObject wpt = Objects.requireNonNull(tb.objAny("Processes[].Watchpoints[]")); + + deleteWatchpoint.invoke(Map.of("watchpoint", wpt)); + + out = conn.executeCapture("watchpoint list"); + assertThat(out, containsString("No watchpoints")); + } + } + } + + private void start(LldbAndHandler conn, String obj) { + conn.execute("file "+obj); + conn.execute("ghidra_trace_start"); + conn.execute("process launch --stop-at-entry"); + } + + private void txPut(LldbAndHandler conn, String obj) { + conn.execute("ghidra_trace_txstart 'Tx'"); + conn.execute("ghidra_trace_put_"+obj); + conn.execute("ghidra_trace_txcommit"); + } + + private void txCreate(LldbAndHandler conn, String path) { + conn.execute("ghidra_trace_txstart 'Fake'"); + conn.execute("ghidra_trace_create_obj %s".formatted(path)); + conn.execute("ghidra_trace_txcommit"); + } + + private void breakAt(LldbAndHandler conn, String fn) { + conn.execute("ghidra_trace_sync_enable"); + conn.execute("breakpoint set -n "+fn); + conn.execute("run"); + } + + private int getDepth(LldbAndHandler conn) { + String[] split = conn.executeCapture("bt").split("\n"); + int initDepth = 0; + for (String str : split) { + if (str.contains("frame #")) { + initDepth++; + } + } + return initDepth; + } + +} diff --git a/gradle/debugger/hasProtobuf.gradle b/gradle/debugger/hasProtobuf.gradle new file mode 100644 index 0000000000..23b4ce74bb --- /dev/null +++ b/gradle/debugger/hasProtobuf.gradle @@ -0,0 +1,94 @@ +/* ### + * 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. + */ +/*plugins { + id 'com.google.protobuf' version '0.8.10' +}*/ + +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' + } + } +} + +/*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/gradle/debugger/hasPythonPackage.gradle b/gradle/debugger/hasPythonPackage.gradle new file mode 100644 index 0000000000..95a0426348 --- /dev/null +++ b/gradle/debugger/hasPythonPackage.gradle @@ -0,0 +1,86 @@ +/* ### + * 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. + */ +configurations { + pypkgInstall +} + +task assemblePyPackage(type: Copy) { + from "src/main/py" + into "build/pypkg/" +} + +def getGdbPython() { + def out = new ByteArrayOutputStream() + exec { + commandLine "gdb", "--batch" + args "-ex", "python import sys" + args "-ex", "python print(f'python{sys.version_info.major}.{sys.version_info.minor}')" + standardOutput = out + } + return "$out".strip() +} + +def getLldbPython() { + def out = new ByteArrayOutputStream() + exec { + commandLine "lldb", "--batch" + args "-ex", "python import sys" + args "-ex", "python print(f'python{sys.version_info.major}.{sys.version_info.minor}')" + standardOutput = out + } + return "$out".strip() +} + +task configureBuildPyPackage { + doLast { + def gdbPython = getGdbPython() + buildPyPackage.commandLine gdbPython, "-m", "build" + } +} + +task buildPyPackage(type: Exec) { + dependsOn(configureBuildPyPackage) + ext.dist = { file("build/pypkg/dist") } + inputs.files(assemblePyPackage) + outputs.dir(dist) + workingDir { "build/pypkg" } +} + +task configureInstallPyPackage { + dependsOn(configurations.pypkgInstall) + doLast { + def gdbPython = getGdbPython() + installPyPackage.commandLine gdbPython, "-m", "pip", "install", "--force-reinstall" + installPyPackage.args configurations.pypkgInstall.filter { f -> !f.name.endsWith(".jar") } + installPyPackage.args file("build/pypkg") + } +} + +task installPyPackage(type: Exec) { + dependsOn(configureInstallPyPackage) + inputs.files(assemblePyPackage) +} + +task phonyJarPyPackage(type: Jar) { + dependsOn(assemblePyPackage) +} + +afterEvaluate { + artifacts { + pypkgInstall file("build/pypkg") + pypkgInstall phonyJarPyPackage + } +}