diff --git a/Ghidra/Debug/Debugger/data/debugger.theme.properties b/Ghidra/Debug/Debugger/data/debugger.theme.properties index 6cde8822ee..8079d12938 100644 --- a/Ghidra/Debug/Debugger/data/debugger.theme.properties +++ b/Ghidra/Debug/Debugger/data/debugger.theme.properties @@ -65,6 +65,7 @@ color.debugger.plugin.resources.breakpoint.marker.disabled.ineffective = color.d icon.debugger.object.populated = object-populated.png icon.debugger.object.unpopulated = object-unpopulated.png +font.debugger.sleigh = font.monospaced font.debugger.object.tree.renderer = Tahoma-plain-11 icon.debugger.display.graph = breakpoint-enable.png // TODO this icon was missing 'breakpoints.png' @@ -168,10 +169,11 @@ icon.debugger.diff = table_relationship.png icon.debugger.diff.previous = up.png icon.debugger.diff.next = down.png -icon.debugger.edit.mode.read.only = write-disabled.png -icon.debugger.edit.mode.write.target = write-target.png -icon.debugger.edit.mode.write.trace = write-trace.png -icon.debugger.edit.mode.write.emulator = write-emulator.png +icon.debugger.edit.mode.ro.target = record.png +icon.debugger.edit.mode.rw.target = write-target.png +icon.debugger.edit.mode.ro.trace = video-x-generic16.png +icon.debugger.edit.mode.rw.trace = write-trace.png +icon.debugger.edit.mode.rw.emulator = write-emulator.png icon.debugger.marker.register = register-marker.png icon.debugger.marker.event = icon.debugger.marker.register diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerBreakpointMarkerPlugin/DebuggerBreakpointMarkerPlugin.html b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerBreakpointMarkerPlugin/DebuggerBreakpointMarkerPlugin.html index 4b73f04ca7..93887c6913 100644 --- a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerBreakpointMarkerPlugin/DebuggerBreakpointMarkerPlugin.html +++ b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerBreakpointMarkerPlugin/DebuggerBreakpointMarkerPlugin.html @@ -62,8 +62,8 @@ breakpoints on arbitrary expressions, use the Set Breakpoint action of the Objects window. NOTE: These actions may also appear in - other address-based contexts, e.g., the decompiler listing; however, those contexts often lack - any indication of breakpoint presence or state.

+ other address-based contexts, but not all of those contexts include visual breakpoint + indicators.

Toggle Breakpoint (K)

diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerBreakpointsPlugin/DebuggerBreakpointsPlugin.html b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerBreakpointsPlugin/DebuggerBreakpointsPlugin.html index 3d09ebaa36..40912e874d 100644 --- a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerBreakpointsPlugin/DebuggerBreakpointsPlugin.html +++ b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerBreakpointsPlugin/DebuggerBreakpointsPlugin.html @@ -22,126 +22,145 @@ -

The breakpoints window tabulates and manipulates breakpoints among all live and traced - targets. Only address-based breakpoints are tabulated. For other traps, e.g., "break on +

The breakpoints window tabulates and manipulates breakpoints among all traces, including + live targets. Only address-based breakpoints are tabulated. For other traps, e.g., "break on exception," see the Objects Window. Breakpoints can also be manipulated from address-based views, especially the disassembly listings. See Breakpoints in - the Listings. Display of breakpoints in other views, e.g., the decompiler, is not yet - implemented.

+ the Listings.

-

Individual breakpoint locations from among the targets are consolidated into logical +

Individual breakpoint locations from among the traces are consolidated into logical breakpoints, based on their addresses in the static listing. The static locations are typically stored as bookmarks in their respective Ghidra programs, comprising the current breakpoint set. See the Static Mappings window for the finer details of address mapping. A breakpoint which cannot be mapped to a static address becomes its own logical breakpoint at its dynamic address. The top table of the - provider displays logical breakpoints; the bottom table displays breakpoint locations. - NOTE: The breakpoints window cannot display or manipulate breakpoints from a target - until that target is recorded into a trace. Furthermore, dead traces are not included. Nor are - breakpoints from the past included, even when viewing past machine state.

+ provider displays logical breakpoints; the bottom table displays individual trace breakpoint + locations. NOTE: The breakpoints window cannot display or manipulate breakpoints from a + live target until that target is recorded into a trace. Only those breakpoints visible at the + current snapshot of each trace are included. For live targets, this is typically the latest + snapshot, i.e., the present.

Depending on what is supported by the connected debugger, breakpoints can trap a target when an address or range is executed, read, or written; using software or hardware mechanisms. In - case of "read" or "write," debuggers may differ in terminology, e.g., GDB might call them - "watchpoints," but Ghidra still calls these "breakpoints." Some debuggers allow the user to - specify a breakpoint location other than by address, but ultimately each specification is - realized by 0 or more addressable locations. To accommodate this, the Objects window will typically display a list of specifications, each listing its locations as children. However, - the grouping of breakpoint locations into logical breakpoints by this manager is done - without respect to the specifications. Often the specification is at a higher stratum - than Ghidra natively understands, e.g., the source filename and line number, and so such - specifications are not relevant. Note, however, that the model might not permit locations to be - manipulated independently of their specification, which may limit how the manager can operate - on each breakpoint location.

+ the grouping of breakpoint locations into logical breakpoints by Ghidra's breakpoint manager is + done without respect to the debugger's specifications. A specification may be at a + higher stratum than Ghidra natively understands, e.g., the source filename and line number, and + so such specifications are not relevant. Also note that the debugger might not permit locations + to be manipulated independently of their specifications. This may limit how Ghidra can operate, + since in that case, it must configure the specification, which may affect more locations than + intended.

+ +

When the control + mode is set to "trace" or "emulator," it is possible to rewind the trace to past snapshots + and examine old breakpoints. You may also emulate from those snapshots, even if the target is + no longer alive. By default, those historical breakpoints are disabled in the integrated + emulator, but they can be toggled in the usual ways. In addition, the locations can be + manipulated independently, since the emulator has its own breakpoint set. Emulated breakpoints + can be configured with conditions expressed in Sleigh using the Set + Condition action, or configured to replace the instruction's semantics altogether using the + Set Injection action.

Because of the logical grouping of breakpoints, it is possible for a breakpoint to be in a mixed or inconsistent state. This happens quite commonly, e.g., when a breakpoint is placed in - a Ghidra program before that program is mapped in any traced target. Once mapped in, the - location of that breakpoint in the trace is computed and noted as missing. A logical breakpoint - without any location in a trace (i.e., on an actual target) is called ineffective and - is drawn in grey, e.g.: . An enabled - logical breakpoint having a disabled location is called inconsistent and its icon will - include an exclamation mark: . A - disabled logical breakpoint having an enabled location is similarly inconsistent. Toggling an - ineffective or inconsistent logical breakpoint enables and/or places all its locations, aiming - for a consistent enabled state. Toggling it again disables all locations.

+ a Ghidra program before that program is mapped to any trace. Once mapped, the location of that + breakpoint in the trace is computed and noted as missing. A logical breakpoint without any + location in a trace is called ineffective and is drawn in grey, e.g.: . An enabled logical breakpoint having a disabled location + is called inconsistent and its icon will include an exclamation mark: . A disabled logical breakpoint having an enabled + location is similarly inconsistent. Toggling an ineffective or inconsistent logical breakpoint + enables and/or places all its mapped locations, aiming for a consistent enabled state. Toggling + it again disables all locations.

Tables and Columns

The top table, which lists logical breakpoints, has the following columns:

The bottom table, which lists breakpoint locations, has the following columns:

Breakpoint Actions

@@ -194,6 +213,124 @@

This action is always available. Use with caution! It deletes every breakpoint.

+

Set Condition (Emulator)

+ +

This action is available when all selected locations are emulated execution breakpoints. + (Conditional access breakpoints are not yet implemented.) It sets the condition using a Sleigh + expression that when true will trap the emulator. When false, the emulator continues past the + breakpoint without interruption. Sleigh operates similarly to C: zero is considered false, + while everything else is considered true. The dialog provides syntax checking, but does not + verify the semantics. If the breakpoint condition is not semantically correct, then the + breakpoint behaves as if unconditional; it will interrupt the emulator then indicate the + semantic error. To trap unconditionally (the default) use 1:1. Otherwise, use a + boolean Sleigh expression, such as RAX >= 0x1000. Sleigh conditions are rather + expressive. For example, on an x86 target, you might place a breakpoint at the entry of a + function and set the condition to (*:8 RSP) & 0xfff00000 == 0x00400000. This + will break on calls to the function from any address matching 0x004?????.

+ +

Set Injection (Emulator)

+ +

This action is available when all selected locations are emulated execution breakpoints. + (Injections on access breakpoints are not yet implemented.) It replaces the instruction's usual + Sleigh semantics with those entered into the dialog. The Sleigh syntax is the same as used in + the processor language's Sleigh specification (.slaspec and .sinc + files). The dialog provides syntax checking, but does not verify the semantics. If the + injection is not semantically correct, then the breakpoint behaves as if unconditional; it will + interrupt the emulator then indicate the semantic error. NOTE: The semantics at the + breakpoint address are completely replaced, ignoring the original instruction + entirely. This includes its control flow behavior, even fall through. The replacement semantics + must provide control flow behavior, or else the emulator's program counter will not + advance, and the same injection will be executed repeatedly. Here are three ways to provide + control flow:

+ + + +

Here are a few examples:

+ + +

Filter Actions

For organizing breakpoints the manager provides the following actions:

diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerBreakpointsPlugin/images/DebuggerBreakpointsPlugin.png b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerBreakpointsPlugin/images/DebuggerBreakpointsPlugin.png index 41ffdea30a..46fcfafe1e 100644 Binary files a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerBreakpointsPlugin/images/DebuggerBreakpointsPlugin.png and b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerBreakpointsPlugin/images/DebuggerBreakpointsPlugin.png differ diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/DebuggerControlPlugin.html b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/DebuggerControlPlugin.html index 0aa7a7ed5d..652154a25c 100644 --- a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/DebuggerControlPlugin.html +++ b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/DebuggerControlPlugin.html @@ -16,50 +16,60 @@

This plugin presents actions for controlling targets and modifying machine state. It provides a drop-down action in the main toolbar for choosing what to control for the current target: The live target, the integrated emulator, or the recorded trace. Only those control - actions suitable for the selection are displayed. Machine-state edits throughout the UI are - directed accordingly.

+ actions suitable for the selection are displayed. Machine-state edits and breakpoint commands + throughout the UI are directed accordingly.

Actions

The plugin provides several actions, but only certain ones are displayed, depending on the current mode.

-

Edit Mode

+

Control Mode

This action changes the mode for the active trace and, if applicable, its associated live target. It is always displayed, but only available when a trace is active. The possible modes are:

Target Control Actions

-

These actions are visible when the "Control Target" or "Control Target w/Edits Disabled" - mode is selected. They are available only when the current trace has an associated live target. - Commands are directed to the focused object or a suitable substitute.

+

These actions are visible when a Control Target mode is selected. They are available + only when the current trace has an associated live target. Commands are directed to the focused + object or a suitable substitute.

Resume

@@ -111,28 +121,28 @@

Trace Navigation Actions

-

These actions are visible when the "Control Trace" mode is selected. They are available when - there is an active trace.

+

These actions are visible when a Control Trace mode is selected. They are available + when there is an active trace.

Snapshot Backward

-

This navigates the trace backward one snapshot. All windows displaying machine state will - show that recorded in the current snapshot. This is available only when there exists a snapshot +

This activates the previous snapshot. All windows displaying machine state will show that + recorded in the activated snapshot. This is available only when there exists a snapshot previous to the current.

Snapshot Forward

-

This nativates the trace forward one snapshot. All windows displaying machine state will - show that recorded in the current snapshot. This is available only when there exists a snapshot - after the current.

+

This activates the next snapshot. All windows displaying machine state will show that + recorded in the activated snapshot. This is available only when there exists a snapshot after + the current.

-

Emulation Actions

+

Emulation Actions

-

These actions are visible when the "Control Emulator" mode is selected. They are available - when there is an active trace. Commands are directed to the integrated emulator for the current - trace.

+

These actions are visible when the Control Emulator mode is selected. They are + available when there is an active trace. Commands are directed to the integrated emulator for + the current trace.

Resume

@@ -141,15 +151,15 @@ Only one emulator can be run from the GUI at a time. If the current snapshot represents the live target, the emulator may read additional machine state from the live target. For non-contrived programs, the emulator will likely be interrupted, since some instructions and - all system calls are not yet supported. It could also start executing from unmapped memory or - enter a infinite loop. If it seems to carry on too long, interrupt it and examine.

+ system calls are not yet supported. It could also start executing from unmapped memory or enter + an infinite loop. If it seems to carry on too long, interrupt it and examine.

Interrupt

Interrupt the currently-running emulator. This is available when any integrated emulator is running. In most cases, this is the emulator for the current trace, but it may not be. Canceling the dialog for an emulation task will also interrupt the emulator. Upon interruption, - the emulation schedule is recorded and the snapshot displayed in the GUI.

+ the emulation schedule is noted and the snapshot displayed in the GUI.

Step Back

@@ -164,8 +174,8 @@

Steps the emulator to the next instruction, by flow. This is available when there is an active thread. At worst, this operates by repeating the current emulation schedule with one more step of the current thread. In most cases, this can use the cached emulator for the - current snapshot and advanced it a single step. Note that "Step Over" is not currently - supported by the emulator.

+ current snapshot and advance it a single step. Note that "Step Over" is not currently supported + by the emulator.

Skip Over

@@ -179,20 +189,21 @@

Recommendations

-

Write Target is the default mode, because in most cases, this is the desired behavior. When - the target dies, Write Target essentially means Read Only. For the most part, modifying the - recording itself is discouraged. There are few reasons, perhaps including 1) Hand-generating an - experimental trace; 2) Generating a trace from a script, e.g., importing an event log, - recording an emulated target; 3) Patching in state missed by the original recording. Be wary - when switching from emulation to trace mode, since you could accidentally edit scratch space in - the trace. It is allowed but can produce non-intuitive and erroneous results, since the - emulator caches its snapshots in scratch space.

+

The default mode is Control Target w/Edits Disabled, because in most cases, this is + the desired behavior. When the target dies, the mode becomes Control Trace w/Edits + Disabled. For the most part, modifying the recorded trace itself is discouraged. There are + few reasons to edit a trace, perhaps including 1) Hand-generating an experimental trace; 2) + Generating a trace from a script, e.g., importing an event log, recording an emulated target; + 3) Patching in state missed by the original recording. Be wary of Control Trace mode, + especially after emulating, since you could accidentally edit scratch space in the trace. It is + allowed but can produce non-intuitive and erroneous results, since the emulator caches its + snapshots in scratch space.

-

To prevent accidental edits to a live target, use the Control Target w/Edits Disabled mode. - This is only effective against edits from the UI, and only when all plugins and scripts use the - state editing service. This cannot prevent a component from accessing Ghidra's Target API to - modify a target. Nor can it prevent edits via the connected debugger's command-line - interpreter. The following components all use the service: To modify state in a live target, use the Control Target mode. NOTE: Some UI + components will also require you to toggle a write protector. This also affects scripts that + use the state editing service. However, disabling edits cannot prevent a script from directly + accessing Ghidra's Target API to modify a target. Nor can it prevent edits via the connected + debugger's command-line interpreter. The following components all use the service: Dynamic Listing, Memory (Dynamic Bytes), - Debugger: Model Service + Debugger: Emulation Service @@ -26,20 +26,21 @@

This action is available whenever a program is active. It will create a new trace suitable for "pure emulation" of the current program starting at the current address. More precisely, it - will open a new blank trace, initializes it with the current program's memory map, allocates a - stack, and creates a thread whose program counter is initialized to the current address and - whose registers are initialized to the register context at the current address. Optionally, any - other initialization can be done by manually modifying the trace in the UI, or using a script. - The trace and/or p-code stepping actions can then be used to emulate.

+ will open a new blank trace, initialize it with the current program's memory map, allocate a + stack, and create a thread whose program counter is initialized to the current address and + whose registers are initialized to the register context at that address. Optionally, any other + initialization can be done by manually modifying the trace in the UI, or using a script. The emulator + controls can then be used.

Add Emulated Thread

This action is available whenever a "pure emulation" trace is active. It spawns a new thread - in the current trace suitable for emulating starting at the current address. More precisely, it - allocates another stack and creates a new thread whose program counter is initialized to the - current address and whose registers are initialized to the register context at the current - address. Optionally, other registers can be initialized via the UI or a script. The new thread - is activated so that stepping actions will affect it by default.

+ in the current trace suitable for emulation starting at the current address. More precisely, it + will allocate another stack and create a new thread whose program counter is initialized to the + current address and whose registers are initialized to the register context at that address. + Optionally, other registers can be initialized via the UI or a script. The new thread is + activated so that control actions will affect it by default.

Configure Emulator

diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerModulesPlugin/DebuggerModulesPlugin.html b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerModulesPlugin/DebuggerModulesPlugin.html index 68ad1d0a5f..e02e142170 100644 --- a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerModulesPlugin/DebuggerModulesPlugin.html +++ b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerModulesPlugin/DebuggerModulesPlugin.html @@ -103,6 +103,13 @@ is a suitable mapping when the current program is loaded in the trace without relocation. It is also a fallback worth trying in the absence of a module list.

+

Map Manually

+ +

This action is always available. It simply displays the Static Mappings + window. From there, it is possible to construct the map from trace memory to static images + entirely by hand.

+

Map Modules

This action is available from the modules' or sections' pop-up menu. It searches the tool's diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerThreadsPlugin/DebuggerThreadsPlugin.html b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerThreadsPlugin/DebuggerThreadsPlugin.html index 1f6d17c630..f3d5a1cb86 100644 --- a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerThreadsPlugin/DebuggerThreadsPlugin.html +++ b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerThreadsPlugin/DebuggerThreadsPlugin.html @@ -82,103 +82,11 @@

  • Comment - a user-modifiable comment about the thread.
  • Plot - a graphical representation of the thread's lifespan. Unlike other column headers, - clicking and dragging in this one will navigate through time. To rearrange this column, hold - SHIFT while dragging.
  • + clicking and dragging in this one will navigate trace snapshots. To rearrange this column, + hold SHIFT while dragging. -

    Navigating Time

    - -

    The user can navigate through time within the current trace by using the caret in the plot - column header. There are also actions for "stepping the trace" forward and backward. See the Time window for a way to - display and navigate to specific events in the trace's timeline. Note that stepping away from - the present will prevent most windows from interacting with the live target. While some - components and scripts may interact with the target and record things "into the present," such - updates may not appear on screen until the user steps back to the present. Stepping the trace - does not affect the target, except that viewing the present requires querying the live target, - which can perturb its state.

    - -

    Step Trace - Snap Backward

    - -

    This action is available when there exists a snapshot previous to the current. It steps the - trace backward to the previous snapshot, causing most windows to display the recorded data from - the new point in time.

    - -

    Step Trace - Snap Forward

    - -

    This action is available when there exists a snapshot ahead of the current. It steps the - trace forward to the next snapshot, causing most windows to display the recorded data from the - new point in time.

    - -

    Emulate Trace - Tick Backward

    - -

    This action is available when the current point in time includes emulated steps. It steps - the trace backward to the previous tick.

    - -

    Emulate Trace - Tick Forward

    - -

    This action is available when a thread is selected. It steps the current thread forward to - the next tick, using emulation. Note that emulation does not affect the target. Furthermore, - emulation may halt early if it encounters certain instructions or causes an exception.

    - -

    Emulate - Trace Skip Tick Forward

    - -

    This action is available when a thread is selected. It steps the current thread forward by - skipping the next instruction, using emulation. Note that emulation does not affect the target. - Furthermore, emulation may halt early if it encounters certain instructions or causes an - exception. This action may be used skip subroutines; however, the stack may require - additional patching, e.g., to clean up stack parameters, depending on the calling convention. - This action does not perform those patches automatically. It only advances the program - counter. You may use Go To Time to append the require stack patch, - e.g., t0-{RSP=RSP+8}.

    - -

    Seek Trace to - Present

    - -

    This toggle is always available and is enabled by default. When first enabled, if the - current trace is live, it immediately steps the trace to the present. Furthermore, as long as - it is enabled and the current trace is live, whenever the recorder steps forward, the tool - steps with it. In most cases, this option should remain on, otherwise stepping the target will - not cause any windows to update. Toggling it off then on is a quick way to return to the - present after browsing the past.

    - -

    Go To Time

    - -

    This action is available when a trace is active. It prompts for a Time Schedule - expression. This is the same form as the expression in the title bar of the threads window. In - many cases, it is simply the snapshot number, e.g., 3, which will go to the - snapshot with key 3. It may optionally include an emulation schedule, for example, - 3:10 will use snapshot 3 for an emulator's initial state and step 10 machine - instructions on snapshot 3's event thread. If the snapshot does not give an event thread, then - the thread must be specified in the expression, e.g., 3:t1-10. That expression - will start at snapshot 3, get the thread with key 1, and step it 10 machine instructions. The - stepping commands can be repeated any number of times, separated by semicolons, to step threads - in a specified sequence, e.g., 3:t1-10;t2-5 will do the same as before, then get - thread 2 and step it 5 times.

    - -

    The emulator's state can also be modified by the schedule. Instead of specifying a number of - steps, write a Sleigh statement, e.g., 3:t1-{r0=0x1234};10. This will start - at snapshot 3, patch thread 1's r0 to 0x1234, then step 10 instructions. Like stepping - commands, the thread may be omitted for Sleigh commands. Each command without a thread - specified implicitly uses the one from the previous command, or in the case of the first - command, the event thread. Only one Sleigh statement is permitted per command.

    - -

    A second command sequence may be appended, following a dot, to command the emulator at the - level of p-code operations as well. This is particularly useful when debugging a processor - specification. See also the P-code Stepper - window. For example, 3:2.10 will start at snapshot 3 and step the event thread 2 - machine instructions, followed by 10 p-code operations. The same thread-by-thread sequencing - and state patching commands are allowed in the p-code command sequence. The entire - instruction sequence precedes the entire p-code sequence, i.e., only a single dot is allowed. - Once the expression enters p-code mode, it cannot re-enter instruction mode.

    - -

    Other Actions

    +

    Actions

    Synchronize Trace and Target Focus

    diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerThreadsPlugin/images/DebuggerThreadsPlugin.png b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerThreadsPlugin/images/DebuggerThreadsPlugin.png index cd304800d4..6adc910b1a 100644 Binary files a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerThreadsPlugin/images/DebuggerThreadsPlugin.png and b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerThreadsPlugin/images/DebuggerThreadsPlugin.png differ diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerTimePlugin/DebuggerTimePlugin.html b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerTimePlugin/DebuggerTimePlugin.html index fea1e5b359..c3f81835a9 100644 --- a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerTimePlugin/DebuggerTimePlugin.html +++ b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerTimePlugin/DebuggerTimePlugin.html @@ -48,8 +48,7 @@ snapshot. This always applies to "scratch" snapshots produced by emulation, but may also apply if the stepping schedule between recorded events is somehow known. Typically, it is just the number of steps of the source snapshot's event thread; however, the notation does - allow other threads to be stepped, too. See the Go To Time + allow other threads to be stepped, too. See the Go To Time action.
  • Description - a user-modifiable description of the snapshot or event. This defaults to @@ -65,6 +64,38 @@ snapshot. This is a shortcut to modifying the description in the time table, but can be accessed outside of the time window.

    +

    Go To Time

    + +

    This action is available when a trace is active. It prompts for a Time Schedule + expression. This is the same form as the expression in the sub-title of the Threads window. In many + cases, it is simply the snapshot number, e.g., 3, which will go to the snapshot + with key 3. It may optionally include an emulation schedule, for example, 3:10 + will use snapshot 3 for an emulator's initial state and step 10 machine instructions on + snapshot 3's event thread. If the snapshot does not give an event thread, then the thread must + be specified in the expression, e.g., 3:t1-10. That expression will start at + snapshot 3, get the thread with key 1, and step it 10 machine instructions. The stepping + commands can be repeated any number of times, separated by semicolons, to step threads in a + specified sequence, e.g., 3:t1-10;t2-5 will do the same as before, then get thread + 2 and step it 5 times.

    + +

    The emulator's state can also be modified by the schedule. Instead of specifying a number of + steps, write a Sleigh statement, e.g., 3:t1-{r0=0x1234};10. This will start + at snapshot 3, patch thread 1's r0 to 0x1234, then step 10 instructions. Like stepping + commands, the thread may be omitted for Sleigh commands. Each command without a thread + specified implicitly uses the one from the previous command, or in the case of the first + command, the event thread. Only one Sleigh statement is permitted per command.

    + +

    A second command sequence may be appended, following a dot, to command the emulator at the + level of p-code operations as well. This is particularly useful when debugging a processor + specification. See also the P-code Stepper + window. For example, 3:2.10 will start at snapshot 3 and step the event thread 2 + machine instructions then 10 p-code operations. The same thread-by-thread sequencing and state + patching commands are allowed in the p-code command sequence. The entire instruction + sequence precedes the entire p-code sequence, i.e., only a single dot is allowed. Once the + expression enters p-code mode, it cannot re-enter instruction mode.

    +

    Hide Scratch

    This toggle action is always available in the drop-down actions of the Time window. It is diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerTraceViewDiffPlugin/DebuggerTraceViewDiffPlugin.html b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerTraceViewDiffPlugin/DebuggerTraceViewDiffPlugin.html index 4afee181b0..a8e988312f 100644 --- a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerTraceViewDiffPlugin/DebuggerTraceViewDiffPlugin.html +++ b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerTraceViewDiffPlugin/DebuggerTraceViewDiffPlugin.html @@ -111,8 +111,8 @@ Remember, the variable's storage should encode its value.

    Optionally, the specified time may also include emulation. See the Go To Time action - for the syntax of the Time Schedule expression. For simple schedules, the step buttons + "help/topics/DebuggerTimePlugin/DebuggerTimePlugin.html#goto_time">Go To Time action for + the syntax of the Time Schedule expression. For simple schedules, the step buttons provide convenient forward and backward changes to the emulation schedule. Perhaps the most common use of this is to see what changes from executing an isolated block of code. Ideally, the baseline is a relatively complete capture or represents the present in a live session, so @@ -123,10 +123,10 @@ interesting block of code.

  • Keeping the target alive, use the Emulate - Forward and/or Go To Time - actions to reach the end of the interesting block.
  • + "help/topics/DebuggerControlPlugin/DebuggerControlPlugin.html#emu_actions">Emulator + Control and/or Go To Time actions to + reach the end of the interesting block.
  • Use this Compare action and select the baseline snapshot.
  • diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/event/TraceActivatedPluginEvent.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/event/TraceActivatedPluginEvent.java index b09100f4fe..642b4fbc66 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/event/TraceActivatedPluginEvent.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/event/TraceActivatedPluginEvent.java @@ -16,19 +16,27 @@ package ghidra.app.plugin.core.debug.event; import ghidra.app.plugin.core.debug.DebuggerCoordinates; +import ghidra.app.services.DebuggerTraceManagerService.ActivationCause; import ghidra.framework.plugintool.PluginEvent; public class TraceActivatedPluginEvent extends PluginEvent { + static final String NAME = "Trace Location"; private final DebuggerCoordinates coordinates; + private final ActivationCause cause; - public TraceActivatedPluginEvent(String source, DebuggerCoordinates coordinates) { + public TraceActivatedPluginEvent(String source, DebuggerCoordinates coordinates, ActivationCause cause) { super(source, NAME); this.coordinates = coordinates; + this.cause = cause; } public DebuggerCoordinates getActiveCoordinates() { return coordinates; } + + public ActivationCause getCause() { + return cause; + } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/event/TraceInactiveCoordinatesPluginEvent.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/event/TraceInactiveCoordinatesPluginEvent.java new file mode 100644 index 0000000000..e17cf03e3d --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/event/TraceInactiveCoordinatesPluginEvent.java @@ -0,0 +1,34 @@ +/* ### + * 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.event; + +import ghidra.app.plugin.core.debug.DebuggerCoordinates; +import ghidra.framework.plugintool.PluginEvent; + +public class TraceInactiveCoordinatesPluginEvent extends PluginEvent { + static final String NAME = "Trace Inactive Location"; + + private final DebuggerCoordinates coordinates; + + public TraceInactiveCoordinatesPluginEvent(String source, DebuggerCoordinates coordinates) { + super(source, NAME); + this.coordinates = coordinates; + } + + public DebuggerCoordinates getCoordinates() { + return coordinates; + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java index 278a317c25..6b4b22e291 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java @@ -174,17 +174,6 @@ public interface DebuggerResources { Icon ICON_DIFF_PREV = new GIcon("icon.debugger.diff.previous"); Icon ICON_DIFF_NEXT = new GIcon("icon.debugger.diff.next"); - Icon ICON_EDIT_MODE_READ_ONLY = new GIcon("icon.debugger.edit.mode.read.only"); - Icon ICON_EDIT_MODE_WRITE_TARGET = new GIcon("icon.debugger.edit.mode.write.target"); - Icon ICON_EDIT_MODE_WRITE_TRACE = new GIcon("icon.debugger.edit.mode.write.trace"); - Icon ICON_EDIT_MODE_WRITE_EMULATOR = - new GIcon("icon.debugger.edit.mode.write.emulator"); - - String NAME_EDIT_MODE_READ_ONLY = "Control Target w/ Edits Disabled"; - String NAME_EDIT_MODE_WRITE_TARGET = "Control Target"; - String NAME_EDIT_MODE_WRITE_TRACE = "Control Trace"; - String NAME_EDIT_MODE_WRITE_EMULATOR = "Control Emulator"; - HelpLocation HELP_PACKAGE = new HelpLocation("Debugger", "package"); String HELP_ANCHOR_PLUGIN = "plugin"; @@ -419,7 +408,7 @@ public interface DebuggerResources { public AbstractConnectAction(Plugin owner) { super(NAME, owner.getName()); - setDescription("Create a new connection to an debugging agent"); + setDescription("Create a new connection to a debugging agent"); setHelpLocation(new HelpLocation(owner.getName(), HELP_ANCHOR)); } } @@ -1660,23 +1649,6 @@ public interface DebuggerResources { } } - interface SeekTracePresentAction { - String NAME = "Seek Trace Present"; - String DESCRIPTION = "Track the tool to the latest snap"; - Icon ICON = ICON_SEEK_PRESENT; - String GROUP = "zz"; - String HELP_ANCHOR = "seek_trace_present"; - - static ToggleActionBuilder builder(Plugin owner) { - String ownerName = owner.getName(); - return new ToggleActionBuilder(NAME, ownerName) - .description(DESCRIPTION) - .toolBarIcon(ICON) - .toolBarGroup(GROUP) - .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); - } - } - // TODO: Perhaps to reduce overloading of "snapshot" we should use "event" instead? interface RenameSnapshotAction { String NAME = "Rename Current Snapshot"; @@ -1712,22 +1684,6 @@ public interface DebuggerResources { } } - interface GoToTimeAction { - String NAME = "Go To Time"; - String DESCRIPTION = "Go to a specific time, optionally using emulation"; - Icon ICON = ICON_TIME; - String HELP_ANCHOR = "goto_time"; - - static ActionBuilder builder(Plugin owner) { - String ownerName = owner.getName(); - return new ActionBuilder(NAME, ownerName).description(DESCRIPTION) - .menuPath(NAME) - .menuIcon(ICON) - .keyBinding("CTRL SHIFT T") - .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); - } - } - interface SaveByDefaultAction { String NAME = "Save Traces By Default"; String DESCRIPTION = "Automatically save traces to the project"; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/AbstractDebuggerSleighInputDialog.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/AbstractDebuggerSleighInputDialog.java new file mode 100644 index 0000000000..29c281b12d --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/AbstractDebuggerSleighInputDialog.java @@ -0,0 +1,140 @@ +/* ### + * 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.gui.breakpoint; + +import java.awt.BorderLayout; +import java.awt.Color; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import javax.swing.text.*; + +import docking.DialogComponentProvider; +import generic.theme.GColor; +import generic.theme.Gui; +import ghidra.framework.plugintool.PluginTool; +import ghidra.pcode.exec.SleighUtils.SleighParseError; +import ghidra.pcode.exec.SleighUtils.SleighParseErrorEntry; +import ghidra.util.*; + +public abstract class AbstractDebuggerSleighInputDialog extends DialogComponentProvider { + protected static final Color COLOR_ERROR = new GColor("color.fg.error"); + protected static final AttributeSet RED_UNDERLINE; + + static { + MutableAttributeSet attributes = new SimpleAttributeSet(); + StyleConstants.setForeground(attributes, COLOR_ERROR); + StyleConstants.setUnderline(attributes, true); + RED_UNDERLINE = attributes; + } + + protected final JPanel panel; + protected final JLabel label; + protected final StyledDocument docInput; + protected final JTextPane textInput; + private JScrollPane spInput; + protected boolean isValid = false; + + static class SleighTextPane extends JTextPane { + public SleighTextPane(StyledDocument document) { + super(document); + Gui.registerFont(this, "font.debugger.sleigh"); + } + } + + protected AbstractDebuggerSleighInputDialog(String title, String prompt) { + super(title, true, true, true, false); + panel = new JPanel(new BorderLayout()); + panel.setBorder(new EmptyBorder(16, 16, 16, 16)); + label = new JLabel(prompt); + label.getMaximumSize().width = 400; + panel.add(label, BorderLayout.NORTH); + + docInput = new DefaultStyledDocument(); + textInput = new SleighTextPane(docInput); + spInput = new JScrollPane(textInput); + spInput.getMaximumSize().height = 300; + panel.add(spInput); + + addWorkPanel(panel); + + addOKButton(); + addCancelButton(); + } + + public String prompt(PluginTool tool, String defaultInput) { + setStatusText(""); + textInput.setText(defaultInput); + validateAndMarkup(); + Swing.runLater(() -> repack()); + tool.showDialog(this); + if (isValid) { + return getInput(); + } + return null; + } + + public String getInput() { + return textInput.getText(); + } + + protected abstract void validate(); + + protected void clearAttributes() { + docInput.setCharacterAttributes(0, docInput.getLength() + 1, SimpleAttributeSet.EMPTY, + true); + } + + protected void addErrorAttribute(int start, int stop) { + int length = stop - start + 1; + docInput.setCharacterAttributes(start, length, RED_UNDERLINE, true); + } + + protected void validateAndMarkup() { + isValid = false; + clearAttributes(); + try { + validate(); + isValid = true; + } + catch (SleighParseError e) { + setStatusText("
    " + HTMLUtilities.escapeHTML(e.getMessage()) + "
    ", + MessageType.ERROR, false); + Swing.runLater(() -> { + if (spInput.getPreferredSize().height > spInput.getSize().height) { + repack(); + } + }); + for (SleighParseErrorEntry error : e.getErrors()) { + addErrorAttribute(error.start(), error.stop()); + } + } + } + + @Override + protected void okCallback() { + validateAndMarkup(); + if (isValid) { + close(); + } + } + + @Override + protected void cancelCallback() { + isValid = false; + close(); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/BreakpointLocationRow.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/BreakpointLocationRow.java index d9e7758e17..2d0fa3bb57 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/BreakpointLocationRow.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/BreakpointLocationRow.java @@ -20,7 +20,7 @@ import java.util.stream.Collectors; import ghidra.app.services.LogicalBreakpoint; import ghidra.app.services.LogicalBreakpoint.State; -import ghidra.app.services.TraceRecorder; +import ghidra.pcode.exec.SleighUtils; import ghidra.program.model.address.Address; import ghidra.trace.model.breakpoint.TraceBreakpoint; import ghidra.trace.model.thread.TraceThread; @@ -40,8 +40,8 @@ public class BreakpointLocationRow { } public boolean isEnabled() { - TraceRecorder recorder = provider.modelService.getRecorder(loc.getTrace()); - return recorder != null && loc.isEnabled(recorder.getSnap()); + long snap = provider.traceManager.getCurrentFor(loc.getTrace()).getSnap(); + return loc.isEnabled(snap); } public State getState() { @@ -106,6 +106,10 @@ public class BreakpointLocationRow { } } + public boolean hasSleigh() { + return !SleighUtils.UNCONDITIONAL_BREAK.equals(loc.getEmuSleigh()); + } + public TraceBreakpoint getTraceBreakpoint() { return loc; } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointMarkerPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointMarkerPlugin.java index a80b230451..78a7280986 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointMarkerPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointMarkerPlugin.java @@ -51,7 +51,8 @@ import ghidra.framework.options.annotation.*; import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.annotation.AutoServiceConsumed; import ghidra.framework.plugintool.util.PluginStatus; -import ghidra.program.model.address.*; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressRange; import ghidra.program.model.listing.*; import ghidra.program.model.pcode.PcodeOp; import ghidra.program.model.pcode.Varnode; @@ -576,17 +577,11 @@ public class DebuggerBreakpointMarkerPlugin extends Plugin return false; } ProgramLocation loc = getSingleLocationFromContext(context); - if (!(loc.getProgram() instanceof TraceProgramView)) { + if (!(loc.getProgram() instanceof TraceProgramView view)) { return true; } - TraceRecorder recorder = getRecorderFromContext(context); - if (recorder == null) { - return false; - } - if (!recorder.getSupportedBreakpointKinds().containsAll(kinds)) { - return false; - } - return true; + Set supported = getSupportedKindsFromTrace(view.getTrace()); + return supported.containsAll(kinds); } } @@ -720,6 +715,8 @@ public class DebuggerBreakpointMarkerPlugin extends Plugin private DebuggerTraceManagerService traceManager; @AutoServiceConsumed private DebuggerConsoleService consoleService; + @AutoServiceConsumed + private DebuggerStateEditingService editingService; // @AutoServiceConsumed via method DecompilerMarginService decompilerMarginService; @SuppressWarnings("unused") @@ -830,66 +827,67 @@ public class DebuggerBreakpointMarkerPlugin extends Plugin } } - protected TraceRecorder getRecorderFromContext(ActionContext context) { - if (modelService == null) { - return null; - } - Trace trace = getTraceFromContext(context); - return modelService.getRecorder(trace); - } - - protected Set getRecordersFromContext(ActionContext context) { - TraceRecorder single = getRecorderFromContext(context); + protected Set getTracesFromContext(ActionContext context) { + Trace single = getTraceFromContext(context); if (single != null) { return Set.of(single); } - if (mappingService == null || modelService == null) { + if (mappingService == null) { return Set.of(); } ProgramLocation loc = getSingleLocationFromContext(context); - if (loc == null || loc.getProgram() instanceof TraceProgramView) { + assert !(loc.getProgram() instanceof TraceProgramView); + if (loc == null) { return Set.of(); } Set mappedLocs = mappingService.getOpenMappedLocations(loc); if (mappedLocs == null || mappedLocs.isEmpty()) { return Set.of(); } - Set result = new HashSet<>(); + Set result = new HashSet<>(); for (TraceLocation tloc : mappedLocs) { - TraceRecorder rec = modelService.getRecorder(tloc.getTrace()); - if (rec != null) { - result.add(rec); - } + result.add(tloc.getTrace()); } return result; } - protected boolean contextHasRecorder(ActionContext ctx) { - return getRecorderFromContext(ctx) != null; - } - protected boolean contextCanManipulateBreakpoints(ActionContext ctx) { - if (breakpointService == null) { - return false; - } - if (!contextHasLocation(ctx)) { - return false; - } - // Programs, or live traces, but not dead traces - if (contextHasTrace(ctx) && !contextHasRecorder(ctx)) { + if (breakpointService == null || !contextHasLocation(ctx)) { return false; } return true; } - protected Set getSupportedKindsFromContext(ActionContext context) { - Set recorders = getRecordersFromContext(context); - if (recorders.isEmpty()) { + protected Set getSupportedKindsFromTrace(Trace trace) { + StateEditingMode mode = editingService == null ? StateEditingMode.DEFAULT + : editingService.getCurrentMode(trace); + if (mode.useEmulatedBreakpoints()) { return EnumSet.allOf(TraceBreakpointKind.class); } - return recorders.stream() - .flatMap(rec -> rec.getSupportedBreakpointKinds().stream()) - .collect(Collectors.toSet()); + if (modelService == null) { + return Set.of(); + } + TraceRecorder recorder = modelService.getRecorder(trace); + if (recorder == null) { + return Set.of(); + } + return recorder.getSupportedBreakpointKinds(); + } + + protected Set getSupportedKindsFromContext(ActionContext context) { + Set traces = getTracesFromContext(context); + if (traces.isEmpty()) { + return EnumSet.allOf(TraceBreakpointKind.class); + } + Set result = new HashSet<>(); + for (Trace t : traces) { + result.addAll(getSupportedKindsFromTrace(t)); + if (result.size() == TraceBreakpointKind.COUNT) { + // Short circuit if saturated + return result; + } + } + return result; } protected void doToggleBreakpointsAt(String title, ActionContext context) { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointsProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointsProvider.java index d20079907e..c89fa5abbd 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointsProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointsProvider.java @@ -28,31 +28,36 @@ import javax.swing.table.TableColumnModel; import docking.ActionContext; import docking.WindowPosition; import docking.action.*; +import docking.action.builder.ActionBuilder; import docking.widgets.table.*; import docking.widgets.table.DefaultEnumeratedColumnTableModel.EnumeratedTableColumn; +import ghidra.app.context.ProgramLocationActionContext; +import ghidra.app.plugin.core.debug.DebuggerCoordinates; import ghidra.app.plugin.core.debug.DebuggerPluginPackage; import ghidra.app.plugin.core.debug.gui.DebuggerResources; import ghidra.app.plugin.core.debug.gui.DebuggerResources.*; import ghidra.app.services.*; +import ghidra.app.services.DebuggerStateEditingService.StateEditingModeChangeListener; import ghidra.app.services.LogicalBreakpoint.State; import ghidra.framework.model.DomainObject; -import ghidra.framework.plugintool.AutoService; -import ghidra.framework.plugintool.ComponentProviderAdapter; +import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.annotation.AutoServiceConsumed; +import ghidra.pcode.exec.SleighUtils; import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressRange; +import ghidra.program.util.MarkerLocation; import ghidra.program.util.ProgramLocation; import ghidra.trace.model.*; import ghidra.trace.model.Trace.TraceBreakpointChangeType; import ghidra.trace.model.breakpoint.TraceBreakpoint; +import ghidra.trace.model.breakpoint.TraceBreakpointKind; import ghidra.util.*; import ghidra.util.database.ObjectKey; -import ghidra.util.datastruct.CollectionChangeListener; import ghidra.util.table.GhidraTable; import ghidra.util.table.GhidraTableFilterPanel; public class DebuggerBreakpointsProvider extends ComponentProviderAdapter - implements LogicalBreakpointsChangeListener { + implements LogicalBreakpointsChangeListener, StateEditingModeChangeListener { protected enum LogicalBreakpointTableColumns implements EnumeratedTableColumn { @@ -63,7 +68,8 @@ public class DebuggerBreakpointsProvider extends ComponentProviderAdapter IMAGE("Image", String.class, LogicalBreakpointRow::getImageName, true), LENGTH("Length", Long.class, LogicalBreakpointRow::getLength, true), KINDS("Kinds", String.class, LogicalBreakpointRow::getKinds, true), - LOCATIONS("Locations", Integer.class, LogicalBreakpointRow::getLocationCount, true); + LOCATIONS("Locations", Integer.class, LogicalBreakpointRow::getLocationCount, true), + SLEIGH("Sleigh", Boolean.class, LogicalBreakpointRow::hasSleigh, true); private final String header; private final Class cls; @@ -132,7 +138,8 @@ public class DebuggerBreakpointsProvider extends ComponentProviderAdapter public LogicalBreakpointTableModel(DebuggerBreakpointsProvider provider) { super(provider.getTool(), "Breakpoints", LogicalBreakpointTableColumns.class, lb -> lb, - lb -> new LogicalBreakpointRow(provider, lb)); + lb -> new LogicalBreakpointRow(provider, lb), + LogicalBreakpointRow::getLogicalBreakpoint); } @Override @@ -149,7 +156,8 @@ public class DebuggerBreakpointsProvider extends ComponentProviderAdapter ADDRESS("Address", Address.class, BreakpointLocationRow::getAddress, true), TRACE("Trace", String.class, BreakpointLocationRow::getTraceName, true), THREADS("Threads", String.class, BreakpointLocationRow::getThreads, true), - COMMENT("Comment", String.class, BreakpointLocationRow::getComment, BreakpointLocationRow::setComment, true); + COMMENT("Comment", String.class, BreakpointLocationRow::getComment, BreakpointLocationRow::setComment, true), + SLEIGH("Sleigh", Boolean.class, BreakpointLocationRow::hasSleigh, true); private final String header; private final Function getter; @@ -210,7 +218,8 @@ public class DebuggerBreakpointsProvider extends ComponentProviderAdapter public BreakpointLocationTableModel(DebuggerBreakpointsProvider provider) { super(provider.getTool(), "Locations", BreakpointLocationTableColumns.class, - TraceBreakpoint::getObjectKey, loc -> new BreakpointLocationRow(provider, loc)); + TraceBreakpoint::getObjectKey, loc -> new BreakpointLocationRow(provider, loc), + BreakpointLocationRow::getTraceBreakpoint); } @Override @@ -525,6 +534,38 @@ public class DebuggerBreakpointsProvider extends ComponentProviderAdapter } } + interface SetEmulatedBreakpointConditionAction { + String NAME = "Set Condition (Emulator)"; + String DESCRIPTION = "Set a Sleigh condition for this emulated breakpoint"; + String GROUP = DebuggerResources.GROUP_BREAKPOINTS; + String HELP_ANCHOR = "set_condition"; + + static ActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new ActionBuilder(NAME, ownerName) + .description(DESCRIPTION) + .popupMenuPath(NAME) + .popupMenuGroup(GROUP) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); + } + } + + interface SetEmulatedBreakpointInjectionAction { + String NAME = "Set Injection (Emulator)"; + String DESCRIPTION = "Set a Sleigh injection for this emulated breakpoint"; + String GROUP = DebuggerResources.GROUP_BREAKPOINTS; + String HELP_ANCHOR = "set_injection"; + + static ActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new ActionBuilder(NAME, ownerName) + .description(DESCRIPTION) + .popupMenuPath(NAME) + .popupMenuGroup(GROUP) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); + } + } + class LocationsBySelectedBreakpointsTableFilter implements TableFilter { @Override public boolean acceptsRow(BreakpointLocationRow locationRow) { @@ -555,28 +596,11 @@ public class DebuggerBreakpointsProvider extends ComponentProviderAdapter } } - protected class TrackRecordersListener implements CollectionChangeListener { - @Override - public void elementAdded(TraceRecorder element) { - Swing.runIfSwingOrRunLater(() -> traceRecordingStarted(element)); - } - - @Override - public void elementRemoved(TraceRecorder element) { - Swing.runIfSwingOrRunLater(() -> traceRecordingStopped(element)); - } - } - protected class ForBreakpointLocationsTraceListener extends TraceDomainObjectListener { - private final TraceRecorder recorder; private final Trace trace; - public ForBreakpointLocationsTraceListener(TraceRecorder recorder) { - // TODO: What if recorder advances past a trace breakpoint? - // Tends never to happen during recording, since upper is unbounded - // (Same in LogicalBreak service) - this.recorder = recorder; - this.trace = recorder.getTrace(); + public ForBreakpointLocationsTraceListener(Trace trace) { + this.trace = trace; listenForUntyped(DomainObject.DO_OBJECT_RESTORED, e -> objectRestored()); listenFor(TraceBreakpointChangeType.ADDED, this::locationAdded); listenFor(TraceBreakpointChangeType.CHANGED, this::locationChanged); @@ -587,22 +611,23 @@ public class DebuggerBreakpointsProvider extends ComponentProviderAdapter } private void objectRestored() { - reloadBreakpointLocations(recorder); + reloadBreakpointLocations(trace); } - private boolean isLive(TraceBreakpoint location) { - return location.getLifespan().contains(recorder.getSnap()); + private boolean isVisible(TraceBreakpoint location) { + long snap = traceManager.getCurrentFor(trace).getSnap(); + return location.getLifespan().contains(snap); } private void locationAdded(TraceBreakpoint location) { - if (!isLive(location)) { + if (!isVisible(location)) { return; } breakpointLocationAdded(location); } private void locationChanged(TraceBreakpoint location) { - if (!isLive(location)) { + if (!isVisible(location)) { return; } breakpointLocationUpdated(location); @@ -610,8 +635,9 @@ public class DebuggerBreakpointsProvider extends ComponentProviderAdapter private void locationLifespanChanged(TraceBreakpoint location, Lifespan oldSpan, Lifespan newSpan) { - boolean isLiveOld = oldSpan.contains(recorder.getSnap()); - boolean isLiveNew = newSpan.contains(recorder.getSnap()); + long snap = traceManager.getCurrentFor(trace).getSnap(); + boolean isLiveOld = oldSpan.contains(snap); + boolean isLiveNew = newSpan.contains(snap); if (isLiveOld == isLiveNew) { return; } @@ -624,7 +650,7 @@ public class DebuggerBreakpointsProvider extends ComponentProviderAdapter } private void locationDeleted(TraceBreakpoint location) { - if (!isLive(location)) { + if (!isVisible(location)) { return; } breakpointLocationRemoved(location); @@ -639,20 +665,19 @@ public class DebuggerBreakpointsProvider extends ComponentProviderAdapter // @AutoServiceConsumed via method DebuggerLogicalBreakpointService breakpointService; - // @AutoServiceConsumed via method, package access for BreakpointLogicalRow - DebuggerModelService modelService; @AutoServiceConsumed private DebuggerListingService listingService; @AutoServiceConsumed - private DebuggerTraceManagerService traceManager; + DebuggerTraceManagerService traceManager; @AutoServiceConsumed private DebuggerConsoleService consoleService; + // @AutoServiceConsumed via method + private DebuggerStateEditingService editingService; @AutoServiceConsumed private GoToService goToService; @SuppressWarnings("unused") private AutoService.Wiring autoServiceWiring; - private final TrackRecordersListener recorderListener = new TrackRecordersListener(); private final Map listenersByTrace = new HashMap<>(); @@ -686,6 +711,8 @@ public class DebuggerBreakpointsProvider extends ComponentProviderAdapter MakeBreakpointsEffectiveResolutionAction actionMakeBreakpointsEffectiveResolution; ToggleDockingAction actionFilterByCurrentTrace; ToggleDockingAction actionFilterLocationsByBreakpoints; + DockingAction actionSetCondition; + DockingAction actionSetInjection; public DebuggerBreakpointsProvider(final DebuggerBreakpointsPlugin plugin) { super(plugin.getTool(), DebuggerResources.TITLE_PROVIDER_BREAKPOINTS, plugin.getName()); @@ -747,17 +774,6 @@ public class DebuggerBreakpointsProvider extends ComponentProviderAdapter contextChanged(); } - @AutoServiceConsumed - private void setModelService(DebuggerModelService modelService) { - if (this.modelService != null) { - this.modelService.removeTraceRecordersChangedListener(recorderListener); - } - this.modelService = modelService; - if (this.modelService != null) { - this.modelService.addTraceRecordersChangedListener(recorderListener); - } - } - @AutoServiceConsumed private void setConsoleService(DebuggerConsoleService consoleService) { if (consoleService != null) { @@ -767,6 +783,25 @@ public class DebuggerBreakpointsProvider extends ComponentProviderAdapter } } + @AutoServiceConsumed + private void setEditingService(DebuggerStateEditingService editingService) { + if (this.editingService != null) { + this.editingService.removeModeChangeListener(this); + } + this.editingService = editingService; + if (this.editingService != null) { + this.editingService.addModeChangeListener(this); + } + } + + @Override + public void modeChanged(Trace trace, StateEditingMode mode) { + Swing.runIfSwingOrRunLater(() -> { + reloadBreakpointLocations(trace); + contextChanged(); + }); + } + protected void loadBreakpoints() { Set all = breakpointService.getAllBreakpoints(); breakpointTableModel.addAllItems(all); @@ -824,22 +859,41 @@ public class DebuggerBreakpointsProvider extends ComponentProviderAdapter }); } - private void loadBreakpointLocations(TraceRecorder recorder) { - Trace trace = recorder.getTrace(); - for (AddressRange range : trace.getBaseAddressFactory().getAddressSet()) { - locationTableModel.addAllItems(trace.getBreakpointManager() - .getBreakpointsIntersecting(Lifespan.at(recorder.getSnap()), range)); + private void loadBreakpointLocations(Trace trace) { + StateEditingMode mode = editingService == null + ? StateEditingMode.DEFAULT + : editingService.getCurrentMode(trace); + DebuggerCoordinates currentFor = traceManager.getCurrentFor(trace); + TraceRecorder recorder = currentFor.getRecorder(); + if (!mode.useEmulatedBreakpoints() && recorder == null) { + return; } + Lifespan span = Lifespan.at(currentFor.getSnap()); + Collection visible = new ArrayList<>(); + for (AddressRange range : trace.getBaseAddressFactory().getAddressSet()) { + Collection breaks = trace.getBreakpointManager() + .getBreakpointsIntersecting(span, range); + if (mode.useEmulatedBreakpoints()) { + visible.addAll(breaks); + } + else { + for (TraceBreakpoint l : breaks) { + if (recorder.getTargetBreakpoint(l) != null) { + visible.add(l); + } + } + } + } + locationTableModel.addAllItems(visible); } private void unloadBreakpointLocations(Trace trace) { - locationTableModel.deleteWith(r -> r.getTraceBreakpoint().getTrace() == trace); + locationTableModel.deleteItemsWith(l -> l.getTrace() == trace); } - private void reloadBreakpointLocations(TraceRecorder recorder) { - Trace trace = recorder.getTrace(); + private void reloadBreakpointLocations(Trace trace) { unloadBreakpointLocations(trace); - loadBreakpointLocations(recorder); + loadBreakpointLocations(trace); } private void breakpointLocationAdded(TraceBreakpoint location) { @@ -858,13 +912,13 @@ public class DebuggerBreakpointsProvider extends ComponentProviderAdapter locationTableModel.deleteItem(location); } - private void doTrackTrace(Trace trace, TraceRecorder recorder) { + private void doTrackTrace(Trace trace) { if (listenersByTrace.containsKey(trace)) { Msg.warn(this, "Already tracking trace breakpoints"); return; } - listenersByTrace.put(trace, new ForBreakpointLocationsTraceListener(recorder)); - loadBreakpointLocations(recorder); + listenersByTrace.put(trace, new ForBreakpointLocationsTraceListener(trace)); + loadBreakpointLocations(trace); } private void doUntrackTrace(Trace trace) { @@ -875,24 +929,8 @@ public class DebuggerBreakpointsProvider extends ComponentProviderAdapter } } - private void traceRecordingStarted(TraceRecorder recorder) { - Trace trace = recorder.getTrace(); - if (!traceManager.getOpenTraces().contains(trace)) { - return; - } - doTrackTrace(trace, recorder); - } - - private void traceRecordingStopped(TraceRecorder recorder) { - doUntrackTrace(recorder.getTrace()); - } - protected void traceOpened(Trace trace) { - TraceRecorder recorder = modelService.getRecorder(trace); - if (recorder == null) { - return; - } - doTrackTrace(trace, recorder); + doTrackTrace(trace); } protected void traceClosed(Trace trace) { @@ -1031,6 +1069,10 @@ public class DebuggerBreakpointsProvider extends ComponentProviderAdapter TableColumn locsCol = bptColModel.getColumn(LogicalBreakpointTableColumns.LOCATIONS.ordinal()); locsCol.setPreferredWidth(20); + TableColumn bptSleighCol = + bptColModel.getColumn(LogicalBreakpointTableColumns.SLEIGH.ordinal()); + bptSleighCol.setMaxWidth(24); + bptSleighCol.setMinWidth(24); GTableColumnModel locColModel = (GTableColumnModel) locationTable.getColumnModel(); TableColumn locEnCol = @@ -1049,7 +1091,13 @@ public class DebuggerBreakpointsProvider extends ComponentProviderAdapter locAddrCol.setCellRenderer(CustomToStringCellRenderer.MONO_OBJECT); TableColumn locThreadsCol = locColModel.getColumn(BreakpointLocationTableColumns.THREADS.ordinal()); + TableColumn locSleighCol = + locColModel.getColumn(BreakpointLocationTableColumns.SLEIGH.ordinal()); + locSleighCol.setMaxWidth(24); + locSleighCol.setMinWidth(24); + locColModel.setVisible(locThreadsCol, false); + locColModel.setVisible(locSleighCol, false); } protected void navigateToSelectedBreakpoint() { @@ -1122,9 +1170,171 @@ public class DebuggerBreakpointsProvider extends ComponentProviderAdapter .onAction(this::toggledFilterLocationsByBreakpoints) .buildAndInstallLocal(this); + actionSetCondition = SetEmulatedBreakpointConditionAction.builder(plugin) + .popupWhen(this::isPopupSetCondition) + .onAction(this::activatedSetCondition) + .buildAndInstall(tool); + actionSetInjection = SetEmulatedBreakpointInjectionAction.builder(plugin) + .popupWhen(this::isPopupSetInjection) + .onAction(this::activatedSetInjection) + .buildAndInstall(tool); + actionMakeBreakpointsEffectiveResolution = new MakeBreakpointsEffectiveResolutionAction(); } + private Collection getLogicalBreakpoints(ActionContext ctx) { + if (ctx instanceof DebuggerLogicalBreakpointsActionContext lbCtx) { + return lbCtx.getBreakpoints(); + } + if (ctx instanceof ProgramLocationActionContext locCtx) { + return breakpointService.getBreakpointsAt(locCtx.getLocation()); + } + if (ctx.getContextObject() instanceof MarkerLocation ml) { + return breakpointService + .getBreakpointsAt(new ProgramLocation(ml.getProgram(), ml.getAddr())); + } + return null; + } + + private boolean isAllInvolvedTracesUsingEmulatedBreakpoints(ActionContext ctx) { + if (editingService == null) { + return false; + } + Set traces = new HashSet<>(); + Collection breakpoints = getLogicalBreakpoints(ctx); + if (breakpoints != null) { + if (breakpoints.isEmpty()) { + return false; + } + for (LogicalBreakpoint lb : breakpoints) { + traces.addAll(lb.getParticipatingTraces()); + } + } + else if (ctx instanceof DebuggerBreakpointLocationsActionContext locCtx) { + Collection locations = locCtx.getLocations(); + if (locations.isEmpty()) { + return false; + } + for (TraceBreakpoint tb : locations) { + traces.add(tb.getTrace()); + } + } + else { + return false; + } + for (Trace trace : traces) { + if (!editingService.getCurrentMode(trace).useEmulatedBreakpoints()) { + return false; + } + } + return true; + } + + private static final Set EXECUTE_KINDS = + Set.of(TraceBreakpointKind.SW_EXECUTE, TraceBreakpointKind.HW_EXECUTE); + + private boolean isAllBreakpointsExecution(ActionContext ctx) { + // TODO GP-2988: Remove this. Implement injection on emu access breakpoints, too + Collection breakpoints = getLogicalBreakpoints(ctx); + if (breakpoints != null) { + for (LogicalBreakpoint lb : breakpoints) { + if (!EXECUTE_KINDS.containsAll(lb.getKinds())) { + return false; + } + } + return true; + } + else if (ctx instanceof DebuggerBreakpointLocationsActionContext locCtx) { + for (TraceBreakpoint tb : locCtx.getLocations()) { + if (!EXECUTE_KINDS.containsAll(tb.getKinds())) { + return false; + } + } + return true; + } + return false; + } + + private boolean isPopupSetCondition(ActionContext ctx) { + return isAllInvolvedTracesUsingEmulatedBreakpoints(ctx) && isAllBreakpointsExecution(ctx); + } + + private boolean isPopupSetInjection(ActionContext ctx) { + return isAllInvolvedTracesUsingEmulatedBreakpoints(ctx) && isAllBreakpointsExecution(ctx); + } + + private String deriveCurrentSleigh(ActionContext ctx) { + String sleigh = null; + Collection breakpoints = getLogicalBreakpoints(ctx); + if (breakpoints != null) { + for (LogicalBreakpoint lb : breakpoints) { + String s = lb.getEmuSleigh(); + if (sleigh != null && !sleigh.equals(s)) { + return null; + } + sleigh = s; + } + return sleigh; + } + else if (ctx instanceof DebuggerBreakpointLocationsActionContext locCtx) { + for (TraceBreakpoint tb : locCtx.getLocations()) { + String s = tb.getEmuSleigh(); + if (sleigh != null && !sleigh.equals(s)) { + return null; + } + sleigh = s; + } + return sleigh; + } + return null; + } + + private String deriveCurrentCondition(ActionContext ctx) { + String sleigh = deriveCurrentSleigh(ctx); + return sleigh == null ? null : SleighUtils.recoverConditionFromBreakpoint(sleigh); + } + + private void injectSleigh(ActionContext ctx, String sleigh) { + Collection breakpoints = getLogicalBreakpoints(ctx); + if (breakpoints != null) { + for (LogicalBreakpoint lb : breakpoints) { + lb.setEmuSleigh(sleigh); + } + } + else if (ctx instanceof DebuggerBreakpointLocationsActionContext locCtx) { + for (TraceBreakpoint tb : locCtx.getLocations()) { + tb.setEmuSleigh(sleigh); + } + } + else { + throw new AssertionError(); + } + } + + private void activatedSetCondition(ActionContext ctx) { + String curCondition = deriveCurrentCondition(ctx); + if (curCondition == null) { + curCondition = SleighUtils.CONDITION_ALWAYS; + } + String condition = DebuggerSleighExpressionInputDialog.INSTANCE.prompt(tool, curCondition); + if (condition == null) { + return; // Cancelled + } + injectSleigh(ctx, SleighUtils.sleighForConditionalBreak(condition)); + } + + private void activatedSetInjection(ActionContext ctx) { + String curSleigh = deriveCurrentSleigh(ctx); + if (curSleigh == null) { + curSleigh = SleighUtils.UNCONDITIONAL_BREAK; + } + String sleigh = DebuggerSleighSemanticInputDialog.INSTANCE.prompt(tool, curSleigh); + if (sleigh == null) { + return; // Cancelled + } + injectSleigh(ctx, sleigh); + } + private void toggledFilterByCurrentTrace(ActionContext ignored) { breakpointTableModel.fireTableDataChanged(); locationTableModel.fireTableDataChanged(); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerSleighExpressionInputDialog.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerSleighExpressionInputDialog.java new file mode 100644 index 0000000000..87ac94d678 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerSleighExpressionInputDialog.java @@ -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. + */ +package ghidra.app.plugin.core.debug.gui.breakpoint; + +import ghidra.app.plugin.core.debug.gui.breakpoint.DebuggerBreakpointsProvider.SetEmulatedBreakpointConditionAction; +import ghidra.framework.plugintool.util.PluginUtils; +import ghidra.pcode.exec.SleighUtils; +import ghidra.util.HelpLocation; + +public class DebuggerSleighExpressionInputDialog extends AbstractDebuggerSleighInputDialog { + public static final DebuggerSleighExpressionInputDialog INSTANCE = + new DebuggerSleighExpressionInputDialog(); + + protected DebuggerSleighExpressionInputDialog() { + super("Breakpoint Sleigh Condition", """ + +

    Enter Emulated Breakpoint Sleigh Condition, e.g:

    +
      +
    • 1:1 (Always)
    • +
    • 0:1 (Never)
    • +
    • RAX == 7
    • +
    +

    Press F1 for help.

    + """); + setHelpLocation(new HelpLocation( + PluginUtils.getPluginNameFromClass(DebuggerBreakpointsPlugin.class), + SetEmulatedBreakpointConditionAction.HELP_ANCHOR)); + } + + @Override + protected void validate() { + SleighUtils.parseSleighExpression(getInput()); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerSleighSemanticInputDialog.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerSleighSemanticInputDialog.java new file mode 100644 index 0000000000..3b94db0bc2 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerSleighSemanticInputDialog.java @@ -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. + */ +package ghidra.app.plugin.core.debug.gui.breakpoint; + +import ghidra.app.plugin.core.debug.gui.breakpoint.DebuggerBreakpointsProvider.SetEmulatedBreakpointInjectionAction; +import ghidra.framework.plugintool.util.PluginUtils; +import ghidra.pcode.exec.SleighUtils; +import ghidra.util.HelpLocation; + +public class DebuggerSleighSemanticInputDialog extends AbstractDebuggerSleighInputDialog { + public static final DebuggerSleighSemanticInputDialog INSTANCE = + new DebuggerSleighSemanticInputDialog(); + + protected DebuggerSleighSemanticInputDialog() { + super("Breakpoint Sleigh Injection", """ + +

    Enter Emulated Breakpoint Sleigh Injection, e.g., an unconditional break:

    +
    +				emu_swi();
    +				emu_exec_decoded();
    +				
    +

    Press F1 for help and more examples.

    +

    Be sure to include control flow, or the emulator may get stuck!

    + """); + setHelpLocation(new HelpLocation( + PluginUtils.getPluginNameFromClass(DebuggerBreakpointsPlugin.class), + SetEmulatedBreakpointInjectionAction.HELP_ANCHOR)); + } + + @Override + protected void validate() { + SleighUtils.parseSleighSemantic(getInput()); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/LogicalBreakpointRow.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/LogicalBreakpointRow.java index 1656787b39..362ee3d67a 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/LogicalBreakpointRow.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/LogicalBreakpointRow.java @@ -22,6 +22,7 @@ import ghidra.app.services.LogicalBreakpoint.Mode; import ghidra.app.services.LogicalBreakpoint.State; import ghidra.framework.model.DomainFile; import ghidra.framework.model.DomainObject; +import ghidra.pcode.exec.SleighUtils; import ghidra.program.model.address.Address; import ghidra.program.model.listing.Program; import ghidra.trace.model.breakpoint.TraceBreakpointKind.TraceBreakpointKindSet; @@ -139,6 +140,11 @@ public class LogicalBreakpointRow { return lb.getTraceBreakpoints().size(); } + public boolean hasSleigh() { + String sleigh = lb.getEmuSleigh(); + return sleigh != null && !SleighUtils.UNCONDITIONAL_BREAK.equals(sleigh); + } + /** * Check if it has mapped locations, regardless of whether those locations are present * diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/DebuggerConsoleProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/DebuggerConsoleProvider.java index c0513000cf..0b2a694df7 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/DebuggerConsoleProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/DebuggerConsoleProvider.java @@ -205,7 +205,7 @@ public class DebuggerConsoleProvider extends ComponentProviderAdapter LogTableColumns, ActionContext, LogRow, LogRow> { public LogTableModel(PluginTool tool) { - super(tool, "Log", LogTableColumns.class, r -> r.getActionContext(), r -> r); + super(tool, "Log", LogTableColumns.class, r -> r.getActionContext(), r -> r, r -> r); } @Override diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/control/DebuggerControlPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/control/DebuggerControlPlugin.java index d56d31723c..6d155f269a 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/control/DebuggerControlPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/control/DebuggerControlPlugin.java @@ -42,8 +42,8 @@ import ghidra.app.plugin.core.debug.service.emulation.DebuggerPcodeMachine; import ghidra.app.services.*; import ghidra.app.services.DebuggerEmulationService.CachedEmulator; import ghidra.app.services.DebuggerEmulationService.EmulatorStateListener; -import ghidra.app.services.DebuggerStateEditingService.StateEditingMode; import ghidra.app.services.DebuggerStateEditingService.StateEditingModeChangeListener; +import ghidra.app.services.DebuggerTraceManagerService.ActivationCause; import ghidra.async.AsyncUtils; import ghidra.dbg.DebuggerObjectModel; import ghidra.dbg.target.*; @@ -210,11 +210,11 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin String GROUP = DebuggerResources.GROUP_CONTROL; } - interface EditModeAction { - String NAME = "Edit Mode"; - String DESCRIPTION = "Choose what to edit in dynamic views"; + interface ControlModeAction { + String NAME = "Control Mode"; + String DESCRIPTION = "Choose what to control and edit in dynamic views"; String GROUP = DebuggerResources.GROUP_CONTROL; - String HELP_ANCHOR = "edit_mode"; + String HELP_ANCHOR = "control_mode"; static MultiStateActionBuilder builder(Plugin owner) { String ownerName = owner.getName(); @@ -606,12 +606,13 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin protected Set getActionSet(StateEditingMode mode) { switch (mode) { - case READ_ONLY: - case WRITE_TARGET: + case RO_TARGET: + case RW_TARGET: return actionsTarget; - case WRITE_TRACE: + case RO_TRACE: + case RW_TRACE: return actionsTrace; - case WRITE_EMULATOR: + case RW_EMULATOR: return actionsEmulate; default: throw new AssertionError(); @@ -639,7 +640,7 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin } protected void createActions() { - actionEditMode = EditModeAction.builder(this) + actionEditMode = ControlModeAction.builder(this) .enabled(false) .enabledWhen(c -> current.getTrace() != null) .onActionStateChanged(this::activateEditMode) @@ -867,7 +868,7 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin DebuggerCoordinates current = this.current; emulationService.backgroundRun(current.getPlatform(), current.getTime(), Scheduler.oneThread(current.getThread())).thenAcceptAsync(r -> { - traceManager.activate(current.time(r.schedule())); + traceManager.activate(current.time(r.schedule()), ActivationCause.USER); }, AsyncUtils.SWING_EXECUTOR).exceptionally(ex -> { Msg.showError(this, null, "Emulate", "Error emulating", ex); return null; @@ -974,10 +975,10 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin // TODO: We're sort of piggy-backing our mode onto that of the editing service. // Seems we should have our own? if (editingService == null) { - return StateEditingMode.READ_ONLY; + return StateEditingMode.DEFAULT; } if (current.getTrace() == null) { - return StateEditingMode.READ_ONLY; + return StateEditingMode.DEFAULT; } return editingService.getCurrentMode(current.getTrace()); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/copying/DebuggerCopyPlan.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/copying/DebuggerCopyPlan.java index b8e224a785..72dd6647e8 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/copying/DebuggerCopyPlan.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/copying/DebuggerCopyPlan.java @@ -20,7 +20,7 @@ import java.util.*; import javax.swing.JCheckBox; import ghidra.app.plugin.core.debug.gui.DebuggerResources; -import ghidra.app.plugin.core.debug.service.breakpoint.LogicalBreakpointInternal.ProgramBreakpoint; +import ghidra.app.plugin.core.debug.service.breakpoint.ProgramBreakpoint; import ghidra.app.util.viewer.listingpanel.PropertyBasedBackgroundColorModel; import ghidra.program.database.IntRangeMap; import ghidra.program.model.address.*; @@ -233,6 +233,7 @@ public class DebuggerCopyPlan { else { pb.disable(); } + pb.setEmuSleigh(bpt.getEmuSleigh()); } } }, diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProvider.java index 9054ae7f2b..648c4ab67e 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProvider.java @@ -52,7 +52,6 @@ import ghidra.app.plugin.core.marker.MarkerMarginProvider; import ghidra.app.plugin.core.marker.MarkerOverviewProvider; import ghidra.app.services.*; import ghidra.app.services.DebuggerListingService.LocationTrackingSpecChangeListener; -import ghidra.app.services.DebuggerStateEditingService.StateEditingMode; import ghidra.app.util.viewer.format.FormatManager; import ghidra.app.util.viewer.listingpanel.ListingPanel; import ghidra.framework.model.DomainFile; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerLegacyRegionsPanel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerLegacyRegionsPanel.java index a6844aecdb..6305335d24 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerLegacyRegionsPanel.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerLegacyRegionsPanel.java @@ -113,7 +113,7 @@ public class DebuggerLegacyRegionsPanel extends JPanel { public RegionTableModel(PluginTool tool) { super(tool, "Regions", RegionTableColumns.class, TraceMemoryRegion::getObjectKey, - RegionRow::new); + RegionRow::new, RegionRow::getRegion); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerRegionsProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerRegionsProvider.java index d9bc4b9775..3c65bf5065 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerRegionsProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerRegionsProvider.java @@ -204,10 +204,6 @@ public class DebuggerRegionsProvider extends ComponentProviderAdapter { createActions(); } - protected static boolean isLegacy(Trace trace) { - return trace != null && trace.getObjectManager().getRootSchema() == null; - } - protected void buildMainPanel() { panel = new DebuggerRegionsPanel(this); mainPanel.add(panel); @@ -246,7 +242,7 @@ public class DebuggerRegionsProvider extends ComponentProviderAdapter { @Override public ActionContext getActionContext(MouseEvent event) { final ActionContext context; - if (isLegacy(current.getTrace())) { + if (Trace.isLegacy(current.getTrace())) { context = legacyPanel.getActionContext(); } else { @@ -395,7 +391,7 @@ public class DebuggerRegionsProvider extends ComponentProviderAdapter { } public void setSelectedRegions(Set sel) { - if (isLegacy(current.getTrace())) { + if (Trace.isLegacy(current.getTrace())) { legacyPanel.setSelectedRegions(sel); } else { @@ -465,7 +461,7 @@ public class DebuggerRegionsProvider extends ComponentProviderAdapter { current = coordinates; - if (isLegacy(coordinates.getTrace())) { + if (Trace.isLegacy(coordinates.getTrace())) { panel.coordinatesActivated(DebuggerCoordinates.NOWHERE); legacyPanel.coordinatesActivated(coordinates); if (ArrayUtils.indexOf(mainPanel.getComponents(), legacyPanel) == -1) { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerLegacyModulesPanel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerLegacyModulesPanel.java index 0d030f6f6d..d9ade9941d 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerLegacyModulesPanel.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerLegacyModulesPanel.java @@ -146,7 +146,7 @@ public class DebuggerLegacyModulesPanel extends JPanel { public ModuleTableModel(PluginTool tool) { super(tool, "Modules", ModuleTableColumns.class, TraceModule::getObjectKey, - ModuleRow::new); + ModuleRow::new, ModuleRow::getModule); } @Override diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerLegacySectionsPanel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerLegacySectionsPanel.java index 39f0b93c7d..93d44ee24e 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerLegacySectionsPanel.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerLegacySectionsPanel.java @@ -130,7 +130,7 @@ public class DebuggerLegacySectionsPanel extends JPanel { public SectionTableModel(PluginTool tool) { super(tool, "Sections", SectionTableColumns.class, TraceSection::getObjectKey, - SectionRow::new); + SectionRow::new, SectionRow::getSection); } @Override diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProvider.java index b5ce14bab5..ab2c663686 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProvider.java @@ -513,10 +513,6 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter { } } - protected static boolean isLegacy(Trace trace) { - return trace != null && trace.getObjectManager().getRootSchema() == null; - } - @Override public ActionContext getActionContext(MouseEvent event) { if (myActionContext == null) { @@ -1026,7 +1022,7 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter { current = coordinates; - if (isLegacy(coordinates.getTrace())) { + if (Trace.isLegacy(coordinates.getTrace())) { modulesPanel.coordinatesActivated(DebuggerCoordinates.NOWHERE); sectionsPanel.coordinatesActivated(DebuggerCoordinates.NOWHERE); legacyModulesPanel.coordinatesActivated(coordinates); @@ -1057,7 +1053,7 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter { } public void setSelectedModules(Set sel) { - if (isLegacy(current.getTrace())) { + if (Trace.isLegacy(current.getTrace())) { legacyModulesPanel.setSelectedModules(sel); } else { @@ -1066,7 +1062,7 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter { } public void setSelectedSections(Set sel) { - if (isLegacy(current.getTrace())) { + if (Trace.isLegacy(current.getTrace())) { legacySectionsPanel.setSelectedSections(sel); } else { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerStaticMappingProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerStaticMappingProvider.java index 76bdee41e0..22839ce755 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerStaticMappingProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerStaticMappingProvider.java @@ -101,7 +101,8 @@ public class DebuggerStaticMappingProvider extends ComponentProviderAdapter public MappingTableModel(PluginTool tool) { super(tool, "Mappings", StaticMappingTableColumns.class, - TraceStaticMapping::getObjectKey, StaticMappingRow::new); + TraceStaticMapping::getObjectKey, StaticMappingRow::new, + StaticMappingRow::getMapping); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/DebuggerObjectsProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/DebuggerObjectsProvider.java index 94c0b95d99..06477d138a 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/DebuggerObjectsProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/DebuggerObjectsProvider.java @@ -53,6 +53,7 @@ import ghidra.app.plugin.core.debug.gui.objects.components.*; import ghidra.app.plugin.core.debug.mapping.DebuggerMemoryMapper; import ghidra.app.script.*; import ghidra.app.services.*; +import ghidra.app.services.DebuggerTraceManagerService.ActivationCause; import ghidra.async.*; import ghidra.dbg.*; import ghidra.dbg.error.DebuggerMemoryAccessException; @@ -1489,7 +1490,7 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter //this.recorder = rec; Trace trace = rec.getTrace(); traceManager.openTrace(trace); - traceManager.activateTrace(trace); + traceManager.activate(traceManager.resolveTrace(trace), ActivationCause.START_RECORDING); } public void stopRecording(TargetObject targetObject) { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackProvider.java index 153186c853..dfee8e9c01 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackProvider.java @@ -131,14 +131,10 @@ public class DebuggerStackProvider extends ComponentProviderAdapter { return mainPanel; } - protected static boolean isLegacy(Trace trace) { - return trace != null && trace.getObjectManager().getRootSchema() == null; - } - @Override public ActionContext getActionContext(MouseEvent event) { final ActionContext context; - if (isLegacy(current.getTrace())) { + if (Trace.isLegacy(current.getTrace())) { context = legacyPanel.getActionContext(); } else { @@ -167,7 +163,7 @@ public class DebuggerStackProvider extends ComponentProviderAdapter { current = coordinates; - if (isLegacy(coordinates.getTrace())) { + if (Trace.isLegacy(coordinates.getTrace())) { panel.coordinatesActivated(DebuggerCoordinates.NOWHERE); legacyPanel.coordinatesActivated(coordinates); if (ArrayUtils.indexOf(mainPanel.getComponents(), legacyPanel) == -1) { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerLegacyThreadsPanel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerLegacyThreadsPanel.java index 47c2066af1..c95313dbb5 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerLegacyThreadsPanel.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerLegacyThreadsPanel.java @@ -120,7 +120,8 @@ public class DebuggerLegacyThreadsPanel extends JPanel { public ThreadTableModel(DebuggerThreadsProvider provider) { super(provider.getTool(), "Threads", ThreadTableColumns.class, - TraceThread::getObjectKey, t -> new ThreadRow(provider.modelService, t)); + TraceThread::getObjectKey, t -> new ThreadRow(provider.modelService, t), + ThreadRow::getThread); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProvider.java index 2aa581c0fc..2349afda78 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProvider.java @@ -26,11 +26,11 @@ import org.apache.commons.lang3.ArrayUtils; import docking.ActionContext; import docking.WindowPosition; import docking.action.*; -import docking.widgets.dialogs.InputDialog; import ghidra.app.plugin.core.debug.DebuggerCoordinates; import ghidra.app.plugin.core.debug.DebuggerPluginPackage; import ghidra.app.plugin.core.debug.gui.DebuggerResources; -import ghidra.app.plugin.core.debug.gui.DebuggerResources.*; +import ghidra.app.plugin.core.debug.gui.DebuggerResources.SynchronizeFocusAction; +import ghidra.app.plugin.core.debug.gui.DebuggerResources.ToToggleSelectionListener; import ghidra.app.services.*; import ghidra.app.services.DebuggerTraceManagerService.BooleanChangeAdapter; import ghidra.framework.model.DomainObject; @@ -42,8 +42,6 @@ import ghidra.trace.model.Trace; import ghidra.trace.model.Trace.TraceSnapshotChangeType; import ghidra.trace.model.TraceDomainObjectListener; import ghidra.trace.model.time.TraceSnapshot; -import ghidra.trace.model.time.schedule.TraceSchedule; -import ghidra.util.Msg; public class DebuggerThreadsProvider extends ComponentProviderAdapter { @@ -108,8 +106,6 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter { @SuppressWarnings("unused") private final AutoService.Wiring autoServiceWiring; - private final BooleanChangeAdapter activatePresentChangeListener = - this::changedAutoActivatePresent; private final BooleanChangeAdapter synchronizeFocusChangeListener = this::changedSynchronizeFocus; @@ -123,9 +119,7 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter { DebuggerLegacyThreadsPanel legacyPanel; DockingAction actionSaveTrace; - ToggleDockingAction actionSeekTracePresent; ToggleDockingAction actionSyncFocus; - DockingAction actionGoToTime; ActionContext myActionContext; @@ -155,17 +149,11 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter { @AutoServiceConsumed public void setTraceManager(DebuggerTraceManagerService traceManager) { if (this.traceManager != null) { - this.traceManager - .removeAutoActivatePresentChangeListener(activatePresentChangeListener); this.traceManager.removeSynchronizeFocusChangeListener(synchronizeFocusChangeListener); } this.traceManager = traceManager; if (traceManager != null) { - traceManager.addAutoActivatePresentChangeListener(activatePresentChangeListener); traceManager.addSynchronizeFocusChangeListener(synchronizeFocusChangeListener); - if (actionSeekTracePresent != null) { - actionSeekTracePresent.setSelected(traceManager.isAutoActivatePresent()); - } if (actionSyncFocus != null) { actionSyncFocus.setSelected(traceManager.isSynchronizeFocus()); } @@ -178,10 +166,6 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter { contextChanged(); } - private boolean isLegacy(Trace trace) { - return trace != null && trace.getObjectManager().getRootSchema() == null; - } - public void coordinatesActivated(DebuggerCoordinates coordinates) { if (sameCoordinates(current, coordinates)) { current = coordinates; @@ -191,7 +175,7 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter { current = coordinates; traceTabs.coordinatesActivated(coordinates); - if (isLegacy(coordinates.getTrace())) { + if (Trace.isLegacy(coordinates.getTrace())) { panel.coordinatesActivated(DebuggerCoordinates.NOWHERE); legacyPanel.coordinatesActivated(coordinates); if (ArrayUtils.indexOf(mainPanel.getComponents(), legacyPanel) == -1) { @@ -252,43 +236,15 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter { } protected void createActions() { - actionSeekTracePresent = SeekTracePresentAction.builder(plugin) - .enabledWhen(this::isSeekTracePresentEnabled) - .onAction(this::toggledSeekTracePresent) - .selected(traceManager == null ? false : traceManager.isAutoActivatePresent()) - .buildAndInstallLocal(this); - actionSyncFocus = SynchronizeFocusAction.builder(plugin) .selected(traceManager != null && traceManager.isSynchronizeFocus()) .enabledWhen(c -> traceManager != null) .onAction(c -> toggleSyncFocus(actionSyncFocus.isSelected())) .buildAndInstallLocal(this); - actionGoToTime = GoToTimeAction.builder(plugin) - .enabledWhen(c -> current.getTrace() != null) - .onAction(c -> activatedGoToTime()) - .buildAndInstallLocal(this); traceManager.addSynchronizeFocusChangeListener(toToggleSelectionListener = new ToToggleSelectionListener(actionSyncFocus)); } - private boolean isSeekTracePresentEnabled(ActionContext context) { - return traceManager != null; - } - - private void toggledSeekTracePresent(ActionContext context) { - if (traceManager == null) { - return; - } - traceManager.setAutoActivatePresent(actionSeekTracePresent.isSelected()); - } - - private void changedAutoActivatePresent(boolean value) { - if (actionSeekTracePresent == null || actionSeekTracePresent.isSelected()) { - return; - } - actionSeekTracePresent.setSelected(value); - } - private void changedSynchronizeFocus(boolean value) { if (actionSyncFocus == null || actionSyncFocus.isSelected()) { return; @@ -303,22 +259,6 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter { traceManager.setSynchronizeFocus(enabled); } - private void activatedGoToTime() { - InputDialog dialog = - new InputDialog("Go To Time", "Schedule:", current.getTime().toString()); - tool.showDialog(dialog); - if (dialog.isCanceled()) { - return; - } - try { - TraceSchedule time = TraceSchedule.parse(dialog.getValue()); - traceManager.activateTime(time); - } - catch (IllegalArgumentException e) { - Msg.showError(this, getComponent(), "Go To Time", "Could not parse schedule"); - } - } - @Override public JComponent getComponent() { return mainPanel; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimeProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimeProvider.java index 6f49bc3dec..cf85456fe2 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimeProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimeProvider.java @@ -21,11 +21,13 @@ import java.awt.event.MouseEvent; import java.lang.invoke.MethodHandles; import java.util.Objects; +import javax.swing.Icon; import javax.swing.JComponent; import docking.ActionContext; -import docking.action.DockingActionIf; -import docking.action.ToggleDockingAction; +import docking.action.*; +import docking.action.builder.ActionBuilder; +import docking.widgets.dialogs.InputDialog; import ghidra.app.plugin.core.debug.DebuggerCoordinates; import ghidra.app.plugin.core.debug.DebuggerPluginPackage; import ghidra.app.plugin.core.debug.gui.DebuggerResources; @@ -36,11 +38,32 @@ import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.AutoService.Wiring; import ghidra.framework.plugintool.annotation.AutoConfigStateField; import ghidra.framework.plugintool.annotation.AutoServiceConsumed; +import ghidra.trace.model.time.schedule.TraceSchedule; +import ghidra.util.HelpLocation; +import ghidra.util.Msg; public class DebuggerTimeProvider extends ComponentProviderAdapter { private static final AutoConfigState.ClassHandler CONFIG_STATE_HANDLER = AutoConfigState.wireHandler(DebuggerTimeProvider.class, MethodHandles.lookup()); + interface GoToTimeAction { + String NAME = "Go To Time"; + String DESCRIPTION = "Go to a specific time, optionally using emulation"; + String GROUP = GROUP_TRACE; + Icon ICON = ICON_TIME; + String HELP_ANCHOR = "goto_time"; + + static ActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new ActionBuilder(NAME, ownerName).description(DESCRIPTION) + .menuPath(DebuggerPluginPackage.NAME, NAME) + .menuGroup(GROUP) + .menuIcon(ICON) + .keyBinding("CTRL G") + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); + } + } + protected static boolean sameCoordinates(DebuggerCoordinates a, DebuggerCoordinates b) { if (!Objects.equals(a.getTrace(), b.getTrace())) { return false; @@ -56,16 +79,17 @@ public class DebuggerTimeProvider extends ComponentProviderAdapter { DebuggerCoordinates current = DebuggerCoordinates.NOWHERE; @AutoServiceConsumed - protected DebuggerTraceManagerService viewManager; + protected DebuggerTraceManagerService traceManager; @SuppressWarnings("unused") private final Wiring autoServiceWiring; /*testing*/ final DebuggerSnapshotTablePanel mainPanel; - private DebuggerSnapActionContext myActionContext; - + DockingAction actionGoToTime; ToggleDockingAction actionHideScratch; + private DebuggerSnapActionContext myActionContext; + @AutoConfigStateField /*testing*/ boolean hideScratch = true; @@ -122,18 +146,38 @@ public class DebuggerTimeProvider extends ComponentProviderAdapter { return; } myActionContext = new DebuggerSnapActionContext(current.getTrace(), snap); - viewManager.activateSnap(snap); + traceManager.activateSnap(snap); contextChanged(); }); } protected void createActions() { + actionGoToTime = GoToTimeAction.builder(plugin) + .enabledWhen(c -> current.getTrace() != null) + .onAction(c -> activatedGoToTime()) + .buildAndInstall(tool); actionHideScratch = DebuggerResources.HideScratchSnapshotsAction.builder(plugin) .selected(hideScratch) .onAction(this::activatedHideScratch) .buildAndInstallLocal(this); } + private void activatedGoToTime() { + InputDialog dialog = + new InputDialog("Go To Time", "Schedule:", current.getTime().toString()); + tool.showDialog(dialog); + if (dialog.isCanceled()) { + return; + } + try { + TraceSchedule time = TraceSchedule.parse(dialog.getValue()); + traceManager.activateTime(time); + } + catch (IllegalArgumentException e) { + Msg.showError(this, getComponent(), "Go To Time", "Could not parse schedule"); + } + } + private void activatedHideScratch(ActionContext ctx) { hideScratch = !hideScratch; mainPanel.setHideScratchSnapshots(hideScratch); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/BreakpointActionItem.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/BreakpointActionItem.java index 7bb0c3fb94..59cdd8a5ac 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/BreakpointActionItem.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/BreakpointActionItem.java @@ -17,6 +17,36 @@ package ghidra.app.plugin.core.debug.service.breakpoint; import java.util.concurrent.CompletableFuture; +import ghidra.async.AsyncUtils; +import ghidra.program.model.address.*; + +/** + * An invocation is planning an action on a breakpoint + * + * @see BreakpointActionSet + */ public interface BreakpointActionItem { + /** + * Compute a range from an address and length + * + * @param address the min address + * @param length the length + * @return the range + */ + default AddressRange range(Address address, long length) { + try { + return new AddressRangeImpl(address, length); + } + catch (AddressOverflowException e) { + throw new AssertionError(e); + } + } + + /** + * Perform the action + * + * @return the future for the action. Synchronous invocations can just return + * {@link AsyncUtils#NIL}. + */ public CompletableFuture execute(); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/BreakpointActionSet.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/BreakpointActionSet.java index db6f9761ba..7eae6d3a28 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/BreakpointActionSet.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/BreakpointActionSet.java @@ -20,62 +20,122 @@ import java.util.concurrent.CompletableFuture; import ghidra.async.AsyncFence; import ghidra.dbg.target.*; +import ghidra.trace.model.breakpoint.TraceBreakpoint; /** - * A de-duplicated collection of target breakpoint actions necessary to implement a logical - * breakpoint action. + * A de-duplicated collection of breakpoint action items necessary to implement a logical breakpoint + * action. + * + *

    + * This will de-duplicate action items, but it does not check them for sanity. For example, deleting + * a breakpoint then enabling it. Typically, all the items are the same type, so such sanity checks + * are not necessary. */ public class BreakpointActionSet extends LinkedHashSet { - public EnableBreakpointActionItem planEnable(TargetBreakpointLocation loc) { + /** + * Add an item to enable a target breakpoint + * + * @param loc the target breakpoint + * @return the added item + */ + public EnableTargetBreakpointActionItem planEnableTarget(TargetBreakpointLocation loc) { if (loc instanceof TargetTogglable) { - EnableBreakpointActionItem action = - new EnableBreakpointActionItem((TargetTogglable) loc); + EnableTargetBreakpointActionItem action = + new EnableTargetBreakpointActionItem((TargetTogglable) loc); add(action); return action; } TargetBreakpointSpec spec = loc.getSpecification(); if (spec instanceof TargetTogglable) { - EnableBreakpointActionItem action = new EnableBreakpointActionItem(spec); + EnableTargetBreakpointActionItem action = new EnableTargetBreakpointActionItem(spec); add(action); return action; } return null; } - public DisableBreakpointActionItem planDisable(TargetBreakpointLocation loc) { + /** + * Add an item to enable an emulated breakpoint + * + * @param bpt the trace breakpoint + * @return the added item + */ + public EnableEmuBreakpointActionItem planEnableEmu(TraceBreakpoint bpt) { + EnableEmuBreakpointActionItem action = new EnableEmuBreakpointActionItem(bpt); + add(action); + return action; + } + + /** + * Add an item to disable a target breakpoint + * + * @param loc the target breakpoint + * @return the added item + */ + public DisableTargetBreakpointActionItem planDisableTarget(TargetBreakpointLocation loc) { if (loc instanceof TargetTogglable) { - DisableBreakpointActionItem action = - new DisableBreakpointActionItem((TargetTogglable) loc); + DisableTargetBreakpointActionItem action = + new DisableTargetBreakpointActionItem((TargetTogglable) loc); add(action); return action; } TargetBreakpointSpec spec = loc.getSpecification(); if (spec instanceof TargetTogglable) { - DisableBreakpointActionItem action = new DisableBreakpointActionItem(spec); + DisableTargetBreakpointActionItem action = new DisableTargetBreakpointActionItem(spec); add(action); return action; } return null; } - public DeleteBreakpointActionItem planDelete(TargetBreakpointLocation loc) { + /** + * Add an item to disable an emulated breakpoint + * + * @param bpt the trace breakpoint + * @return the added item + */ + public DisableEmuBreakpointActionItem planDisableEmu(TraceBreakpoint bpt) { + DisableEmuBreakpointActionItem action = new DisableEmuBreakpointActionItem(bpt); + add(action); + return action; + } + + /** + * Add an item to delete a target breakpoint + * + * @param loc the target breakpoint + * @return the added item + */ + public DeleteTargetBreakpointActionItem planDeleteTarget(TargetBreakpointLocation loc) { if (loc instanceof TargetDeletable) { - DeleteBreakpointActionItem action = - new DeleteBreakpointActionItem((TargetDeletable) loc); + DeleteTargetBreakpointActionItem action = + new DeleteTargetBreakpointActionItem((TargetDeletable) loc); add(action); return action; } TargetBreakpointSpec spec = loc.getSpecification(); if (spec instanceof TargetTogglable) { - DeleteBreakpointActionItem action = - new DeleteBreakpointActionItem((TargetDeletable) spec); + DeleteTargetBreakpointActionItem action = + new DeleteTargetBreakpointActionItem((TargetDeletable) spec); add(action); return action; } return null; } + /** + * Add an item to delete an emulated breakpoint + * + * @param bpt the trace breakpoint + * @return the added item + */ + public DeleteEmuBreakpointActionItem planDeleteEmu(TraceBreakpoint bpt) { + DeleteEmuBreakpointActionItem action = new DeleteEmuBreakpointActionItem(bpt); + add(action); + return action; + } + /** * Carry out the actions in the order they were added * diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/DebuggerLogicalBreakpointServicePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/DebuggerLogicalBreakpointServicePlugin.java index 5418a3d6dd..920aedec03 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/DebuggerLogicalBreakpointServicePlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/DebuggerLogicalBreakpointServicePlugin.java @@ -27,11 +27,11 @@ import generic.CatenatedCollection; import ghidra.app.events.ProgramClosedPluginEvent; import ghidra.app.events.ProgramOpenedPluginEvent; import ghidra.app.plugin.PluginCategoryNames; +import ghidra.app.plugin.core.debug.DebuggerCoordinates; import ghidra.app.plugin.core.debug.DebuggerPluginPackage; -import ghidra.app.plugin.core.debug.event.TraceClosedPluginEvent; -import ghidra.app.plugin.core.debug.event.TraceOpenedPluginEvent; -import ghidra.app.plugin.core.debug.service.breakpoint.LogicalBreakpointInternal.ProgramBreakpoint; +import ghidra.app.plugin.core.debug.event.*; import ghidra.app.services.*; +import ghidra.app.services.DebuggerStateEditingService.StateEditingModeChangeListener; import ghidra.app.services.LogicalBreakpoint.State; import ghidra.async.SwingExecutorService; import ghidra.dbg.target.TargetBreakpointLocation; @@ -41,8 +41,7 @@ import ghidra.framework.model.*; import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.annotation.AutoServiceConsumed; import ghidra.framework.plugintool.util.PluginStatus; -import ghidra.program.model.address.Address; -import ghidra.program.model.address.AddressRange; +import ghidra.program.model.address.*; import ghidra.program.model.listing.*; import ghidra.program.util.*; import ghidra.trace.model.*; @@ -65,6 +64,8 @@ import ghidra.util.datastruct.ListenerSet; ProgramOpenedPluginEvent.class, ProgramClosedPluginEvent.class, TraceOpenedPluginEvent.class, + TraceActivatedPluginEvent.class, + TraceInactiveCoordinatesPluginEvent.class, TraceClosedPluginEvent.class, }, servicesRequired = { @@ -186,6 +187,13 @@ public class DebuggerLogicalBreakpointServicePlugin extends Plugin } } + protected class TrackModesListener implements StateEditingModeChangeListener { + @Override + public void modeChanged(Trace trace, StateEditingMode mode) { + processChange(c -> evtModeChanged(c, trace), "modeChanged"); + } + } + private class TraceBreakpointsListener extends TraceDomainObjectListener { private final InfoPerTrace info; private ChangeCollector c; @@ -218,29 +226,28 @@ public class DebuggerLogicalBreakpointServicePlugin extends Plugin } private void breakpointAdded(TraceBreakpoint tb) { - if (!tb.getLifespan().contains(info.recorder.getSnap())) { - // NOTE: User/script probably added historical breakpoint + if (!tb.getLifespan().contains(info.snap)) { return; } try { info.trackTraceBreakpoint(c.a, tb, false); } catch (TrackedTooSoonException e) { - Msg.info(this, "Ignoring " + tb + - " added until service has finished loading its trace"); + Msg.info(this, + "Ignoring " + tb + " added until service has finished loading its trace"); } } private void breakpointChanged(TraceBreakpoint tb) { - if (!tb.getLifespan().contains(info.recorder.getSnap())) { + if (!tb.getLifespan().contains(info.snap)) { return; } try { info.trackTraceBreakpoint(c.a, tb, true); } catch (TrackedTooSoonException e) { - Msg.info(this, "Ignoring " + tb + - " changed until service has finished loading its trace"); + Msg.info(this, + "Ignoring " + tb + " changed until service has finished loading its trace"); } catch (NoSuchElementException e) { // TODO: This catch clause should not be necessary. @@ -252,8 +259,8 @@ public class DebuggerLogicalBreakpointServicePlugin extends Plugin private void breakpointLifespanChanged(TraceAddressSpace spaceIsNull, TraceBreakpoint tb, Lifespan oldSpan, Lifespan newSpan) { // NOTE: User/script probably modified historical breakpoint - boolean isInOld = oldSpan.contains(info.recorder.getSnap()); - boolean isInNew = newSpan.contains(info.recorder.getSnap()); + boolean isInOld = oldSpan.contains(info.snap); + boolean isInNew = newSpan.contains(info.snap); if (isInOld == isInNew) { return; } @@ -272,11 +279,7 @@ public class DebuggerLogicalBreakpointServicePlugin extends Plugin } private void breakpointDeleted(TraceBreakpoint tb) { - if (!tb.getLifespan().contains(info.recorder.getSnap())) { - // NOTE: User/script probably removed historical breakpoint - // assert false; - return; - } + // Could check snap, but might as well just be sure it's gone info.forgetTraceBreakpoint(c.r, tb); } } @@ -375,8 +378,6 @@ public class DebuggerLogicalBreakpointServicePlugin extends Plugin Address address, TraceBreakpoint tb) { Set set = breakpointsByAddress.get(address); if (set == null) { - Msg.warn(this, "Breakpoint to remove is not present: " + tb + ", trace=" + - tb.getTrace()); return null; } for (LogicalBreakpointInternal lb : Set.copyOf(set)) { @@ -393,8 +394,6 @@ public class DebuggerLogicalBreakpointServicePlugin extends Plugin return lb; } } - Msg.warn(this, "Breakpoint to remove is not present: " + tb + ", trace=" + - tb.getTrace()); return null; } @@ -451,22 +450,41 @@ public class DebuggerLogicalBreakpointServicePlugin extends Plugin } protected class InfoPerTrace extends AbstractInfo { - final TraceRecorder recorder; final Trace trace; final TraceBreakpointsListener breakpointListener; - public InfoPerTrace(TraceRecorder recorder) { - this.recorder = recorder; - this.trace = recorder.getTrace(); + TraceRecorder recorder; + long snap = -1; + + public InfoPerTrace(Trace trace) { + this.trace = Objects.requireNonNull(trace); this.breakpointListener = new TraceBreakpointsListener(this); trace.addListener(breakpointListener); } + protected void setRecorderAndSnap(TraceRecorder recorder, long snap, ChangeCollector c) { + if (this.recorder == recorder && this.snap == snap) { + return; + } + this.recorder = recorder; + this.snap = snap; + + for (InfoPerProgram info : programInfos.values()) { + // This is heavy, but not sure a better method + // Could examine mapping service to narrow down relevant programs... + info.reloadBreakpoints(c); + } + reloadBreakpoints(c); + } + @Override protected LogicalBreakpointInternal createLogicalBreakpoint(Address address, long length, Collection kinds) { - return new LoneLogicalBreakpoint(recorder, address, length, kinds); + LoneLogicalBreakpoint lb = + new LoneLogicalBreakpoint(tool, trace, address, length, kinds); + lb.setRecorder(trace, recorder); + return lb; } @Override @@ -477,33 +495,45 @@ public class DebuggerLogicalBreakpointServicePlugin extends Plugin protected void reloadBreakpoints(ChangeCollector c) { forgetTraceInvalidBreakpoints(c.r); - trackTraceLiveBreakpoints(c.a); + trackTraceBreakpoints(c.a); } protected void forgetAllBreakpoints(RemoveCollector r) { - Collection live = new ArrayList<>(); + Collection toForget = new ArrayList<>(); for (AddressRange range : trace.getBaseAddressFactory().getAddressSet()) { - live.addAll(trace + toForget.addAll(trace .getBreakpointManager() - .getBreakpointsIntersecting(Lifespan.at(recorder.getSnap()), range)); + .getBreakpointsIntersecting(Lifespan.ALL, range)); } - for (TraceBreakpoint tb : live) { + for (TraceBreakpoint tb : toForget) { forgetTraceBreakpoint(r, tb); } } protected void forgetTraceInvalidBreakpoints(RemoveCollector r) { - // Breakpoint can become invalid because it is itself invalid (deleted) - // Or because the mapping to static space has changed or become invalid - + /** + * Breakpoint can become invalid because it is itself invalid (deleted), because the + * snap has changed to where it's no longer present, because the mapping to static space + * has changed or become invalid, or because it has no live breakpoint in target mode. + */ + StateEditingMode mode = getMode(trace); for (Set set : List .copyOf(breakpointsByAddress.values())) { for (LogicalBreakpointInternal lb : Set.copyOf(set)) { for (TraceBreakpoint tb : Set.copyOf(lb.getTraceBreakpoints(trace))) { + if (!mode.useEmulatedBreakpoints() && + (recorder == null || recorder.getTargetBreakpoint(tb) == null)) { + forgetTraceBreakpoint(r, tb); + continue; + } if (!trace.getBreakpointManager().getAllBreakpoints().contains(tb)) { forgetTraceBreakpoint(r, tb); continue; } + if (!tb.getLifespan().contains(snap)) { + forgetTraceBreakpoint(r, tb); + continue; + } ProgramLocation progLoc = computeStaticLocation(tb); if (!Objects.equals(lb.getProgramLocation(), progLoc)) { // NOTE: This can happen to Lone breakpoints. @@ -516,14 +546,28 @@ public class DebuggerLogicalBreakpointServicePlugin extends Plugin } } - protected void trackTraceLiveBreakpoints(AddCollector a) { - Collection live = new ArrayList<>(); - for (AddressRange range : trace.getBaseAddressFactory().getAddressSet()) { - live.addAll(trace - .getBreakpointManager() - .getBreakpointsIntersecting(Lifespan.at(recorder.getSnap()), range)); + protected void trackTraceBreakpoints(AddCollector a) { + StateEditingMode mode = getMode(trace); + if (!mode.useEmulatedBreakpoints() && recorder == null) { + return; } - trackTraceBreakpoints(a, live); + Collection visible = new ArrayList<>(); + for (AddressRange range : trace.getBaseAddressFactory().getAddressSet()) { + Collection breaks = trace + .getBreakpointManager() + .getBreakpointsIntersecting(Lifespan.at(snap), range); + if (mode.useEmulatedBreakpoints()) { + visible.addAll(breaks); + } + else { + for (TraceBreakpoint tb : breaks) { + if (recorder.getTargetBreakpoint(tb) != null) { + visible.add(tb); + } + } + } + } + trackTraceBreakpoints(a, visible); } protected void trackTraceBreakpoints(AddCollector a, @@ -549,8 +593,8 @@ public class DebuggerLogicalBreakpointServicePlugin extends Plugin */ return null; } - return mappingService.getOpenMappedLocation(new DefaultTraceLocation(trace, - null, Lifespan.at(recorder.getSnap()), tb.getMinAddress())); + return mappingService.getOpenMappedLocation( + new DefaultTraceLocation(trace, null, Lifespan.at(snap), tb.getMinAddress())); } protected void trackTraceBreakpoint(AddCollector a, TraceBreakpoint tb, boolean forceUpdate) @@ -585,7 +629,7 @@ public class DebuggerLogicalBreakpointServicePlugin extends Plugin // Must be shutting down return null; } - return mappingService.getOpenMappedLocation(trace, loc, recorder.getSnap()); + return mappingService.getOpenMappedLocation(trace, loc, snap); } } @@ -606,7 +650,8 @@ public class DebuggerLogicalBreakpointServicePlugin extends Plugin if (loc == null) { continue; } - lb.setTraceAddress(ti.recorder, loc.getAddress()); + lb.setTraceAddress(ti.trace, loc.getAddress()); + lb.setRecorder(ti.trace, ti.recorder); ti.breakpointsByAddress.computeIfAbsent(loc.getAddress(), __ -> new HashSet<>()) .add(lb); } @@ -616,7 +661,7 @@ public class DebuggerLogicalBreakpointServicePlugin extends Plugin protected LogicalBreakpointInternal createLogicalBreakpoint(Address address, long length, Collection kinds) { MappedLogicalBreakpoint lb = - new MappedLogicalBreakpoint(program, address, length, kinds); + new MappedLogicalBreakpoint(tool, program, address, length, kinds); mapTraceAddresses(lb); return lb; } @@ -763,6 +808,8 @@ public class DebuggerLogicalBreakpointServicePlugin extends Plugin private DebuggerTraceManagerService traceManager; // @AutoServiceConsumed via method private DebuggerStaticMappingService mappingService; + // @AutoServiceConsumed via method + private DebuggerStateEditingService editingService; @SuppressWarnings("unused") private final AutoService.Wiring autoServiceWiring; @@ -772,6 +819,7 @@ public class DebuggerLogicalBreakpointServicePlugin extends Plugin private final TrackRecordersListener recorderListener = new TrackRecordersListener(); private final TrackMappingsListener mappingListener = new TrackMappingsListener(); + private final TrackModesListener modeListener = new TrackModesListener(); private final Map traceInfos = new HashMap<>(); private final Map programInfos = new HashMap<>(); @@ -832,6 +880,13 @@ public class DebuggerLogicalBreakpointServicePlugin extends Plugin } } + protected void evtModeChanged(ChangeCollector c, Trace trace) { + InfoPerTrace info = traceInfos.get(trace); + if (info != null) { + info.reloadBreakpoints(c); + } + } + protected void removeLogicalBreakpointGlobally(LogicalBreakpoint lb) { ProgramLocation pLoc = lb.getProgramLocation(); if (pLoc != null) { @@ -871,6 +926,17 @@ public class DebuggerLogicalBreakpointServicePlugin extends Plugin } } + @AutoServiceConsumed + public void setEditingService(DebuggerStateEditingService editingService) { + if (this.editingService != null) { + this.editingService.removeModeChangeListener(modeListener); + } + this.editingService = editingService; + if (this.editingService != null) { + this.editingService.addModeChangeListener(modeListener); + } + } + private void programOpened(Program program) { processChange(c -> evtProgramOpened(c, program), "programOpened"); } @@ -899,14 +965,13 @@ public class DebuggerLogicalBreakpointServicePlugin extends Plugin // The mapping removals, if any, will clean up related traces } - private void doTrackTrace(ChangeCollector c, Trace trace, TraceRecorder recorder) { - if (traceInfos.containsKey(trace)) { - Msg.warn(this, "Already tracking trace breakpoints"); - return; + private void doTrackTrace(ChangeCollector c, Trace trace, TraceRecorder recorder, long snap) { + InfoPerTrace info = traceInfos.get(trace); + if (info == null) { + info = new InfoPerTrace(trace); + traceInfos.put(trace, info); } - InfoPerTrace info = new InfoPerTrace(recorder); - traceInfos.put(trace, info); - info.reloadBreakpoints(c); + info.setRecorderAndSnap(recorder, snap, c); } private void doUntrackTrace(ChangeCollector c, Trace trace) { @@ -932,23 +997,35 @@ public class DebuggerLogicalBreakpointServicePlugin extends Plugin if (!traceManager.getOpenTraces().contains(trace)) { return; } - doTrackTrace(c, trace, recorder); + long snap = traceManager.getCurrentFor(trace).getSnap(); + doTrackTrace(c, trace, recorder, snap); } private void evtTraceRecordingStopped(ChangeCollector c, TraceRecorder recorder) { - doUntrackTrace(c, recorder.getTrace()); + Trace trace = recorder.getTrace(); + if (!traceManager.getOpenTraces().contains(trace)) { + return; + } + long snap = traceManager.getCurrentFor(trace).getSnap(); + doTrackTrace(c, trace, null, snap); } private void traceOpened(Trace trace) { processChange(c -> { TraceRecorder recorder = modelService.getRecorder(trace); - if (recorder == null) { - return; - } - doTrackTrace(c, trace, recorder); + long snap = traceManager.getCurrentFor(trace).getSnap(); + doTrackTrace(c, trace, recorder, snap); }, "traceOpened"); } + private void traceSnapChanged(DebuggerCoordinates coordinates) { + if (coordinates.getTrace() == null) { + return; + } + processChange(c -> doTrackTrace(c, coordinates.getTrace(), coordinates.getRecorder(), + coordinates.getSnap()), "coordinatesActivated"); + } + private void traceClosed(Trace trace) { processChange(c -> doUntrackTrace(c, trace), "traceClosed"); } @@ -1074,14 +1151,16 @@ public class DebuggerLogicalBreakpointServicePlugin extends Plugin * had to re-implement here, anyway. The actual logical breakpoint is created by event * processors. */ - MappedLogicalBreakpoint lb = new MappedLogicalBreakpoint(program, address, length, kinds); + MappedLogicalBreakpoint lb = + new MappedLogicalBreakpoint(tool, program, address, length, kinds); synchronized (lock) { for (InfoPerTrace ti : traceInfos.values()) { TraceLocation loc = ti.toDynamicLocation(lb.getProgramLocation()); if (loc == null) { continue; } - lb.setTraceAddress(ti.recorder, loc.getAddress()); + lb.setTraceAddress(ti.trace, loc.getAddress()); + lb.setRecorder(ti.trace, ti.recorder); } } return lb; @@ -1097,21 +1176,22 @@ public class DebuggerLogicalBreakpointServicePlugin extends Plugin @Override public CompletableFuture placeBreakpointAt(Trace trace, Address address, long length, Collection kinds, String name) { + long snap = traceManager.getCurrentFor(trace).getSnap(); TraceRecorder recorder = modelService.getRecorder(trace); - if (recorder == null) { - throw new IllegalArgumentException("Given trace is not live"); - } ProgramLocation staticLocation = mappingService.getOpenMappedLocation( - new DefaultTraceLocation(trace, null, Lifespan.at(recorder.getSnap()), address)); + new DefaultTraceLocation(trace, null, Lifespan.at(snap), address)); if (staticLocation == null) { - return new LoneLogicalBreakpoint(recorder, address, length, kinds) - .enableForTrace(trace); + LoneLogicalBreakpoint lb = + new LoneLogicalBreakpoint(tool, trace, address, length, kinds); + lb.setRecorder(trace, recorder); + return lb.enableForTrace(trace); } - MappedLogicalBreakpoint lb = new MappedLogicalBreakpoint(staticLocation.getProgram(), + MappedLogicalBreakpoint lb = new MappedLogicalBreakpoint(tool, staticLocation.getProgram(), staticLocation.getByteAddress(), length, kinds); - lb.setTraceAddress(recorder, address); + lb.setTraceAddress(trace, address); + lb.setRecorder(trace, recorder); lb.enableForProgramWithName(name); return lb.enableForTrace(trace); } @@ -1126,7 +1206,7 @@ public class DebuggerLogicalBreakpointServicePlugin extends Plugin protected CompletableFuture actOnAll(Collection col, Trace trace, Consumer consumerForProgram, - BiConsumer consumerForTarget) { + BiConsumer consumerForTrace) { BreakpointActionSet actions = new BreakpointActionSet(); for (LogicalBreakpoint lb : col) { Set participants = lb.getParticipatingTraces(); @@ -1136,7 +1216,7 @@ public class DebuggerLogicalBreakpointServicePlugin extends Plugin if (!(lb instanceof LogicalBreakpointInternal lbi)) { continue; } - consumerForTarget.accept(actions, lbi); + consumerForTrace.accept(actions, lbi); } return actions.execute(); } @@ -1174,8 +1254,48 @@ public class DebuggerLogicalBreakpointServicePlugin extends Plugin }, (actions, lbi) -> lbi.planDelete(actions, trace)); } + private StateEditingMode getMode(Trace trace) { + return editingService == null + ? StateEditingMode.DEFAULT + : editingService.getCurrentMode(trace); + } + + private void planActOnLoc(BreakpointActionSet actions, TraceBreakpoint tb, + BiConsumer targetLocConsumer, + BiConsumer emuLocConsumer) { + StateEditingMode mode = getMode(tb.getTrace()); + if (mode.useEmulatedBreakpoints()) { + planActOnLocEmu(actions, tb, emuLocConsumer); + } + else { + planActOnLocTarget(actions, tb, targetLocConsumer); + } + } + + private void planActOnLocTarget(BreakpointActionSet actions, TraceBreakpoint tb, + BiConsumer targetLocConsumer) { + TraceRecorder recorder = modelService.getRecorder(tb.getTrace()); + if (recorder == null) { + return; + } + List path = PathUtils.parse(tb.getPath()); + TargetObject object = recorder.getTarget().getModel().getModelObject(path); + if (!(object instanceof TargetBreakpointLocation)) { + Msg.error(this, tb.getPath() + " is not a target breakpoint location"); + return; + } + TargetBreakpointLocation loc = (TargetBreakpointLocation) object; + targetLocConsumer.accept(actions, loc); + } + + private void planActOnLocEmu(BreakpointActionSet actions, TraceBreakpoint tb, + BiConsumer emuLocConsumer) { + emuLocConsumer.accept(actions, tb); + } + protected CompletableFuture actOnLocs(Collection col, - BiConsumer locConsumer, + BiConsumer targetLocConsumer, + BiConsumer emuLocConsumer, Consumer progConsumer) { BreakpointActionSet actions = new BreakpointActionSet(); for (TraceBreakpoint tb : col) { @@ -1183,38 +1303,35 @@ public class DebuggerLogicalBreakpointServicePlugin extends Plugin if (col.containsAll(lb.getTraceBreakpoints())) { progConsumer.accept(lb); } - TraceRecorder recorder = modelService.getRecorder(tb.getTrace()); - if (recorder == null) { - continue; - } - List path = PathUtils.parse(tb.getPath()); - TargetObject object = recorder.getTarget().getModel().getModelObject(path); - if (!(object instanceof TargetBreakpointLocation)) { - Msg.error(this, tb.getPath() + " is not a target breakpoint location"); - continue; - } - TargetBreakpointLocation loc = (TargetBreakpointLocation) object; - locConsumer.accept(actions, loc); + planActOnLoc(actions, tb, targetLocConsumer, emuLocConsumer); } return actions.execute(); } @Override public CompletableFuture enableLocs(Collection col) { - return actOnLocs(col, BreakpointActionSet::planEnable, LogicalBreakpoint::enableForProgram); + return actOnLocs(col, + BreakpointActionSet::planEnableTarget, + BreakpointActionSet::planEnableEmu, + LogicalBreakpoint::enableForProgram); } @Override public CompletableFuture disableLocs(Collection col) { - return actOnLocs(col, BreakpointActionSet::planDisable, + return actOnLocs(col, + BreakpointActionSet::planDisableTarget, + BreakpointActionSet::planDisableEmu, LogicalBreakpoint::disableForProgram); } @Override public CompletableFuture deleteLocs(Collection col) { - return actOnLocs(col, BreakpointActionSet::planDelete, lb -> { - // Never delete bookmark when user requests deleting locations - }); + return actOnLocs(col, + BreakpointActionSet::planDeleteTarget, + BreakpointActionSet::planDeleteEmu, + lb -> { + // Never delete bookmark when user requests deleting locations + }); } @Override @@ -1266,21 +1383,23 @@ public class DebuggerLogicalBreakpointServicePlugin extends Plugin @Override public void processEvent(PluginEvent event) { - if (event instanceof ProgramOpenedPluginEvent) { - ProgramOpenedPluginEvent openedEvt = (ProgramOpenedPluginEvent) event; - programOpened(openedEvt.getProgram()); + if (event instanceof ProgramOpenedPluginEvent evt) { + programOpened(evt.getProgram()); } - else if (event instanceof ProgramClosedPluginEvent) { - ProgramClosedPluginEvent closedEvt = (ProgramClosedPluginEvent) event; - programClosed(closedEvt.getProgram()); + else if (event instanceof ProgramClosedPluginEvent evt) { + programClosed(evt.getProgram()); } - else if (event instanceof TraceOpenedPluginEvent) { - TraceOpenedPluginEvent openedEvt = (TraceOpenedPluginEvent) event; - traceOpened(openedEvt.getTrace()); + else if (event instanceof TraceOpenedPluginEvent evt) { + traceOpened(evt.getTrace()); } - else if (event instanceof TraceClosedPluginEvent) { - TraceClosedPluginEvent closedEvt = (TraceClosedPluginEvent) event; - traceClosed(closedEvt.getTrace()); + else if (event instanceof TraceActivatedPluginEvent evt) { + traceSnapChanged(evt.getActiveCoordinates()); + } + else if (event instanceof TraceInactiveCoordinatesPluginEvent evt) { + traceSnapChanged(evt.getCoordinates()); + } + else if (event instanceof TraceClosedPluginEvent evt) { + traceClosed(evt.getTrace()); } } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/DeleteEmuBreakpointActionItem.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/DeleteEmuBreakpointActionItem.java new file mode 100644 index 0000000000..731ff4927b --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/DeleteEmuBreakpointActionItem.java @@ -0,0 +1,39 @@ +/* ### + * 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.breakpoint; + +import java.util.concurrent.CompletableFuture; + +import ghidra.async.AsyncUtils; +import ghidra.trace.model.breakpoint.TraceBreakpoint; +import ghidra.util.database.UndoableTransaction; + +public record DeleteEmuBreakpointActionItem(TraceBreakpoint bpt) implements BreakpointActionItem { + @Override + public CompletableFuture execute() { + try (UndoableTransaction tid = + UndoableTransaction.start(bpt.getTrace(), "Delete Emulated Breakpoint")) { + String emuName = PlaceEmuBreakpointActionItem.createName(bpt.getMinAddress()); + if (bpt.getPath().contains(emuName)) { + bpt.delete(); + } + else { + bpt.setEmuEnabled(false); + } + } + return AsyncUtils.NIL; + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/DeleteBreakpointActionItem.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/DeleteTargetBreakpointActionItem.java similarity index 57% rename from Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/DeleteBreakpointActionItem.java rename to Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/DeleteTargetBreakpointActionItem.java index c725b4dd61..1a8e6ae2a9 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/DeleteBreakpointActionItem.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/DeleteTargetBreakpointActionItem.java @@ -15,35 +15,12 @@ */ package ghidra.app.plugin.core.debug.service.breakpoint; -import java.util.Objects; import java.util.concurrent.CompletableFuture; import ghidra.dbg.target.TargetDeletable; -public class DeleteBreakpointActionItem implements BreakpointActionItem { - private final TargetDeletable deletable; - - public DeleteBreakpointActionItem(TargetDeletable deletable) { - this.deletable = deletable; - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof DeleteBreakpointActionItem)) { - return false; - } - DeleteBreakpointActionItem that = (DeleteBreakpointActionItem) obj; - if (this.deletable != that.deletable) { - return false; - } - return true; - } - - @Override - public int hashCode() { - return Objects.hash(getClass(), deletable); - } - +public record DeleteTargetBreakpointActionItem(TargetDeletable deletable) + implements BreakpointActionItem { @Override public CompletableFuture execute() { return deletable.delete(); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/DisableEmuBreakpointActionItem.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/DisableEmuBreakpointActionItem.java new file mode 100644 index 0000000000..672138f009 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/DisableEmuBreakpointActionItem.java @@ -0,0 +1,34 @@ +/* ### + * 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.breakpoint; + +import java.util.concurrent.CompletableFuture; + +import ghidra.async.AsyncUtils; +import ghidra.trace.model.breakpoint.TraceBreakpoint; +import ghidra.util.database.UndoableTransaction; + +public record DisableEmuBreakpointActionItem(TraceBreakpoint bpt) + implements BreakpointActionItem { + @Override + public CompletableFuture execute() { + try (UndoableTransaction tid = + UndoableTransaction.start(bpt.getTrace(), "Disable Emulated Breakpoint")) { + bpt.setEmuEnabled(false); + } + return AsyncUtils.NIL; + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/DisableBreakpointActionItem.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/DisableTargetBreakpointActionItem.java similarity index 57% rename from Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/DisableBreakpointActionItem.java rename to Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/DisableTargetBreakpointActionItem.java index 1936302739..69d2ca4363 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/DisableBreakpointActionItem.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/DisableTargetBreakpointActionItem.java @@ -15,35 +15,12 @@ */ package ghidra.app.plugin.core.debug.service.breakpoint; -import java.util.Objects; import java.util.concurrent.CompletableFuture; import ghidra.dbg.target.TargetTogglable; -public class DisableBreakpointActionItem implements BreakpointActionItem { - private final TargetTogglable togglable; - - public DisableBreakpointActionItem(TargetTogglable togglable) { - this.togglable = togglable; - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof DisableBreakpointActionItem)) { - return false; - } - DisableBreakpointActionItem that = (DisableBreakpointActionItem) obj; - if (this.togglable != that.togglable) { - return false; - } - return true; - } - - @Override - public int hashCode() { - return Objects.hash(getClass(), togglable); - } - +public record DisableTargetBreakpointActionItem(TargetTogglable togglable) + implements BreakpointActionItem { @Override public CompletableFuture execute() { return togglable.disable(); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/EnableEmuBreakpointActionItem.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/EnableEmuBreakpointActionItem.java new file mode 100644 index 0000000000..2e830276c6 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/EnableEmuBreakpointActionItem.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.breakpoint; + +import java.util.concurrent.CompletableFuture; + +import ghidra.async.AsyncUtils; +import ghidra.trace.model.breakpoint.TraceBreakpoint; +import ghidra.util.database.UndoableTransaction; + +public record EnableEmuBreakpointActionItem(TraceBreakpoint bpt) implements BreakpointActionItem { + @Override + public CompletableFuture execute() { + try (UndoableTransaction tid = + UndoableTransaction.start(bpt.getTrace(), "Enable Emulated Breakpoint")) { + bpt.setEmuEnabled(true); + } + return AsyncUtils.NIL; + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/EnableBreakpointActionItem.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/EnableTargetBreakpointActionItem.java similarity index 57% rename from Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/EnableBreakpointActionItem.java rename to Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/EnableTargetBreakpointActionItem.java index c175235b6b..8138a885da 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/EnableBreakpointActionItem.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/EnableTargetBreakpointActionItem.java @@ -15,35 +15,12 @@ */ package ghidra.app.plugin.core.debug.service.breakpoint; -import java.util.Objects; import java.util.concurrent.CompletableFuture; import ghidra.dbg.target.TargetTogglable; -public class EnableBreakpointActionItem implements BreakpointActionItem { - private final TargetTogglable togglable; - - public EnableBreakpointActionItem(TargetTogglable togglable) { - this.togglable = togglable; - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof EnableBreakpointActionItem)) { - return false; - } - EnableBreakpointActionItem that = (EnableBreakpointActionItem) obj; - if (this.togglable != that.togglable) { - return false; - } - return true; - } - - @Override - public int hashCode() { - return Objects.hash(getClass(), togglable); - } - +public record EnableTargetBreakpointActionItem(TargetTogglable togglable) + implements BreakpointActionItem { @Override public CompletableFuture execute() { return togglable.enable(); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/LogicalBreakpointInternal.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/LogicalBreakpointInternal.java index 9984273f3c..121288fcef 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/LogicalBreakpointInternal.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/LogicalBreakpointInternal.java @@ -15,427 +15,23 @@ */ package ghidra.app.plugin.core.debug.service.breakpoint; -import java.util.*; - -import com.google.common.collect.Collections2; - import ghidra.app.services.LogicalBreakpoint; import ghidra.app.services.TraceRecorder; -import ghidra.dbg.target.*; -import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; -import ghidra.program.model.address.*; +import ghidra.program.model.address.Address; import ghidra.program.model.listing.*; -import ghidra.program.util.ProgramLocation; import ghidra.trace.model.Trace; import ghidra.trace.model.breakpoint.TraceBreakpoint; -import ghidra.trace.model.breakpoint.TraceBreakpointKind; -import ghidra.trace.model.breakpoint.TraceBreakpointKind.TraceBreakpointKindSet; -import ghidra.util.Msg; -import ghidra.util.database.UndoableTransaction; -import ghidra.util.exception.CancelledException; -import ghidra.util.task.TaskMonitor; -import utilities.util.IDHashed; public interface LogicalBreakpointInternal extends LogicalBreakpoint { - public static class ProgramBreakpoint { - public static Set kindsFromBookmark(Bookmark mark) { - String[] parts = mark.getCategory().split(";"); - Set result = TraceBreakpointKindSet.decode(parts[0], false); - if (result.isEmpty()) { - Msg.warn(TraceBreakpointKind.class, - "Decoded empty set of kinds from bookmark. Assuming SW_EXECUTE"); - return Set.of(TraceBreakpointKind.SW_EXECUTE); - } - return result; - } - - public static long lengthFromBookmark(Bookmark mark) { - String[] parts = mark.getCategory().split(";"); - if (parts.length < 2) { - Msg.warn(DebuggerLogicalBreakpointServicePlugin.class, - "No length for bookmark breakpoint. Assuming 1."); - return 1; - } - try { - long length = Long.parseLong(parts[1]); - if (length <= 0) { - Msg.warn(DebuggerLogicalBreakpointServicePlugin.class, - "Non-positive length for bookmark breakpoint? Using 1."); - return 1; - } - return length; - } - catch (NumberFormatException e) { - Msg.warn(DebuggerLogicalBreakpointServicePlugin.class, - "Ill-formatted bookmark breakpoint length: " + e + ". Using 1."); - return 1; - } - } - - private final Program program; - private final Address address; - private final ProgramLocation location; - private final long length; - private final Set kinds; - - private Bookmark eBookmark; // when present - private Bookmark dBookmark; // when present - - public ProgramBreakpoint(Program program, Address address, long length, - Set kinds) { - this.program = program; - this.address = address; - this.location = new ProgramLocation(program, address); - this.length = length; - this.kinds = kinds; - } - - @Override - public String toString() { - // volatile reads - Bookmark eBookmark = this.eBookmark; - Bookmark dBookmark = this.dBookmark; - if (eBookmark != null) { - return String.format("", eBookmark.getTypeString(), - eBookmark.getCategory(), eBookmark.getAddress(), program.getName()); - } - else if (dBookmark != null) { - return String.format("", dBookmark.getTypeString(), - dBookmark.getCategory(), dBookmark.getAddress(), program.getName()); - } - else { - return String.format("", address, program.getName()); - } - } - - public ProgramLocation getLocation() { - return location; - } - - public String getName() { - // TODO: Be prepared to use JSON or something, if more fields are needed - Bookmark bookmark = getBookmark(); - if (bookmark == null) { - return ""; - } - return bookmark.getComment(); - } - - public void setName(String name) { - Bookmark bookmark = getBookmark(); - if (bookmark == null) { - throw new IllegalStateException("Must save breakpoint to program before naming it"); - } - try (UndoableTransaction tid = - UndoableTransaction.start(program, "Rename breakpoint")) { - bookmark.set(bookmark.getCategory(), name); - } - } - - public ProgramMode computeMode() { - if (eBookmark != null) { - return ProgramMode.ENABLED; - } - if (dBookmark != null) { - return ProgramMode.DISABLED; - } - else { - return ProgramMode.MISSING; - } - } - - public boolean isEmpty() { - return eBookmark == null && dBookmark == null; - } - - public void deleteFromProgram() { - // volatile reads - Bookmark eBookmark = this.eBookmark; - Bookmark dBookmark = this.dBookmark; - try (UndoableTransaction tid = UndoableTransaction.start(program, "Clear breakpoint")) { - BookmarkManager bookmarkManager = program.getBookmarkManager(); - if (eBookmark != null) { - bookmarkManager.removeBookmark(eBookmark); - } - if (dBookmark != null) { - bookmarkManager.removeBookmark(dBookmark); - } - // (e,d)Bookmark Gets nulled on program change callback - // If null here, logical breakpoint manager will get confused - } - } - - public boolean canMerge(Program candProgram, Bookmark candBookmark) { - if (program != candProgram) { - return false; - } - if (!address.equals(candBookmark.getAddress())) { - return false; - } - if (length != lengthFromBookmark(candBookmark)) { - return false; - } - if (!Objects.equals(kinds, kindsFromBookmark(candBookmark))) { - return false; - } - return true; - } - - public Program getProgram() { - return program; - } - - public boolean add(Bookmark bookmark) { - if (BREAKPOINT_ENABLED_BOOKMARK_TYPE.equals(bookmark.getTypeString())) { - if (eBookmark == bookmark) { - return false; - } - eBookmark = bookmark; - return true; - } - if (BREAKPOINT_DISABLED_BOOKMARK_TYPE.equals(bookmark.getTypeString())) { - if (dBookmark == bookmark) { - return false; - } - dBookmark = bookmark; - return true; - } - return false; - } - - public boolean remove(Bookmark bookmark) { - if (eBookmark == bookmark) { - eBookmark = null; - return true; - } - if (dBookmark == bookmark) { - dBookmark = null; - return true; - } - return false; - } - - public Bookmark getBookmark() { - Bookmark eBookmark = this.eBookmark; - if (eBookmark != null) { - return eBookmark; - } - return dBookmark; - } - - protected String getComment() { - Bookmark bookmark = getBookmark(); - return bookmark == null ? "" : bookmark.getComment(); - } - - public boolean isEnabled() { - return computeMode() == ProgramMode.ENABLED; - } - - public boolean isDisabled() { - return computeMode() == ProgramMode.DISABLED; - } - - public String computeCategory() { - return TraceBreakpointKindSet.encode(kinds) + ";" + Long.toUnsignedString(length); - } - - public void toggleWithComment(boolean enabled, String comment) { - String addType = - enabled ? BREAKPOINT_ENABLED_BOOKMARK_TYPE : BREAKPOINT_DISABLED_BOOKMARK_TYPE; - String delType = - enabled ? BREAKPOINT_DISABLED_BOOKMARK_TYPE : BREAKPOINT_ENABLED_BOOKMARK_TYPE; - try (UndoableTransaction tid = - UndoableTransaction.start(program, "Enable breakpoint")) { - BookmarkManager manager = program.getBookmarkManager(); - String catStr = computeCategory(); - manager.setBookmark(address, addType, catStr, comment); - manager.removeBookmarks(new AddressSet(address), delType, catStr, - TaskMonitor.DUMMY); - } - catch (CancelledException e) { - throw new AssertionError(e); - } - } - - public void enable() { - if (isEnabled()) { - return; - } - toggleWithComment(true, getComment()); - } - - public void disable() { - if (isDisabled()) { - return; - } - toggleWithComment(false, getComment()); - } - } - - static class TraceBreakpointSet { - private final TraceRecorder recorder; - private final Trace trace; - private final Address address; - private Set> breakpoints = new HashSet<>(); - - public TraceBreakpointSet(TraceRecorder recorder, Address address) { - this.recorder = recorder; - this.trace = recorder.getTrace(); - this.address = address; - } - - @Override - public String toString() { - return String.format("", address, trace.getName(), breakpoints); - } - - public Trace getTrace() { - return trace; - } - - public Address getAddress() { - return address; - } - - public Address computeTargetAddress() { - return recorder.getMemoryMapper().traceToTarget(address); - } - - public TraceMode computeMode() { - TraceMode mode = TraceMode.NONE; - for (IDHashed bpt : breakpoints) { - mode = mode.combine(computeMode(bpt.obj)); - if (mode == TraceMode.MISSING) { - return mode; - } - } - return mode; - } - - public TraceMode computeMode(TraceBreakpoint bpt) { - return TraceMode.fromBool(bpt.isEnabled(recorder.getSnap())); - } - - public boolean isEmpty() { - return breakpoints.isEmpty(); - } - - public Collection getBreakpoints() { - return Collections2.transform(breakpoints, e -> e.obj); - } - - public boolean add(TraceBreakpoint bpt) { - return breakpoints.add(new IDHashed<>(bpt)); - } - - public boolean canMerge(TraceBreakpoint bpt) { - if (trace != bpt.getTrace()) { - return false; - } - if (!address.equals(bpt.getMinAddress())) { - return false; - } - return true; - } - - public boolean remove(TraceBreakpoint bpt) { - return breakpoints.remove(new IDHashed<>(bpt)); - } - - /** - * Plan to enable a logical breakpoint within the trace. - * - *

    - * This method prefers to use the existing breakpoint specifications which result in - * breakpoints at this address. In other words, it favors what the user has already done to - * effect a breakpoint at this logical breakpoint's address. If there is no such existing - * specification, then it attempts to place a new breakpoint via the target's breakpoint - * container, usually resulting in a new spec, which should effect exactly the one specified - * address. - * - *

    - * This method must convert applicable addresses to the target space. If the address cannot - * be mapped, it's usually because this logical breakpoint does not apply to the given - * trace's target. E.g., the trace may not have a live target, or the logical breakpoint may - * be in a module not loaded by the trace. - * - * @param actions the action set to populate - * @param kind the kind of breakpoint - * @return a future which completes when the plan is ready - */ - public void planEnable(BreakpointActionSet actions, long length, - Collection kinds) { - if (breakpoints.isEmpty()) { - Set tKinds = - TraceRecorder.traceToTargetBreakpointKinds(kinds); - - for (TargetBreakpointSpecContainer cont : recorder - .collectBreakpointContainers(null)) { - LinkedHashSet supKinds = new LinkedHashSet<>(tKinds); - supKinds.retainAll(cont.getSupportedBreakpointKinds()); - actions.add(new PlaceBreakpointActionItem(cont, computeTargetAddress(), length, - supKinds)); - } - return; - } - for (IDHashed bpt : breakpoints) { - TargetBreakpointLocation loc = recorder.getTargetBreakpoint(bpt.obj); - if (loc == null) { - continue; - } - actions.planEnable(loc); - } - } - - public void planDisable(BreakpointActionSet actions, long length, - Collection kinds) { - Set tKinds = TraceRecorder.traceToTargetBreakpointKinds(kinds); - Address targetAddr = computeTargetAddress(); - for (TargetBreakpointLocation loc : recorder.collectBreakpoints(null)) { - AddressRange range = loc.getRange(); - if (!targetAddr.equals(range.getMinAddress())) { - continue; - } - if (length != range.getLength()) { - continue; - } - TargetBreakpointSpec spec = loc.getSpecification(); - if (!Objects.equals(spec.getKinds(), tKinds)) { - continue; - } - actions.planDisable(loc); - } - } - - public void planDelete(BreakpointActionSet actions, long length, - Set kinds) { - Set tKinds = TraceRecorder.traceToTargetBreakpointKinds(kinds); - Address targetAddr = computeTargetAddress(); - for (TargetBreakpointLocation loc : recorder.collectBreakpoints(null)) { - AddressRange range = loc.getRange(); - if (!targetAddr.equals(range.getMinAddress())) { - continue; - } - if (length != range.getLength()) { - continue; - } - TargetBreakpointSpec spec = loc.getSpecification(); - if (!Objects.equals(spec.getKinds(), tKinds)) { - continue; - } - actions.planDelete(loc); - } - } - } - /** * Set the expected address for trace breakpoints in the given trace * - * @param recorder the recorder for the given trace + * @param trace the trace * @param address the address of this logical breakpoint in the given trace */ - void setTraceAddress(TraceRecorder recorder, Address address); + void setTraceAddress(Trace trace, Address address); + + void setRecorder(Trace trace, TraceRecorder recorder); /** * Remove the given trace from this set diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/LoneLogicalBreakpoint.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/LoneLogicalBreakpoint.java index 9b7be1ca93..47ae1a0dd4 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/LoneLogicalBreakpoint.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/LoneLogicalBreakpoint.java @@ -21,6 +21,7 @@ import java.util.concurrent.CompletableFuture; import ghidra.app.services.TraceRecorder; import ghidra.async.AsyncUtils; import ghidra.framework.model.DomainObject; +import ghidra.framework.plugintool.PluginTool; import ghidra.program.model.address.Address; import ghidra.program.model.listing.Bookmark; import ghidra.program.model.listing.Program; @@ -35,13 +36,13 @@ public class LoneLogicalBreakpoint implements LogicalBreakpointInternal { private final long length; private final Set kinds; - public LoneLogicalBreakpoint(TraceRecorder recorder, Address address, long length, + public LoneLogicalBreakpoint(PluginTool tool, Trace trace, Address address, long length, Collection kinds) { - this.breaks = new TraceBreakpointSet(recorder, address); + this.breaks = new TraceBreakpointSet(tool, trace, address); this.length = length; this.kinds = Set.copyOf(kinds); - this.justThisTrace = Set.of(recorder.getTrace()); + this.justThisTrace = Set.of(trace); } @Override @@ -85,10 +86,25 @@ public class LoneLogicalBreakpoint implements LogicalBreakpointInternal { } @Override - public void setTraceAddress(TraceRecorder recorder, Address address) { + public String getEmuSleigh() { + return breaks.computeSleigh(); + } + + @Override + public void setEmuSleigh(String sleigh) { + breaks.setEmuSleigh(sleigh); + } + + @Override + public void setTraceAddress(Trace trace, Address address) { throw new AssertionError(); } + @Override + public void setRecorder(Trace trace, TraceRecorder recorder) { + breaks.setRecorder(recorder); + } + @Override public void removeTrace(Trace trace) { throw new AssertionError(); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/MappedLogicalBreakpoint.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/MappedLogicalBreakpoint.java index 94b2cc3d4f..1a009898c6 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/MappedLogicalBreakpoint.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/MappedLogicalBreakpoint.java @@ -22,6 +22,8 @@ import ghidra.app.services.DebuggerModelService; import ghidra.app.services.TraceRecorder; import ghidra.async.AsyncUtils; import ghidra.framework.model.DomainObject; +import ghidra.framework.plugintool.PluginTool; +import ghidra.pcode.exec.SleighUtils; import ghidra.program.model.address.Address; import ghidra.program.model.listing.Bookmark; import ghidra.program.model.listing.Program; @@ -32,6 +34,7 @@ import ghidra.trace.model.breakpoint.TraceBreakpointKind; public class MappedLogicalBreakpoint implements LogicalBreakpointInternal { + private final PluginTool tool; private final Set kinds; private final long length; @@ -41,10 +44,13 @@ public class MappedLogicalBreakpoint implements LogicalBreakpointInternal { // They may have different names and/or different conditions private final Map traceBreaks = new HashMap<>(); - protected MappedLogicalBreakpoint(Program program, Address progAddr, long length, + protected MappedLogicalBreakpoint(PluginTool tool, Program program, Address progAddr, + long length, Collection kinds) { - this.length = length; + this.tool = tool; this.kinds = Set.copyOf(kinds); + this.length = length; + this.progBreak = new ProgramBreakpoint(program, progAddr, length, this.kinds); } @@ -83,26 +89,20 @@ public class MappedLogicalBreakpoint implements LogicalBreakpointInternal { return recorder; } - /** - * TODO: How best to allow the user to control behavior when a new static mapping is created - * that could generate new breakpoints in the target. Options: - * - * 1) A toggle during mapping creation for how to sync breakpoints. - * - * 2) A prompt when the mapping is created, asking the user to sync breakpoints. - * - * 3) A UI indicator (probably in the breakpoints provider) that some breakpoints are not in - * sync. Different actions for directions of sync. - * - * I'm thinking both 1 and 3. Option 2 is probably too abrasive, esp., if several mappings are - * created at once. - */ - @Override public void enableForProgram() { progBreak.enable(); } + /** + * Place the program's bookmark with a comment specifying a desired name + * + *

    + * WARNING: Use only when this breakpoint was just placed, otherwise, this will reset + * other extrinsic properties, such as the sleigh injection. + * + * @param name the desired name + */ public void enableForProgramWithName(String name) { progBreak.toggleWithComment(true, name); } @@ -194,21 +194,20 @@ public class MappedLogicalBreakpoint implements LogicalBreakpointInternal { @Override public String generateStatusEnable(Trace trace) { + // NB. It'll place a breakpoint if mappable but not present if (trace == null) { - for (TraceBreakpointSet breaks : traceBreaks.values()) { - if (!breaks.isEmpty()) { - return null; - } + if (!traceBreaks.values().isEmpty()) { + return null; } - return "A breakpoint is not mapped to any live trace. Cannot enable it on target. " + + return "A breakpoint is not mapped to any trace. Cannot enable it. " + "Is there a target? Check your module map."; } TraceBreakpointSet breaks = traceBreaks.get(trace); - if (breaks != null && !breaks.isEmpty()) { + if (breaks != null) { return null; } - return "A breakpoint is not mapped to the trace, or the trace is not live. " + - "Cannot enable it on target. Is there a target? Check your module map."; + return "A breakpoint is not mapped to the trace. " + + "Cannot enable it. Is there a target? Check your module map."; } @Override @@ -296,9 +295,18 @@ public class MappedLogicalBreakpoint implements LogicalBreakpointInternal { } @Override - public void setTraceAddress(TraceRecorder recorder, Address address) { + public void setTraceAddress(Trace trace, Address address) { synchronized (traceBreaks) { - traceBreaks.put(recorder.getTrace(), new TraceBreakpointSet(recorder, address)); + TraceBreakpointSet newSet = new TraceBreakpointSet(tool, trace, address); + newSet.setEmuSleigh(progBreak.getEmuSleigh()); + traceBreaks.put(trace, newSet); + } + } + + @Override + public void setRecorder(Trace trace, TraceRecorder recorder) { + synchronized (traceBreaks) { + traceBreaks.get(trace).setRecorder(recorder); } } @@ -440,6 +448,39 @@ public class MappedLogicalBreakpoint implements LogicalBreakpointInternal { return progMode.combineTrace(traceMode, Perspective.LOGICAL); } + protected String computeTraceSleigh() { + String sleigh = null; + synchronized (traceBreaks) { + for (TraceBreakpointSet breaks : traceBreaks.values()) { + String s = breaks.computeSleigh(); + if (sleigh != null && !sleigh.equals(s)) { + return null; + } + sleigh = s; + } + return sleigh; + } + } + + @Override + public String getEmuSleigh() { + return progBreak.getEmuSleigh(); + } + + protected void setTraceBreakEmuSleigh(String sleigh) { + synchronized (traceBreaks) { + for (TraceBreakpointSet breaks : traceBreaks.values()) { + breaks.setEmuSleigh(sleigh); + } + } + } + + @Override + public void setEmuSleigh(String sleigh) { + progBreak.setEmuSleigh(sleigh); + setTraceBreakEmuSleigh(sleigh); + } + @Override public boolean canMerge(Program program, Bookmark bookmark) { return progBreak.canMerge(program, bookmark); @@ -473,10 +514,21 @@ public class MappedLogicalBreakpoint implements LogicalBreakpointInternal { @Override public boolean trackBreakpoint(Bookmark bookmark) { - return progBreak.add(bookmark); + if (progBreak.add(bookmark)) { + String sleigh = progBreak.getEmuSleigh(); + if (sleigh != null && !SleighUtils.UNCONDITIONAL_BREAK.equals(sleigh)) { + setEmuSleigh(sleigh); + } + return true; + } + return false; } protected void makeBookmarkConsistent() { + // NB. Apparently it is specified that the bookmark should be created automatically + /*if (progBreak.isEmpty()) { + return; + }*/ TraceMode traceMode = computeTraceMode(); if (traceMode == TraceMode.ENABLED) { progBreak.enable(); @@ -493,6 +545,15 @@ public class MappedLogicalBreakpoint implements LogicalBreakpointInternal { breaks = traceBreaks.get(breakpoint.getTrace()); } boolean result = breaks.add(breakpoint); + if (result) { + String traceSleigh = computeTraceSleigh(); + if (traceSleigh != null && !SleighUtils.UNCONDITIONAL_BREAK.equals(traceSleigh)) { + String progSleigh = progBreak.getEmuSleigh(); + if (progSleigh == null || SleighUtils.UNCONDITIONAL_BREAK.equals(progSleigh)) { + progBreak.setEmuSleigh(traceSleigh); + } + } + } makeBookmarkConsistent(); return result; } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/PlaceBreakpointActionItem.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/PlaceBreakpointActionItem.java deleted file mode 100644 index 76c2c9962b..0000000000 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/PlaceBreakpointActionItem.java +++ /dev/null @@ -1,73 +0,0 @@ -/* ### - * 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.breakpoint; - -import java.util.*; -import java.util.concurrent.CompletableFuture; - -import ghidra.dbg.target.TargetBreakpointSpecContainer; -import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; -import ghidra.program.model.address.*; - -public class PlaceBreakpointActionItem implements BreakpointActionItem { - private final TargetBreakpointSpecContainer container; - private final Address address; - private final long length; - private final Set kinds; - - public PlaceBreakpointActionItem(TargetBreakpointSpecContainer container, Address address, - long length, Collection kinds) { - this.container = Objects.requireNonNull(container); - this.address = Objects.requireNonNull(address); - this.length = length; - this.kinds = Set.copyOf(kinds); - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof PlaceBreakpointActionItem)) { - return false; - } - PlaceBreakpointActionItem that = (PlaceBreakpointActionItem) obj; - if (this.container != that.container) { - return false; - } - if (!this.address.equals(that.address)) { - return false; - } - if (!Objects.equals(this.kinds, that.kinds)) { - return false; - } - return true; - } - - @Override - public int hashCode() { - return Objects.hash(getClass(), container, address, kinds); - } - - @Override - public CompletableFuture execute() { - AddressRange range; - try { - range = new AddressRangeImpl(address, length); - } - catch (AddressOverflowException e) { - throw new AssertionError(e); - } - return container.placeBreakpoint(range, kinds); - } -} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/PlaceEmuBreakpointActionItem.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/PlaceEmuBreakpointActionItem.java new file mode 100644 index 0000000000..f7e0d5b92a --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/PlaceEmuBreakpointActionItem.java @@ -0,0 +1,114 @@ +/* ### + * 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.breakpoint; + +import java.util.*; +import java.util.concurrent.CompletableFuture; + +import ghidra.async.AsyncUtils; +import ghidra.dbg.target.TargetBreakpointSpec; +import ghidra.dbg.target.TargetBreakpointSpecContainer; +import ghidra.dbg.util.PathMatcher; +import ghidra.program.model.address.*; +import ghidra.trace.model.Lifespan; +import ghidra.trace.model.Trace; +import ghidra.trace.model.breakpoint.TraceBreakpoint; +import ghidra.trace.model.breakpoint.TraceBreakpointKind; +import ghidra.trace.model.memory.TraceMemoryRegion; +import ghidra.trace.model.memory.TraceObjectMemoryRegion; +import ghidra.trace.model.target.TraceObject; +import ghidra.util.database.UndoableTransaction; +import ghidra.util.exception.DuplicateNameException; + +public record PlaceEmuBreakpointActionItem(Trace trace, long snap, Address address, long length, + Set kinds, String emuSleigh) implements BreakpointActionItem { + + public static String createName(Address address) { + return "emu-" + address; + } + + public PlaceEmuBreakpointActionItem(Trace trace, long snap, Address address, long length, + Set kinds, String emuSleigh) { + this.trace = trace; + this.snap = snap; + this.address = address; + this.length = length; + this.kinds = Set.copyOf(kinds); + this.emuSleigh = emuSleigh; + } + + private TraceObjectMemoryRegion findRegion() { + TraceMemoryRegion region = trace.getMemoryManager().getRegionContaining(snap, address); + if (region != null) { + return (TraceObjectMemoryRegion) region; + } + AddressSpace space = address.getAddressSpace(); + Collection regionsInSpace = trace.getMemoryManager() + .getRegionsIntersecting(Lifespan.at(snap), + new AddressRangeImpl(space.getMinAddress(), space.getMaxAddress())); + if (!regionsInSpace.isEmpty()) { + return (TraceObjectMemoryRegion) regionsInSpace.iterator().next(); + } + return null; + } + + private TraceObject findBreakpointContainer() { + TraceObjectMemoryRegion region = findRegion(); + if (region == null) { + throw new IllegalArgumentException("Address does not belong to a memory in the trace"); + } + return region.getObject().querySuitableTargetInterface(TargetBreakpointSpecContainer.class); + } + + private String computePath() { + String name = createName(address); + if (Trace.isLegacy(trace)) { + return "Breakpoints[" + name + "]"; + } + TraceObject container = findBreakpointContainer(); + if (container == null) { + throw new IllegalArgumentException( + "Address is not associated with a breakpoint container"); + } + PathMatcher specMatcher = + container.getTargetSchema().searchFor(TargetBreakpointSpec.class, true); + if (specMatcher == null) { + throw new IllegalArgumentException("Cannot find path to breakpoint specifications"); + } + List relPath = specMatcher.applyKeys(name).getSingletonPath(); + if (relPath == null) { + throw new IllegalArgumentException("Too many wildcards to breakpoint specification"); + } + return container.getCanonicalPath().extend(relPath).toString(); + } + + @Override + public CompletableFuture execute() { + try (UndoableTransaction tid = + UndoableTransaction.start(trace, "Place Emulated Breakpoint")) { + // Defaults with emuEnable=true + TraceBreakpoint bpt = trace.getBreakpointManager() + .addBreakpoint(computePath(), Lifespan.at(snap), range(address, length), + Set.of(), kinds, false, null); + bpt.setName(createName(address)); + bpt.setEmuSleigh(emuSleigh); + return AsyncUtils.NIL; + } + catch (DuplicateNameException e) { + throw new AssertionError(e); + } + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/PlaceTargetBreakpointActionItem.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/PlaceTargetBreakpointActionItem.java new file mode 100644 index 0000000000..d17406f773 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/PlaceTargetBreakpointActionItem.java @@ -0,0 +1,41 @@ +/* ### + * 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.breakpoint; + +import java.util.*; +import java.util.concurrent.CompletableFuture; + +import ghidra.dbg.target.TargetBreakpointSpecContainer; +import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; +import ghidra.program.model.address.*; + +public record PlaceTargetBreakpointActionItem(TargetBreakpointSpecContainer container, + Address address, long length, Set kinds) + implements BreakpointActionItem { + + public PlaceTargetBreakpointActionItem(TargetBreakpointSpecContainer container, Address address, + long length, Set kinds) { + this.container = container; + this.address = address; + this.length = length; + this.kinds = Set.copyOf(kinds); + } + + @Override + public CompletableFuture execute() { + return container.placeBreakpoint(range(address, length), kinds); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/ProgramBreakpoint.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/ProgramBreakpoint.java new file mode 100644 index 0000000000..6655789ddd --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/ProgramBreakpoint.java @@ -0,0 +1,496 @@ +/* ### + * 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.breakpoint; + +import java.util.*; + +import com.google.gson.*; + +import ghidra.app.services.LogicalBreakpoint.ProgramMode; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressSet; +import ghidra.program.model.listing.*; +import ghidra.program.util.ProgramLocation; +import ghidra.trace.model.breakpoint.TraceBreakpointKind; +import ghidra.trace.model.breakpoint.TraceBreakpointKind.TraceBreakpointKindSet; +import ghidra.util.Msg; +import ghidra.util.database.UndoableTransaction; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +/** + * The static side of a mapped logical breakpoint + * + *

    + * Programs don't have a built-in concept of breakpoints, so we store them as breakpoints with a + * specific type for each state. We also encode other intrinsic properties (length and kinds) to the + * category. Extrinsic properties (name and sleigh) are encoded in the comment. Because traces are + * fairly ephemeral, the program bookmarks are the primary means a user has to save and manage a + * breakpoint set. + */ +public class ProgramBreakpoint { + private static final Gson GSON = new GsonBuilder().create(); + + /** + * A class for (de)serializing breakoint properties in the bookmark's comments + */ + static class BreakpointProperties { + public String name; + public String sleigh; + + public BreakpointProperties(String name, String sleigh) { + this.name = name; + this.sleigh = sleigh; + } + } + + /** + * Get the kinds of a breakpoint from its bookmark + * + * @param mark the bookmark representing a breakpoint + * @return the kinds + */ + public static Set kindsFromBookmark(Bookmark mark) { + String[] parts = mark.getCategory().split(";"); + Set result = TraceBreakpointKindSet.decode(parts[0], false); + if (result.isEmpty()) { + Msg.warn(TraceBreakpointKind.class, + "Decoded empty set of kinds from bookmark. Assuming SW_EXECUTE"); + return Set.of(TraceBreakpointKind.SW_EXECUTE); + } + return result; + } + + /** + * Get the length of a breakpoint from its bookmark + * + * @param mark the bookmark representing a breakpoint + * @return the length in bytes + */ + public static long lengthFromBookmark(Bookmark mark) { + String[] parts = mark.getCategory().split(";"); + if (parts.length < 2) { + Msg.warn(DebuggerLogicalBreakpointServicePlugin.class, + "No length for bookmark breakpoint. Assuming 1."); + return 1; + } + try { + long length = Long.parseLong(parts[1]); + if (length <= 0) { + Msg.warn(DebuggerLogicalBreakpointServicePlugin.class, + "Non-positive length for bookmark breakpoint? Using 1."); + return 1; + } + return length; + } + catch (NumberFormatException e) { + Msg.warn(DebuggerLogicalBreakpointServicePlugin.class, + "Ill-formatted bookmark breakpoint length: " + e + ". Using 1."); + return 1; + } + } + + private final Program program; + private final Address address; + private final ProgramLocation location; + private final long length; + private final Set kinds; + + private Bookmark eBookmark; // when present + private Bookmark dBookmark; // when present + + private String name; + private String sleigh; + + /** + * Construct a program breakpoint + * + * @param program the program + * @param address the static address of the breakpoint (even if a bookmark is not present there) + * @param length the length of the breakpoint in bytes + * @param kinds the kinds of the breakpoint + */ + public ProgramBreakpoint(Program program, Address address, long length, + Set kinds) { + this.program = program; + this.address = address; + this.location = new ProgramLocation(program, address); + this.length = length; + this.kinds = kinds; + } + + @Override + public String toString() { + // volatile reads + Bookmark eBookmark = this.eBookmark; + Bookmark dBookmark = this.dBookmark; + if (eBookmark != null) { + return String.format("", eBookmark.getTypeString(), + eBookmark.getCategory(), eBookmark.getAddress(), program.getName()); + } + else if (dBookmark != null) { + return String.format("", dBookmark.getTypeString(), + dBookmark.getCategory(), dBookmark.getAddress(), program.getName()); + } + else { + return String.format("", address, program.getName()); + } + } + + /** + * Get the breakpoint's static program location + * + * @return the location + */ + public ProgramLocation getLocation() { + return location; + } + + private void syncProperties(Bookmark bookmark) { + if (bookmark == null) { + name = ""; + sleigh = null; + return; + } + String comment = bookmark.getComment(); + if (comment == null || !comment.startsWith("{")) { + // Backward compatibility. + name = comment; + sleigh = null; + return; + } + try { + BreakpointProperties props = GSON.fromJson(comment, BreakpointProperties.class); + name = props.name; + sleigh = props.sleigh; + return; + } + catch (JsonSyntaxException e) { + Msg.error(this, "Could not parse breakpoint bookmark properties", e); + name = ""; + sleigh = null; + return; + } + } + + private String computeComment() { + if ((name == null || "".equals(name)) && (sleigh == null || "".equals(sleigh))) { + return null; + } + return GSON.toJson(new BreakpointProperties(name, sleigh)); + } + + private void writeProperties(Bookmark bookmark) { + try (UndoableTransaction tid = + UndoableTransaction.start(program, "Rename breakpoint")) { + bookmark.set(bookmark.getCategory(), computeComment()); + } + catch (ConcurrentModificationException e) { + /** + * Can happen during breakpoint deletion. Doesn't seem like there's a good way to check. + * In any case, we need to keep processing events, so log and continue. + */ + Msg.error(this, "Could not update breakpoint properties: " + e); + } + } + + /** + * Get the user-defined name of the breakpoint + * + * @return the name + */ + public String getName() { + return name; + } + + /** + * Set the name of the breakpoint + * + * @param name the name + */ + public void setName(String name) { + Bookmark bookmark = getBookmark(); + if (bookmark == null) { + throw new IllegalStateException("Must save breakpoint to program before naming it"); + } + this.name = name; + writeProperties(bookmark); + } + + /** + * Get the sleigh injection for this breakpoint + * + * @return the sleigh injection + */ + public String getEmuSleigh() { + return sleigh; + } + + /*** + * Set the sleigh injection for this breakpoint + * + * @param sleigh the sleigh injection + */ + public void setEmuSleigh(String sleigh) { + this.sleigh = sleigh; + Bookmark bookmark = getBookmark(); + if (bookmark == null) { + return; + } + writeProperties(bookmark); + } + + /** + * Compute the mode of this breakpoint + * + *

    + * In order to ensure at least the saved state (enablement) can be rendered in the marker margin + * in the absence of the breakpoint marker plugin, we use one type of bookmark for disabled + * breakpoints, and another for enabled breakpoints. As the state is changing, it's possible for + * a brief moment that both bookmarks are present. We thus have a variable for each bookmark and + * prefer the "enabled" state. We can determine are state by examining which variable is + * non-null. If both are null, the breakpoint is not actually saved to the program, yet. We + * cannot return {@link ProgramMode#NONE}, because that would imply there is no static location. + * + * @return the state + */ + public ProgramMode computeMode() { + if (eBookmark != null) { + return ProgramMode.ENABLED; + } + if (dBookmark != null) { + return ProgramMode.DISABLED; + } + return ProgramMode.MISSING; + } + + /** + * Check if either bookmark is present + * + * @return true if both are absent, false if either or both is present + */ + public boolean isEmpty() { + return eBookmark == null && dBookmark == null; + } + + /** + * Remove the bookmark + * + *

    + * Note this does not necessarily destroy the breakpoint, since it may still exist in one or + * more traces. + */ + public void deleteFromProgram() { + // volatile reads + Bookmark eBookmark = this.eBookmark; + Bookmark dBookmark = this.dBookmark; + try (UndoableTransaction tid = UndoableTransaction.start(program, "Clear breakpoint")) { + BookmarkManager bookmarkManager = program.getBookmarkManager(); + if (eBookmark != null) { + bookmarkManager.removeBookmark(eBookmark); + } + if (dBookmark != null) { + bookmarkManager.removeBookmark(dBookmark); + } + // (e,d)Bookmark Gets nulled on program change callback + // If null here, logical breakpoint manager will get confused + } + } + + /** + * Check if the given bookmark can fill the static side of this breakpoint + * + * @param candProgram the program containing the bookmark + * @param candBookmark the bookmark + * @return true if the bookmark can represent this breakpoint, false otherwise + */ + public boolean canMerge(Program candProgram, Bookmark candBookmark) { + if (program != candProgram) { + return false; + } + if (!address.equals(candBookmark.getAddress())) { + return false; + } + if (length != lengthFromBookmark(candBookmark)) { + return false; + } + if (!Objects.equals(kinds, kindsFromBookmark(candBookmark))) { + return false; + } + return true; + } + + /** + * Get the program where this breakpoint is located + * + * @return the program + */ + public Program getProgram() { + return program; + } + + /** + * Fill the static side of this breakpoint with the given bookmark + * + *

    + * The caller should first use {@link #canMerge(Program, Bookmark)} to ensure the bookmark can + * actually represent this breakpoint. + * + * @param bookmark the bookmark + * @return true if this changed the breakpoint state + */ + public boolean add(Bookmark bookmark) { + if (LogicalBreakpointInternal.BREAKPOINT_ENABLED_BOOKMARK_TYPE + .equals(bookmark.getTypeString())) { + if (eBookmark == bookmark) { + return false; + } + eBookmark = bookmark; + syncProperties(bookmark); + return true; + } + if (LogicalBreakpointInternal.BREAKPOINT_DISABLED_BOOKMARK_TYPE + .equals(bookmark.getTypeString())) { + if (dBookmark == bookmark) { + return false; + } + dBookmark = bookmark; + syncProperties(bookmark); + return true; + } + return false; + } + + /** + * Remove a bookmark from the static side of this breakpoint + * + * @param bookmark the bookmark + * @return true if this changed the breakpoint state + */ + public boolean remove(Bookmark bookmark) { + if (eBookmark == bookmark) { + eBookmark = null; + return true; + } + if (dBookmark == bookmark) { + dBookmark = null; + return true; + } + return false; + } + + /** + * Get the bookmark representing this breakpoint, if present + * + * @return the bookmark or null + */ + public Bookmark getBookmark() { + Bookmark eBookmark = this.eBookmark; + if (eBookmark != null) { + return eBookmark; + } + return dBookmark; + } + + protected String getComment() { + Bookmark bookmark = getBookmark(); + return bookmark == null ? computeComment() : bookmark.getComment(); + } + + /** + * Check if the bookmark represents an enabled breakpoint + * + * @return true if enabled, false if anything else + */ + public boolean isEnabled() { + return computeMode() == ProgramMode.ENABLED; + } + + /** + * Check if the bookmark represents a disabled breakpoint + * + * @return true if disabled, false if anything else + */ + public boolean isDisabled() { + return computeMode() == ProgramMode.DISABLED; + } + + /** + * Compute the category for a new bookmark representing this breakpoint + * + * @return the category + */ + public String computeCategory() { + return TraceBreakpointKindSet.encode(kinds) + ";" + Long.toUnsignedString(length); + } + + /** + * Change the state of this breakpoint by manipulating bookmarks + * + *

    + * If the breakpoint is already in the desired state, no change is made. Otherwise, this will + * delete the existing bookmark, if present, and create a new bookmark whose type indicates the + * desired state. Thus, some event processing may need to take place before this breakpoint's + * state is actually updated accordingly. + * + * @param enabled the desired state, true for {@link ProgramMode#ENABLED}, false for + * {@link ProgramMode#DISABLED}. + * @param comment the comment to give the breakpoint, almost always from {@link #getComment()}. + */ + public void toggleWithComment(boolean enabled, String comment) { + String addType = + enabled ? LogicalBreakpointInternal.BREAKPOINT_ENABLED_BOOKMARK_TYPE + : LogicalBreakpointInternal.BREAKPOINT_DISABLED_BOOKMARK_TYPE; + String delType = + enabled ? LogicalBreakpointInternal.BREAKPOINT_DISABLED_BOOKMARK_TYPE + : LogicalBreakpointInternal.BREAKPOINT_ENABLED_BOOKMARK_TYPE; + try (UndoableTransaction tid = + UndoableTransaction.start(program, "Enable breakpoint")) { + BookmarkManager manager = program.getBookmarkManager(); + String catStr = computeCategory(); + manager.setBookmark(address, addType, catStr, comment); + manager.removeBookmarks(new AddressSet(address), delType, catStr, + TaskMonitor.DUMMY); + } + catch (CancelledException e) { + throw new AssertionError(e); + } + } + + /** + * Enable this breakpoint + * + * @see #toggleWithComment(boolean, String) + */ + public void enable() { + if (isEnabled()) { + return; + } + toggleWithComment(true, getComment()); + } + + /** + * Disable this breakpoint + * + * @see #toggleWithComment(boolean, String) + */ + public void disable() { + if (isDisabled()) { + return; + } + toggleWithComment(false, getComment()); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/TraceBreakpointSet.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/TraceBreakpointSet.java new file mode 100644 index 0000000000..f25cb18f8a --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/TraceBreakpointSet.java @@ -0,0 +1,463 @@ +/* ### + * 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.breakpoint; + +import java.util.*; + +import com.google.common.collect.Collections2; + +import ghidra.app.services.*; +import ghidra.app.services.LogicalBreakpoint.TraceMode; +import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; +import ghidra.framework.plugintool.PluginTool; +import ghidra.pcode.exec.SleighUtils; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressRange; +import ghidra.trace.model.Trace; +import ghidra.trace.model.breakpoint.TraceBreakpoint; +import ghidra.trace.model.breakpoint.TraceBreakpointKind; +import ghidra.util.database.UndoableTransaction; +import utilities.util.IDHashed; + +/** + * The trace side of a logical breakpoint + * + *

    + * If the logical breakpoint is a mapped, it will have one of these sets for each trace where the + * breakpoint has (or could have) a location. For a lone logical breakpoint, it will have just one + * of these for the one trace where its located. + */ +class TraceBreakpointSet { + private final PluginTool tool; + private final Trace trace; + private final Address address; + + private final Set> breakpoints = new HashSet<>(); + + private TraceRecorder recorder; + private String emuSleigh; + + /** + * Create a set of breakpoint locations for a given trace + * + * @param tool the plugin tool for the UI + * @param trace the trace whose locations this set collects + * @param address the dynamic address where the breakpoint is (or would be) located + */ + public TraceBreakpointSet(PluginTool tool, Trace trace, Address address) { + this.tool = Objects.requireNonNull(tool); + this.trace = Objects.requireNonNull(trace); + this.address = Objects.requireNonNull(address); + } + + @Override + public String toString() { + return String.format("", address, trace.getName(), breakpoints); + } + + /** + * Set the recorder when the trace is associated to a live target + * + * @param recorder the recorder + */ + public void setRecorder(TraceRecorder recorder) { + this.recorder = recorder; + } + + private StateEditingMode getStateEditingMode() { + DebuggerStateEditingService service = tool.getService(DebuggerStateEditingService.class); + return service == null ? StateEditingMode.DEFAULT : service.getCurrentMode(trace); + } + + private long getSnap() { + /** + * TODO: Not exactly ideal.... It'd be nice to have it passed in, but that's infecting a lot + * of methods and putting a burden on the caller, when in most cases, it's going to be the + * "current snap" anyway. + */ + DebuggerTraceManagerService service = tool.getService(DebuggerTraceManagerService.class); + if (service == null) { + return trace.getProgramView().getViewport().getReversedSnaps().get(0); + } + return service.getCurrentFor(trace).getSnap(); + } + + /** + * Get the trace + * + * @return + */ + public Trace getTrace() { + return trace; + } + + /** + * Get the dynamic address where the breakpoint is (or would be) located in this trace + * + * @return the dynamic address + */ + public Address getAddress() { + return address; + } + + /** + * If there is a live target, get the dynamic address in the target's space + * + * @return the dynamic address on target + */ + public Address computeTargetAddress() { + if (recorder == null) { + throw new AssertionError(); + } + return recorder.getMemoryMapper().traceToTarget(address); + } + + /** + * Compute the mode (enablement) of this set + * + *

    + * In most cases, there is 0 or 1 trace breakpoints that "fit" the logical breakpoint. The mode + * is derived from one of {@link TraceBreakpoint#isEnabled(long)} or + * {@link TraceBreakpoint#isEmuEnabled(long)}, depending on the UI's control mode for this + * trace. + * + * @return the mode + */ + public TraceMode computeMode() { + TraceMode mode = TraceMode.NONE; + if (getStateEditingMode().useEmulatedBreakpoints()) { + for (IDHashed bpt : breakpoints) { + mode = mode.combine(computeEmuMode(bpt.obj)); + if (mode == TraceMode.MISSING) { + return mode; + } + } + return mode; + } + for (IDHashed bpt : breakpoints) { + mode = mode.combine(computeTargetMode(bpt.obj)); + if (mode == TraceMode.MISSING) { + return mode; + } + } + return mode; + } + + /** + * Compute the mode (enablement) of the given breakpoint + * + *

    + * The mode is derived from one of {@link TraceBreakpoint#isEnabled(long)} or + * {@link TraceBreakpoint#isEmuEnabled(long)}, depending on the UI's control mode for this + * trace. + * + * @param bpt the breakpoint + * @return the mode + */ + public TraceMode computeMode(TraceBreakpoint bpt) { + return getStateEditingMode().useEmulatedBreakpoints() + ? computeEmuMode(bpt) + : computeTargetMode(bpt); + } + + /** + * Compute the mode of the given breakpoint for the target + * + * @param bpt the breakpoint + * @return the mode + */ + public TraceMode computeTargetMode(TraceBreakpoint bpt) { + return TraceMode.fromBool(bpt.isEnabled(getSnap())); + } + + /** + * Compute the mode of the given breakpoint for the emulator + * + * @param bpt the breakpoint + * @return the mode + */ + public TraceMode computeEmuMode(TraceBreakpoint bpt) { + return TraceMode.fromBool(bpt.isEmuEnabled(getSnap())); + } + + /** + * If all breakpoints agree on sleigh injection, get that injection + * + * @return the injection, or null if there's disagreement. + */ + public String computeSleigh() { + String sleigh = null; + for (IDHashed bpt : breakpoints) { + String s = bpt.obj.getEmuSleigh(); + if (sleigh != null && !sleigh.equals(s)) { + return null; + } + sleigh = s; + } + return sleigh; + } + + /** + * Set the sleigh injection for all breakpoints in this set + * + * @param emuSleigh the sleigh injection + */ + public void setEmuSleigh(String emuSleigh) { + this.emuSleigh = emuSleigh; + try (UndoableTransaction tid = UndoableTransaction.start(trace, "Set breakpoint Sleigh")) { + for (IDHashed bpt : breakpoints) { + bpt.obj.setEmuSleigh(emuSleigh); + } + } + } + + /** + * Check if this set actually contains any trace breakpoints + * + * @return true if empty, false otherwise + */ + public boolean isEmpty() { + return breakpoints.isEmpty(); + } + + /** + * Get the breakpoints in this set + * + * @return the breakpoints + */ + public Collection getBreakpoints() { + return Collections2.transform(breakpoints, e -> e.obj); + } + + /** + * Add a breakpoint to this set + * + *

    + * The caller should first call {@link #canMerge(TraceBreakpoint)} to check if the breakpoint + * "fits." + * + * @param bpt + * @return true if the set actually changed as a result + */ + public boolean add(TraceBreakpoint bpt) { + if (SleighUtils.UNCONDITIONAL_BREAK.equals(bpt.getEmuSleigh()) && emuSleigh != null) { + try (UndoableTransaction tid = + UndoableTransaction.start(trace, "Set breakpoint Sleigh")) { + bpt.setEmuSleigh(emuSleigh); + } + } + return breakpoints.add(new IDHashed<>(bpt)); + } + + /** + * Check if the given trace breakpoint "fits" in this set + * + *

    + * The breakpoint fits if it's dynamic location matches that expected in this set + * + * @param bpt the breakpoint + * @return true if it fits + */ + public boolean canMerge(TraceBreakpoint bpt) { + if (trace != bpt.getTrace()) { + return false; + } + if (!address.equals(bpt.getMinAddress())) { + return false; + } + return true; + } + + /** + * Remove a breakpoint from this set + * + * @param bpt the breakpoint + * @return true if the set actually changes as a result + */ + public boolean remove(TraceBreakpoint bpt) { + return breakpoints.remove(new IDHashed<>(bpt)); + } + + /** + * Plan to enable the logical breakpoint within this trace + * + *

    + * This method prefers to use the existing breakpoint specifications which result in breakpoints + * at this address. In other words, it favors what the user has already done to effect a + * breakpoint at this logical breakpoint's address. If there is no such existing specification, + * then it attempts to place a new breakpoint via the target's breakpoint container, usually + * resulting in a new spec, which should effect exactly the one specified address. If the + * control mode indicates emulated breakpoints, then this simply writes the breakpoint to the + * trace database. + * + *

    + * This method may convert applicable addresses to the target space. If the address cannot be + * mapped, it's usually because this logical breakpoint does not apply to the given trace's + * target. E.g., the trace may not have a live target, or the logical breakpoint may be in a + * module not loaded by the trace. + * + * @param actions the action set to populate + * @param length the length in bytes of the breakpoint + * @param kinds the kinds of breakpoint + */ + public void planEnable(BreakpointActionSet actions, long length, + Collection kinds) { + long snap = getSnap(); + if (breakpoints.isEmpty()) { + if (recorder == null || getStateEditingMode().useEmulatedBreakpoints()) { + planPlaceEmu(actions, snap, length, kinds); + } + else { + planPlaceTarget(actions, snap, length, kinds); + } + } + else { + if (recorder == null || getStateEditingMode().useEmulatedBreakpoints()) { + planEnableEmu(actions); + } + else { + planEnableTarget(actions); + } + } + } + + private void planPlaceTarget(BreakpointActionSet actions, long snap, long length, + Collection kinds) { + if (snap != recorder.getSnap()) { + throw new AssertionError("Target breakpoints must be requested at present snap"); + } + Set tKinds = + TraceRecorder.traceToTargetBreakpointKinds(kinds); + + for (TargetBreakpointSpecContainer cont : recorder + .collectBreakpointContainers(null)) { + LinkedHashSet supKinds = new LinkedHashSet<>(tKinds); + supKinds.retainAll(cont.getSupportedBreakpointKinds()); + actions.add(new PlaceTargetBreakpointActionItem(cont, computeTargetAddress(), + length, supKinds)); + } + } + + private void planPlaceEmu(BreakpointActionSet actions, long snap, long length, + Collection kinds) { + actions.add( + new PlaceEmuBreakpointActionItem(trace, snap, address, length, Set.copyOf(kinds), + emuSleigh)); + } + + private void planEnableTarget(BreakpointActionSet actions) { + for (IDHashed bpt : breakpoints) { + TargetBreakpointLocation loc = recorder.getTargetBreakpoint(bpt.obj); + if (loc == null) { + continue; + } + actions.planEnableTarget(loc); + } + } + + private void planEnableEmu(BreakpointActionSet actions) { + for (IDHashed bpt : breakpoints) { + actions.planEnableEmu(bpt.obj); + } + } + + /** + * Plan to disable the logical breakpoint in this trace + * + * @param actions the action set to populate + * @param length the length in bytes of the breakpoint + * @param kinds the kinds of breakpoint + */ + public void planDisable(BreakpointActionSet actions, long length, + Collection kinds) { + if (getStateEditingMode().useEmulatedBreakpoints()) { + planDisableEmu(actions); + } + else { + planDisableTarget(actions, length, kinds); + } + } + + private void planDisableTarget(BreakpointActionSet actions, long length, + Collection kinds) { + Set tKinds = TraceRecorder.traceToTargetBreakpointKinds(kinds); + Address targetAddr = computeTargetAddress(); + for (TargetBreakpointLocation loc : recorder.collectBreakpoints(null)) { + AddressRange range = loc.getRange(); + if (!targetAddr.equals(range.getMinAddress())) { + continue; + } + if (length != range.getLength()) { + continue; + } + TargetBreakpointSpec spec = loc.getSpecification(); + if (!Objects.equals(spec.getKinds(), tKinds)) { + continue; + } + actions.planDisableTarget(loc); + } + } + + private void planDisableEmu(BreakpointActionSet actions) { + for (IDHashed bpt : breakpoints) { + actions.planDisableEmu(bpt.obj); + } + } + + /** + * Plan to delete the logical breakpoint in this trace + * + * @param actions the action set to populate + * @param length the length in bytes of the breakpoint + * @param kinds the kinds of breakpoint + */ + public void planDelete(BreakpointActionSet actions, long length, + Set kinds) { + if (getStateEditingMode().useEmulatedBreakpoints()) { + planDeleteEmu(actions); + } + else { + planDeleteTarget(actions, length, kinds); + } + } + + private void planDeleteTarget(BreakpointActionSet actions, long length, + Set kinds) { + Set tKinds = TraceRecorder.traceToTargetBreakpointKinds(kinds); + Address targetAddr = computeTargetAddress(); + for (TargetBreakpointLocation loc : recorder.collectBreakpoints(null)) { + AddressRange range = loc.getRange(); + if (!targetAddr.equals(range.getMinAddress())) { + continue; + } + if (length != range.getLength()) { + continue; + } + TargetBreakpointSpec spec = loc.getSpecification(); + if (!Objects.equals(spec.getKinds(), tKinds)) { + continue; + } + actions.planDeleteTarget(loc); + } + } + + private void planDeleteEmu(BreakpointActionSet actions) { + for (IDHashed bpt : breakpoints) { + actions.planDeleteEmu(bpt.obj); + } + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/editing/DebuggerStateEditingServicePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/editing/DebuggerStateEditingServicePlugin.java index 6477158de8..a270db4c80 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/editing/DebuggerStateEditingServicePlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/editing/DebuggerStateEditingServicePlugin.java @@ -21,32 +21,19 @@ import java.util.concurrent.*; import ghidra.app.plugin.PluginCategoryNames; import ghidra.app.plugin.core.debug.*; -import ghidra.app.plugin.core.debug.event.TraceClosedPluginEvent; -import ghidra.app.plugin.core.debug.event.TraceOpenedPluginEvent; +import ghidra.app.plugin.core.debug.event.*; import ghidra.app.services.*; -import ghidra.async.AsyncUtils; +import ghidra.app.services.DebuggerTraceManagerService.ActivationCause; import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.annotation.AutoServiceConsumed; import ghidra.framework.plugintool.util.PluginStatus; import ghidra.program.model.address.Address; -import ghidra.program.model.address.AddressRange; -import ghidra.program.model.lang.Language; -import ghidra.program.model.lang.Register; import ghidra.program.model.mem.*; import ghidra.trace.model.Trace; import ghidra.trace.model.Trace.TraceProgramViewListener; -import ghidra.trace.model.guest.TracePlatform; -import ghidra.trace.model.memory.TraceMemoryOperations; -import ghidra.trace.model.memory.TraceMemorySpace; -import ghidra.trace.model.program.*; -import ghidra.trace.model.thread.TraceThread; -import ghidra.trace.model.time.schedule.PatchStep; -import ghidra.trace.model.time.schedule.TraceSchedule; -import ghidra.trace.util.TraceRegisterUtils; -import ghidra.util.database.UndoableTransaction; +import ghidra.trace.model.program.TraceProgramView; +import ghidra.trace.model.program.TraceProgramViewMemory; import ghidra.util.datastruct.ListenerSet; -import ghidra.util.exception.CancelledException; -import ghidra.util.task.TaskMonitor; @PluginInfo( shortDescription = "Debugger machine-state editing service plugin", @@ -56,6 +43,7 @@ import ghidra.util.task.TaskMonitor; status = PluginStatus.RELEASED, eventsConsumed = { TraceOpenedPluginEvent.class, + TraceActivatedPluginEvent.class, TraceClosedPluginEvent.class, }, servicesRequired = { @@ -73,158 +61,14 @@ public class DebuggerStateEditingServicePlugin extends AbstractDebuggerPlugin public boolean isVariableEditable(Address address, int length) { DebuggerCoordinates coordinates = getCoordinates(); Trace trace = coordinates.getTrace(); - - switch (getCurrentMode(trace)) { - case READ_ONLY: - return false; - case WRITE_TARGET: - return isTargetVariableEditable(coordinates, address, length); - case WRITE_TRACE: - return isTraceVariableEditable(coordinates, address, length); - case WRITE_EMULATOR: - return isEmulatorVariableEditable(coordinates, address, length); - } - throw new AssertionError(); - } - - protected boolean isTargetVariableEditable(DebuggerCoordinates coordinates, Address address, - int length) { - if (!coordinates.isAliveAndPresent()) { - return false; - } - TraceRecorder recorder = coordinates.getRecorder(); - return recorder.isVariableOnTarget(coordinates.getPlatform(), coordinates.getThread(), - coordinates.getFrame(), address, length); - } - - protected boolean isTraceVariableEditable(DebuggerCoordinates coordinates, Address address, - int length) { - return address.isMemoryAddress() || coordinates.getThread() != null; - } - - protected boolean isEmulatorVariableEditable(DebuggerCoordinates coordinates, - Address address, int length) { - if (coordinates.getThread() == null) { - // A limitation in TraceSchedule, which is used to manifest patches - return false; - } - if (!isTraceVariableEditable(coordinates, address, length)) { - return false; - } - // TODO: Limitation from using Sleigh for patching - Register ctxReg = coordinates.getTrace().getBaseLanguage().getContextBaseRegister(); - if (ctxReg == Register.NO_CONTEXT) { - return true; - } - AddressRange ctxRange = TraceRegisterUtils.rangeForRegister(ctxReg); - if (ctxRange.contains(address)) { - return false; - } - return true; + return getCurrentMode(trace).isVariableEditable(coordinates, address, length); } @Override public CompletableFuture setVariable(Address address, byte[] data) { DebuggerCoordinates coordinates = getCoordinates(); Trace trace = coordinates.getTrace(); - - StateEditingMode mode = getCurrentMode(trace); - switch (mode) { - case READ_ONLY: - return CompletableFuture - .failedFuture(new MemoryAccessException("Read-only mode")); - case WRITE_TARGET: - return writeTargetVariable(coordinates, address, data); - case WRITE_TRACE: - return writeTraceVariable(coordinates, address, data); - case WRITE_EMULATOR: - return writeEmulatorVariable(coordinates, address, data); - } - throw new AssertionError(); - } - - protected CompletableFuture writeTargetVariable(DebuggerCoordinates coordinates, - Address address, byte[] data) { - TraceRecorder recorder = coordinates.getRecorder(); - if (recorder == null) { - return CompletableFuture - .failedFuture(new MemoryAccessException("Trace has no live target")); - } - if (!coordinates.isPresent()) { - return CompletableFuture - .failedFuture(new MemoryAccessException("View is not the present")); - } - return recorder.writeVariable(coordinates.getPlatform(), coordinates.getThread(), - coordinates.getFrame(), address, data); - } - - protected CompletableFuture writeTraceVariable(DebuggerCoordinates coordinates, - Address guestAddress, byte[] data) { - Trace trace = coordinates.getTrace(); - TracePlatform platform = coordinates.getPlatform(); - long snap = coordinates.getViewSnap(); - Address hostAddress = platform.mapGuestToHost(guestAddress); - if (hostAddress == null) { - throw new IllegalArgumentException( - "Guest address " + guestAddress + " is not mapped"); - } - TraceMemoryOperations memOrRegs; - Address overlayAddress; - try (UndoableTransaction txid = - UndoableTransaction.start(trace, "Edit Variable")) { - if (hostAddress.isRegisterAddress()) { - TraceThread thread = coordinates.getThread(); - if (thread == null) { - throw new IllegalArgumentException("Register edits require a thread."); - } - TraceMemorySpace regs = trace.getMemoryManager() - .getMemoryRegisterSpace(thread, coordinates.getFrame(), - true); - memOrRegs = regs; - overlayAddress = regs.getAddressSpace().getOverlayAddress(hostAddress); - } - else { - memOrRegs = trace.getMemoryManager(); - overlayAddress = hostAddress; - } - if (memOrRegs.putBytes(snap, overlayAddress, - ByteBuffer.wrap(data)) != data.length) { - return CompletableFuture.failedFuture(new MemoryAccessException()); - } - } - return AsyncUtils.NIL; - } - - protected CompletableFuture writeEmulatorVariable(DebuggerCoordinates coordinates, - Address address, byte[] data) { - if (!(coordinates.getView() instanceof TraceVariableSnapProgramView)) { - throw new IllegalArgumentException("Cannot emulate using a Fixed Program View"); - } - TraceThread thread = coordinates.getThread(); - if (thread == null) { - // TODO: Well, technically, only for register edits - // It's a limitation in TraceSchedule. Every step requires a thread - throw new IllegalArgumentException("Emulator edits require a thread."); - } - Language language = coordinates.getPlatform().getLanguage(); - TraceSchedule time = coordinates.getTime() - .patched(thread, language, PatchStep.generateSleigh(language, address, data)); - - DebuggerCoordinates withTime = coordinates.time(time); - Long found = traceManager.findSnapshot(withTime); - // Materialize it on the same thread (even if swing) - // It shouldn't take long, since we're only appending one step. - if (found == null) { - // TODO: Could still do it async on another thread, no? - // Not sure it buys anything, since program view will call .get on swing thread - try { - emulationSerivce.emulate(coordinates.getPlatform(), time, TaskMonitor.DUMMY); - } - catch (CancelledException e) { - throw new AssertionError(e); - } - } - return traceManager.activateAndNotify(withTime, false); + return getCurrentMode(trace).setVariable(tool, coordinates, address, data); } } @@ -356,10 +200,6 @@ public class DebuggerStateEditingServicePlugin extends AbstractDebuggerPlugin //@AutoServiceConsumed // via method private DebuggerTraceManagerService traceManager; - @AutoServiceConsumed - private DebuggerEmulationService emulationSerivce; - @AutoServiceConsumed - private DebuggerModelService modelService; protected final ListenerForEditorInstallation listenerForEditorInstallation = new ListenerForEditorInstallation(); @@ -368,8 +208,6 @@ public class DebuggerStateEditingServicePlugin extends AbstractDebuggerPlugin super(tool); } - private static final StateEditingMode DEFAULT_MODE = StateEditingMode.WRITE_TARGET; - private final Map currentModes = new HashMap<>(); private final ListenerSet listeners = @@ -378,23 +216,23 @@ public class DebuggerStateEditingServicePlugin extends AbstractDebuggerPlugin @Override public StateEditingMode getCurrentMode(Trace trace) { synchronized (currentModes) { - return currentModes.getOrDefault(Objects.requireNonNull(trace), DEFAULT_MODE); + return currentModes.getOrDefault(Objects.requireNonNull(trace), + StateEditingMode.DEFAULT); } } @Override - public void setCurrentMode(Trace trace, StateEditingMode mode) { - boolean fire = false; + public void setCurrentMode(Trace trace, StateEditingMode newMode) { + StateEditingMode oldMode; synchronized (currentModes) { - StateEditingMode old = - currentModes.getOrDefault(Objects.requireNonNull(trace), DEFAULT_MODE); - if (mode != old) { - currentModes.put(trace, mode); - fire = true; + oldMode = + currentModes.getOrDefault(Objects.requireNonNull(trace), StateEditingMode.DEFAULT); + if (newMode != oldMode) { + currentModes.put(trace, newMode); } } - if (fire) { - listeners.fire.modeChanged(trace, mode); + if (newMode != oldMode) { + listeners.fire.modeChanged(trace, newMode); tool.contextChanged(null); } } @@ -424,6 +262,29 @@ public class DebuggerStateEditingServicePlugin extends AbstractDebuggerPlugin return new FollowsViewStateEditor(view); } + protected void coordinatesActivated(DebuggerCoordinates coordinates, ActivationCause cause) { + if (cause != ActivationCause.USER) { + return; + } + Trace trace = coordinates.getTrace(); + if (trace == null) { + return; + } + StateEditingMode oldMode; + StateEditingMode newMode; + synchronized (currentModes) { + oldMode = currentModes.getOrDefault(trace, StateEditingMode.DEFAULT); + newMode = oldMode.modeOnChange(coordinates); + if (newMode != oldMode) { + currentModes.put(trace, newMode); + } + } + if (newMode != oldMode) { + listeners.fire.modeChanged(trace, newMode); + tool.contextChanged(null); + } + } + protected void installMemoryEditor(TraceProgramView view) { TraceProgramViewMemory memory = view.getMemory(); if (memory.getLiveMemoryHandler() != null) { @@ -481,13 +342,14 @@ public class DebuggerStateEditingServicePlugin extends AbstractDebuggerPlugin @Override public void processEvent(PluginEvent event) { super.processEvent(event); - if (event instanceof TraceOpenedPluginEvent) { - TraceOpenedPluginEvent ev = (TraceOpenedPluginEvent) event; - installAllMemoryEditors(ev.getTrace()); + if (event instanceof TraceOpenedPluginEvent evt) { + installAllMemoryEditors(evt.getTrace()); } - else if (event instanceof TraceClosedPluginEvent) { - TraceClosedPluginEvent ev = (TraceClosedPluginEvent) event; - uninstallAllMemoryEditors(ev.getTrace()); + else if (event instanceof TraceActivatedPluginEvent evt) { + coordinatesActivated(evt.getActiveCoordinates(), evt.getCause()); + } + else if (event instanceof TraceClosedPluginEvent evt) { + uninstallAllMemoryEditors(evt.getTrace()); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/DebuggerEmulationServicePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/DebuggerEmulationServicePlugin.java index a6ee33ed06..7008c30e85 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/DebuggerEmulationServicePlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/DebuggerEmulationServicePlugin.java @@ -41,7 +41,8 @@ import ghidra.async.AsyncLazyMap; import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.annotation.AutoServiceConsumed; import ghidra.framework.plugintool.util.PluginStatus; -import ghidra.pcode.emu.PcodeMachine.AccessKind; +import ghidra.pcode.emu.PcodeMachine.*; +import ghidra.pcode.exec.InjectionErrorPcodeExecutionException; import ghidra.program.model.address.*; import ghidra.program.model.listing.Program; import ghidra.program.util.ProgramLocation; @@ -191,7 +192,12 @@ public class DebuggerEmulationServicePlugin extends Plugin implements DebuggerEm @Override protected EmulationResult compute(TaskMonitor monitor) throws CancelledException { - return doRun(from, monitor, scheduler); + EmulationResult result = doRun(from, monitor, scheduler); + if (result.error() instanceof InjectionErrorPcodeExecutionException) { + Msg.showError(this, null, "Breakpoint Emulation Error", + "Compilation error in user-provided breakpoint Sleigh code."); + } + return result; } } @@ -260,6 +266,8 @@ public class DebuggerEmulationServicePlugin extends Plugin implements DebuggerEm private DebuggerPlatformService platformService; @AutoServiceConsumed private DebuggerStaticMappingService staticMappings; + @AutoServiceConsumed + private DebuggerStateEditingService editingService; @SuppressWarnings("unused") private AutoService.Wiring autoServiceWiring; @@ -359,6 +367,9 @@ public class DebuggerEmulationServicePlugin extends Plugin implements DebuggerEm trace = ProgramEmulationUtils.launchEmulationTrace(program, ctx.getAddress(), this); traceManager.openTrace(trace); traceManager.activateTrace(trace); + if (editingService != null) { + editingService.setCurrentMode(trace, StateEditingMode.RW_EMULATOR); + } } catch (IOException e) { Msg.showError(this, null, actionEmulateProgram.getDescription(), @@ -544,7 +555,7 @@ public class DebuggerEmulationServicePlugin extends Plugin implements DebuggerEm for (AddressSpace as : trace.getBaseAddressFactory().getAddressSpaces()) { for (TraceBreakpoint bpt : bm.getBreakpointsIntersecting(span, new AddressRangeImpl(as.getMinAddress(), as.getMaxAddress()))) { - if (!bpt.isEnabled(snap)) { + if (!bpt.isEmuEnabled(snap)) { continue; } Set kinds = bpt.getKinds(); @@ -554,7 +565,14 @@ public class DebuggerEmulationServicePlugin extends Plugin implements DebuggerEm boolean isRead = kinds.contains(TraceBreakpointKind.READ); boolean isWrite = kinds.contains(TraceBreakpointKind.WRITE); if (isExecute) { - emu.addBreakpoint(bpt.getMinAddress(), "1:1"); + try { + emu.inject(bpt.getMinAddress(), bpt.getEmuSleigh()); + } + catch (Exception e) { // This is a bit broad... + Msg.error(this, + "Error compiling breakpoint Sleigh at " + bpt.getMinAddress(), e); + emu.inject(bpt.getMinAddress(), "emu_injection_err();"); + } } if (isRead && isWrite) { emu.addAccessBreakpoint(bpt.getRange(), AccessKind.RW); @@ -592,6 +610,7 @@ public class DebuggerEmulationServicePlugin extends Plugin implements DebuggerEm emu.clearAllInjects(); emu.clearAccessBreakpoints(); emu.setSuspended(false); + installBreakpoints(key.trace, key.time.getSnap(), be.ce.emulator()); monitor.initialize(time.totalTickCount() - prevKey.time.totalTickCount()); createRegisterSpaces(trace, time, monitor); @@ -603,6 +622,7 @@ public class DebuggerEmulationServicePlugin extends Plugin implements DebuggerEm DebuggerPcodeMachine emu = emulatorFactory.create(tool, platform, time.getSnap(), modelService == null ? null : modelService.getRecorder(trace)); try (BusyEmu be = new BusyEmu(new CachedEmulator(key.trace, emu))) { + installBreakpoints(key.trace, key.time.getSnap(), be.ce.emulator()); monitor.initialize(time.totalTickCount()); createRegisterSpaces(trace, time, monitor); monitor.setMessage("Emulating"); @@ -627,7 +647,15 @@ public class DebuggerEmulationServicePlugin extends Plugin implements DebuggerEm protected TraceSnapshot writeToScratch(CacheKey key, CachedEmulator ce) { try (UndoableTransaction tid = UndoableTransaction.start(key.trace, "Emulate")) { TraceSnapshot destSnap = findScratch(key.trace, key.time); - ce.emulator().writeDown(key.platform, destSnap.getKey(), key.time.getSnap()); + try { + ce.emulator().writeDown(key.platform, destSnap.getKey(), key.time.getSnap()); + } + catch (Throwable e) { + Msg.showError(this, null, "Emulate", + "There was an issue writing the emulation result to trace trace. " + + "The displayed state may be inaccurate and/or incomplete.", + e); + } return destSnap; } } @@ -643,10 +671,11 @@ public class DebuggerEmulationServicePlugin extends Plugin implements DebuggerEm protected EmulationResult doRun(CacheKey key, TaskMonitor monitor, Scheduler scheduler) throws CancelledException { try (BusyEmu be = doEmulateFromCached(key, monitor)) { - installBreakpoints(key.trace, key.time.getSnap(), be.ce.emulator()); TraceThread eventThread = key.time.getEventThread(key.trace); + be.ce.emulator().setSoftwareInterruptMode(SwiMode.IGNORE_STEP); RunResult result = scheduler.run(key.trace, eventThread, be.ce.emulator(), monitor); key = new CacheKey(key.platform, key.time.advanced(result.schedule())); + Msg.info(this, "Stopped emulation at " + key.time); TraceSnapshot destSnap = writeToScratch(key, be.ce); cacheEmulator(key, be.ce); return new RecordEmulationResult(key.time, destSnap.getKey(), result.error()); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/ProgramEmulationUtils.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/ProgramEmulationUtils.java index bc9b811203..cb0903a0c9 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/ProgramEmulationUtils.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/ProgramEmulationUtils.java @@ -23,6 +23,9 @@ import java.util.stream.Stream; import ghidra.app.plugin.core.debug.service.modules.DebuggerStaticMappingUtils; import ghidra.app.services.DebuggerEmulationService; +import ghidra.dbg.target.*; +import ghidra.dbg.target.schema.TargetObjectSchema; +import ghidra.dbg.util.*; import ghidra.framework.model.DomainFile; import ghidra.program.model.address.*; import ghidra.program.model.lang.*; @@ -34,8 +37,9 @@ import ghidra.trace.database.DBTrace; import ghidra.trace.model.*; import ghidra.trace.model.memory.*; import ghidra.trace.model.modules.TraceConflictedMappingException; -import ghidra.trace.model.thread.TraceThread; -import ghidra.trace.model.thread.TraceThreadManager; +import ghidra.trace.model.target.TraceObject; +import ghidra.trace.model.target.TraceObjectKeyPath; +import ghidra.trace.model.thread.*; import ghidra.trace.model.time.TraceSnapshot; import ghidra.util.*; import ghidra.util.database.UndoableTransaction; @@ -137,10 +141,11 @@ public enum ProgramEmulationUtils { * A transaction must already be started on the destination trace. * * @param snapshot the destination snapshot, usually 0 - * @param program the progam to load + * @param program the program to load */ public static void loadExecutable(TraceSnapshot snapshot, Program program) { Trace trace = snapshot.getTrace(); + PathPattern patRegion = computePatternRegion(trace); Map extremaBySpace = new HashMap<>(); try { for (MemoryBlock block : program.getMemory().getBlocks()) { @@ -156,8 +161,9 @@ public enum ProgramEmulationUtils { String modName = getModuleName(program); // TODO: Do I populate modules, since the mapping will already be done? - String path = "Modules[" + modName + "].Sections[" + block.getName() + "-" + - block.getStart() + "]"; + String path = PathUtils.toString(patRegion + .applyKeys(block.getStart() + "-" + modName + ":" + block.getName()) + .getSingletonPath()); trace.getMemoryManager() .createRegion(path, snapshot.getKey(), range, getRegionFlags(block)); } @@ -176,6 +182,28 @@ public enum ProgramEmulationUtils { // N.B. Bytes will be loaded lazily } + public static PathPattern computePattern(Trace trace, Class iface) { + TargetObjectSchema root = trace.getObjectManager().getRootSchema(); + if (root == null) { + return new PathPattern(PathUtils.parse("Memory[]")); + } + PathMatcher matcher = root.searchFor(iface, true); + PathPattern pattern = matcher.getSingletonPattern(); + if (pattern == null || pattern.countWildcards() != 1) { + throw new IllegalArgumentException( + "Cannot find unique " + iface.getSimpleName() + " container"); + } + return pattern; + } + + public static PathPattern computePatternRegion(Trace trace) { + return computePattern(trace, TargetMemoryRegion.class); + } + + public static PathPattern computePatternThread(Trace trace) { + return computePattern(trace, TargetThread.class); + } + /** * Spawn a new thread in the given trace at the given creation snap * @@ -188,12 +216,16 @@ public enum ProgramEmulationUtils { */ public static TraceThread spawnThread(Trace trace, long snap) { TraceThreadManager tm = trace.getThreadManager(); + PathPattern patThread = computePatternThread(trace); long next = tm.getAllThreads().size(); - while (!tm.getThreadsByPath("Threads[" + next + "]").isEmpty()) { + String path; + while (!tm.getThreadsByPath(path = + PathUtils.toString(patThread.applyKeys(Long.toString(next)).getSingletonPath())) + .isEmpty()) { next++; } try { - return tm.createThread("Threads[" + next + "]", "[" + next + "]", snap); + return tm.createThread(path, "[" + next + "]", snap); } catch (DuplicateNameException e) { throw new AssertionError(e); @@ -214,6 +246,20 @@ public enum ProgramEmulationUtils { public static void initializeRegisters(Trace trace, long snap, TraceThread thread, Program program, Address tracePc, Address programPc, TraceMemoryRegion stack) { TraceMemoryManager memory = trace.getMemoryManager(); + if (thread instanceof TraceObjectThread ot) { + TraceObject object = ot.getObject(); + PathPredicates regsMatcher = object.getRoot() + .getTargetSchema() + .searchForRegisterContainer(0, object.getCanonicalPath().getKeyList()); + if (regsMatcher.isEmpty()) { + throw new IllegalArgumentException("Cannot create register container"); + } + for (PathPattern regsPattern : regsMatcher.getPatterns()) { + trace.getObjectManager() + .createObject(TraceObjectKeyPath.of(regsPattern.getSingletonPath())); + break; + } + } TraceMemorySpace regSpace = memory.getMemoryRegisterSpace(thread, true); if (program != null) { ProgramContext ctx = program.getProgramContext(); @@ -269,11 +315,18 @@ public enum ProgramEmulationUtils { TraceMemoryManager mm = trace.getMemoryManager(); AddressSetView left = new DifferenceAddressSetView(except0, mm.getRegionsAddressSet(snap)); + PathPattern patRegion = computePatternRegion(trace); try { for (AddressRange candidate : left) { if (Long.compareUnsigned(candidate.getLength(), size) > 0) { AddressRange alloc = new AddressRangeImpl(candidate.getMinAddress(), size); - return mm.createRegion(thread.getPath() + ".Stack", snap, alloc, + String threadName = PathUtils.isIndex(thread.getName()) + ? PathUtils.parseIndex(thread.getName()) + : thread.getName(); + String path = PathUtils.toString( + patRegion.applyKeys(alloc.getMinAddress() + "-stack " + threadName) + .getSingletonPath()); + return mm.createRegion(path, snap, alloc, TraceMemoryFlag.READ, TraceMemoryFlag.WRITE); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServicePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServicePlugin.java index 45451c2cfc..9d06e75f10 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServicePlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServicePlugin.java @@ -40,6 +40,7 @@ import ghidra.app.plugin.core.debug.mapping.*; import ghidra.app.plugin.core.debug.service.model.launch.DebuggerProgramLaunchOffer; import ghidra.app.plugin.core.debug.service.model.launch.DebuggerProgramLaunchOpinion; import ghidra.app.services.*; +import ghidra.app.services.DebuggerTraceManagerService.ActivationCause; import ghidra.async.AsyncFence; import ghidra.dbg.*; import ghidra.dbg.target.*; @@ -459,7 +460,8 @@ public class DebuggerModelServicePlugin extends Plugin if (traceManager != null) { Trace trace = recorder.getTrace(); traceManager.openTrace(trace); - traceManager.activateTrace(trace); + traceManager.activate(traceManager.resolveTrace(trace), + ActivationCause.ACTIVATE_DEFAULT); } return recorder; } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java index 273db9727d..be9ba6fc64 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java @@ -32,6 +32,7 @@ import ghidra.app.plugin.core.debug.event.*; import ghidra.app.plugin.core.debug.gui.DebuggerResources.*; import ghidra.app.plugin.core.debug.mapping.DebuggerPlatformMapper; import ghidra.app.services.*; +import ghidra.app.services.DebuggerStateEditingService.StateEditingModeChangeListener; import ghidra.async.*; import ghidra.async.AsyncConfigFieldCodec.BooleanAsyncConfigFieldCodec; import ghidra.dbg.target.*; @@ -112,7 +113,7 @@ public class DebuggerTraceManagerServicePlugin extends Plugin if (supportsFocus(recorder)) { // TODO: Same for stack frame? I can't imagine it's as common as this.... if (thread == recorder.getTraceThreadForSuccessor(recorder.getFocus())) { - activate(current.thread(thread)); + activate(current.thread(thread), ActivationCause.SYNC_MODEL); } return; } @@ -122,16 +123,18 @@ public class DebuggerTraceManagerServicePlugin extends Plugin if (current.getThread() != null) { return; } - activate(current.thread(thread)); + activate(current.thread(thread), ActivationCause.ACTIVATE_DEFAULT); } private void threadDeleted(TraceThread thread) { - DebuggerCoordinates last = lastCoordsByTrace.get(trace); - if (last != null && last.getThread() == thread) { - lastCoordsByTrace.remove(trace); + synchronized (listenersByTrace) { + DebuggerCoordinates last = lastCoordsByTrace.get(trace); + if (last != null && last.getThread() == thread) { + lastCoordsByTrace.remove(trace); + } } if (current.getThread() == thread) { - activate(current.thread(null)); + activate(current.thread(null), ActivationCause.ACTIVATE_DEFAULT); } } @@ -146,7 +149,7 @@ public class DebuggerTraceManagerServicePlugin extends Plugin if (!object.isRoot()) { return; } - activate(current.object(object)); + activate(current.object(object), ActivationCause.SYNC_MODEL); } } @@ -231,17 +234,32 @@ public class DebuggerTraceManagerServicePlugin extends Plugin } } + class ForFollowPresentListener implements StateEditingModeChangeListener { + @Override + public void modeChanged(Trace trace, StateEditingMode mode) { + if (trace != current.getTrace() || !mode.followsPresent()) { + return; + } + TraceRecorder curRecorder = current.getRecorder(); + if (curRecorder == null) { + return; + } + // TODO: Also re-sync focused thread/frame? + activateNoFocus(current.snap(curRecorder.getSnap()), ActivationCause.FOLLOW_PRESENT); + } + } + protected final Map lastCoordsByTrace = new WeakHashMap<>(); protected final Map listenersByTrace = new WeakHashMap<>(); protected final Set tracesView = Collections.unmodifiableSet(listenersByTrace.keySet()); private final ForRecordersListener forRecordersListener = new ForRecordersListener(); + private final ForFollowPresentListener forFollowPresentListener = + new ForFollowPresentListener(); protected DebuggerCoordinates current = DebuggerCoordinates.NOWHERE; protected TargetObject curObj; @AutoConfigStateField(codec = BooleanAsyncConfigFieldCodec.class) - protected final AsyncReference autoActivatePresent = new AsyncReference<>(true); - @AutoConfigStateField(codec = BooleanAsyncConfigFieldCodec.class) protected final AsyncReference saveTracesByDefault = new AsyncReference<>(true); @AutoConfigStateField(codec = BooleanAsyncConfigFieldCodec.class) protected final AsyncReference synchronizeFocus = new AsyncReference<>(true); @@ -254,6 +272,8 @@ public class DebuggerTraceManagerServicePlugin extends Plugin private DebuggerEmulationService emulationService; @AutoServiceConsumed private DebuggerPlatformService platformService; + // @AutoServiceConsumed via method + private DebuggerStateEditingService editingService; @SuppressWarnings("unused") private final AutoService.Wiring autoServiceWiring; @@ -448,6 +468,17 @@ public class DebuggerTraceManagerServicePlugin extends Plugin } } + @AutoServiceConsumed + private void setEditingService(DebuggerStateEditingService editingService) { + if (this.editingService != null) { + this.editingService.removeModeChangeListener(forFollowPresentListener); + } + this.editingService = editingService; + if (this.editingService != null) { + this.editingService.addModeChangeListener(forFollowPresentListener); + } + } + @Override public Class[] getSupportedDataTypes() { return new Class[] { Trace.class }; @@ -574,20 +605,38 @@ public class DebuggerTraceManagerServicePlugin extends Plugin return false; } } - activateNoFocus(getCurrentFor(trace).object(obj)); + activateNoFocus(getCurrentFor(trace).object(obj), ActivationCause.SYNC_MODEL); return true; } + private boolean isFollowsPresent(Trace trace) { + StateEditingMode mode = editingService == null + ? StateEditingMode.DEFAULT + : editingService.getCurrentMode(trace); + return mode.followsPresent(); + } + protected void doTraceRecorderAdvanced(TraceRecorder recorder, long snap) { - if (!autoActivatePresent.get()) { + Trace trace = recorder.getTrace(); + if (!isFollowsPresent(trace)) { return; } - if (recorder.getTrace() != current.getTrace()) { - // TODO: Could advance view, which might be desirable anyway - // Would also obviate checks in resolveCoordinates and updateCurrentRecorder + if (trace != current.getTrace()) { + /** + * The snap needs to match upon re-activating this trace, lest it look like the user + * intentionally navigated to the past, causing the mode to switch away from target. + */ + DebuggerCoordinates inactive = null; + synchronized (listenersByTrace) { + DebuggerCoordinates curForTrace = getCurrentFor(trace); + inactive = curForTrace.snap(snap); + lastCoordsByTrace.put(trace, inactive); + } + trace.getProgramView().setSnap(snap); + firePluginEvent(new TraceInactiveCoordinatesPluginEvent(getName(), inactive)); return; } - activateSnap(snap); + activate(resolveSnap(snap), ActivationCause.FOLLOW_PRESENT); } protected TracePlatform getPlatformForMapper(Trace trace, TraceObject object, @@ -607,7 +656,7 @@ public class DebuggerTraceManagerServicePlugin extends Plugin lastCoordsByTrace.put(trace, adj); if (trace == current.getTrace()) { current = adj; - fireLocationEvent(adj); + fireLocationEvent(adj, ActivationCause.MAPPER_CHANGED); } } } @@ -628,12 +677,10 @@ public class DebuggerTraceManagerServicePlugin extends Plugin return; } DebuggerCoordinates toActivate = current.recorder(recorder); - if (autoActivatePresent.get()) { - activate(toActivate.snap(recorder.getSnap())); - } - else { - activate(toActivate); + if (isFollowsPresent(current.getTrace())) { + toActivate = toActivate.snap(recorder.getSnap()); } + activate(toActivate, ActivationCause.FOLLOW_PRESENT); } @Override @@ -740,10 +787,11 @@ public class DebuggerTraceManagerServicePlugin extends Plugin return emulationService.backgroundEmulate(coordinates.getPlatform(), coordinates.getTime()); } - protected CompletableFuture prepareViewAndFireEvent(DebuggerCoordinates coordinates) { + protected CompletableFuture prepareViewAndFireEvent(DebuggerCoordinates coordinates, + ActivationCause cause) { TraceVariableSnapProgramView varView = (TraceVariableSnapProgramView) coordinates.getView(); if (varView == null) { // Should only happen with NOWHERE - fireLocationEvent(coordinates); + fireLocationEvent(coordinates, cause); return AsyncUtils.NIL; } return materialize(coordinates).thenAcceptAsync(snap -> { @@ -751,12 +799,12 @@ public class DebuggerTraceManagerServicePlugin extends Plugin return; // We navigated elsewhere before emulation completed } varView.setSnap(snap); - fireLocationEvent(coordinates); + fireLocationEvent(coordinates, cause); }, SwingExecutorService.MAYBE_NOW); } - protected void fireLocationEvent(DebuggerCoordinates coordinates) { - firePluginEvent(new TraceActivatedPluginEvent(getName(), coordinates)); + protected void fireLocationEvent(DebuggerCoordinates coordinates, ActivationCause cause) { + firePluginEvent(new TraceActivatedPluginEvent(getName(), coordinates, cause)); } @Override @@ -980,7 +1028,7 @@ public class DebuggerTraceManagerServicePlugin extends Plugin //Msg.debug(this, "Remaining Consumers of " + trace + ": " + trace.getConsumerList()); } if (current.getTrace() == trace) { - activate(DebuggerCoordinates.NOWHERE); + activate(DebuggerCoordinates.NOWHERE, ActivationCause.ACTIVATE_DEFAULT); } else { contextChanged(); @@ -1004,7 +1052,7 @@ public class DebuggerTraceManagerServicePlugin extends Plugin @Override protected void dispose() { super.dispose(); - activate(DebuggerCoordinates.NOWHERE); + activate(DebuggerCoordinates.NOWHERE, ActivationCause.ACTIVATE_DEFAULT); synchronized (listenersByTrace) { Iterator it = listenersByTrace.keySet().iterator(); while (it.hasNext()) { @@ -1027,12 +1075,12 @@ public class DebuggerTraceManagerServicePlugin extends Plugin return elem.toString(); } - protected void activateNoFocus(DebuggerCoordinates coordinates) { + protected void activateNoFocus(DebuggerCoordinates coordinates, ActivationCause cause) { DebuggerCoordinates resolved = doSetCurrent(coordinates); if (resolved == null) { return; } - prepareViewAndFireEvent(resolved); + prepareViewAndFireEvent(resolved, cause); } protected static boolean isSameFocus(DebuggerCoordinates prev, DebuggerCoordinates resolved) { @@ -1082,7 +1130,7 @@ public class DebuggerTraceManagerServicePlugin extends Plugin @Override public CompletableFuture activateAndNotify(DebuggerCoordinates coordinates, - boolean syncTargetFocus) { + ActivationCause cause, boolean syncTargetFocus) { DebuggerCoordinates prev; DebuggerCoordinates resolved; @@ -1098,7 +1146,7 @@ public class DebuggerTraceManagerServicePlugin extends Plugin if (resolved == null) { return AsyncUtils.NIL; } - CompletableFuture future = prepareViewAndFireEvent(resolved); + CompletableFuture future = prepareViewAndFireEvent(resolved, cause); if (!syncTargetFocus) { return future; } @@ -1118,12 +1166,8 @@ public class DebuggerTraceManagerServicePlugin extends Plugin } @Override - public void activate(DebuggerCoordinates coordinates) { - activateAndNotify(coordinates, true); // Drop future on floor - } - - public void activateNoFocusChange(DebuggerCoordinates coordinates) { - activateAndNotify(coordinates, false); // Drop future on floor + public void activate(DebuggerCoordinates coordinates, ActivationCause cause) { + activateAndNotify(coordinates, cause, true); // Drop future on floor } @Override @@ -1169,38 +1213,6 @@ public class DebuggerTraceManagerServicePlugin extends Plugin return current.object(object); } - @Override - public void setAutoActivatePresent(boolean enabled) { - autoActivatePresent.set(enabled, null); - TraceRecorder curRecorder = current.getRecorder(); - if (enabled) { - // TODO: Re-sync focus. This wasn't working. Not sure it's appropriate anyway. - /*if (synchronizeFocus && curRef != null) { - if (doModelObjectFocused(curRef, false)) { - return; - } - }*/ - if (curRecorder != null) { - activateNoFocus(current.snap(curRecorder.getSnap())); - } - } - } - - @Override - public boolean isAutoActivatePresent() { - return autoActivatePresent.get(); - } - - @Override - public void addAutoActivatePresentChangeListener(BooleanChangeAdapter listener) { - autoActivatePresent.addChangeListener(listener); - } - - @Override - public void removeAutoActivatePresentChangeListener(BooleanChangeAdapter listener) { - autoActivatePresent.removeChangeListener(listener); - } - @Override public void setSynchronizeFocus(boolean enabled) { synchronizeFocus.set(enabled, null); @@ -1315,17 +1327,20 @@ public class DebuggerTraceManagerServicePlugin extends Plugin @Override public void readDataState(SaveState saveState) { - int traceCount = saveState.getInt(KEY_TRACE_COUNT, 0); - for (int index = 0; index < traceCount; index++) { - String stateName = PREFIX_OPEN_TRACE + index; - // Trace will be opened by readDataState, resolve causes update to focus and view - DebuggerCoordinates coords = - DebuggerCoordinates.readDataState(tool, saveState, stateName); - if (coords.getTrace() != null) { - lastCoordsByTrace.put(coords.getTrace(), coords); + synchronized (listenersByTrace) { + int traceCount = saveState.getInt(KEY_TRACE_COUNT, 0); + for (int index = 0; index < traceCount; index++) { + String stateName = PREFIX_OPEN_TRACE + index; + // Trace will be opened by readDataState, resolve causes update to focus and view + DebuggerCoordinates coords = + DebuggerCoordinates.readDataState(tool, saveState, stateName); + if (coords.getTrace() != null) { + lastCoordsByTrace.put(coords.getTrace(), coords); + } } } - activate(DebuggerCoordinates.readDataState(tool, saveState, KEY_CURRENT_COORDS)); + activate(DebuggerCoordinates.readDataState(tool, saveState, KEY_CURRENT_COORDS), + ActivationCause.RESTORE_STATE); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/utils/DebouncedRowWrappedEnumeratedColumnTableModel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/utils/DebouncedRowWrappedEnumeratedColumnTableModel.java index b986a821db..8c5f32f6ed 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/utils/DebouncedRowWrappedEnumeratedColumnTableModel.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/utils/DebouncedRowWrappedEnumeratedColumnTableModel.java @@ -30,9 +30,9 @@ public class DebouncedRowWrappedEnumeratedColumnTableModel & E AsyncDebouncer debouncer = new AsyncDebouncer(AsyncTimer.DEFAULT_TIMER, 100); public DebouncedRowWrappedEnumeratedColumnTableModel(PluginTool tool, String name, - Class colType, - Function keyFunc, Function wrapper) { - super(tool, name, colType, keyFunc, wrapper); + Class colType, Function keyFunc, Function wrapper, + Function getter) { + super(tool, name, colType, keyFunc, wrapper, getter); debouncer.addListener(this::settled); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerLogicalBreakpointService.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerLogicalBreakpointService.java index 6e9d14c5c1..c82255606a 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerLogicalBreakpointService.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerLogicalBreakpointService.java @@ -33,7 +33,7 @@ import ghidra.trace.model.program.TraceProgramView; @ServiceInfo( // defaultProvider = DebuggerLogicalBreakpointServicePlugin.class, - description = "Aggregate breakpoints for programs and live traces") + description = "Aggregate breakpoints for programs and traces") public interface DebuggerLogicalBreakpointService { /** * Get all logical breakpoints known to the tool. @@ -54,11 +54,11 @@ public interface DebuggerLogicalBreakpointService { NavigableMap> getBreakpoints(Program program); /** - * Get a map of addresses to collected logical breakpoints (at present) for a given trace. + * Get a map of addresses to collected logical breakpoints for a given trace. * *

    - * The trace must be associated with a live target. The returned map collects live breakpoints - * in the recorded target, using trace breakpoints from the recorder's current snapshot. + * The map only includes breakpoints visible in the trace's primary view. Visibility depends on + * the view's snapshot. * * @param trace the trace database * @return the map of logical breakpoints @@ -78,12 +78,11 @@ public interface DebuggerLogicalBreakpointService { Set getBreakpointsAt(Program program, Address address); /** - * Get the collected logical breakpoints (at present) at the given trace location. + * Get the collected logical breakpoints at the given trace location. * *

    - * The trace must be associated with a live target. The returned collection includes live - * breakpoints in the recorded target, using trace breakpoints from the recorders' current - * snapshot. + * The set only includes breakpoints visible in the trace's primary view. Visibility depends on + * the view's snapshot. * * @param trace the trace database * @param address the address @@ -95,8 +94,8 @@ public interface DebuggerLogicalBreakpointService { * Get the logical breakpoint of which the given trace breakpoint is a part * *

    - * If the given trace breakpoint is not part of any logical breakpoint, e.g., because it is not - * on a live target, then null is returned. + * If the given trace breakpoint is not part of any logical breakpoint, e.g., because the trace + * is not opened in the tool or events are still being processed, then null is returned. * * @param bpt the trace breakpoint * @return the logical breakpoint, or null @@ -108,8 +107,8 @@ public interface DebuggerLogicalBreakpointService { * *

    * The {@code program} field for the location may be either a program database (static image) or - * a view for a trace associated with a live target. If it is the latter, the view's current - * snapshot is ignored, in favor of the associated recorder's current snapshot. + * a view for a trace. If it is the latter, the view's snapshot is ignored in favor of the + * trace's primary view's snapshot. * *

    * If {@code program} is a static image, this is equivalent to using @@ -252,8 +251,7 @@ public interface DebuggerLogicalBreakpointService { } /** - * Create an enabled breakpoint at the given program location and each mapped live trace - * location. + * Create an enabled breakpoint at the given program location and each mapped trace location. * *

    * The implementation should take care not to create the same breakpoint multiple times. The @@ -278,12 +276,12 @@ public interface DebuggerLogicalBreakpointService { * this is the case, the breakpoint will have no name. * *

    - * Note, the debugger ultimately determines the placement behavior. If it is managing multiple - * targets, it is possible the breakpoint will be effective in another trace. This fact should - * be reflected correctly in the resulting logical markings once all resulting events have been - * processed. + * Note for live targets, the debugger ultimately determines the placement behavior. If it is + * managing multiple targets, it is possible the breakpoint will be effective in another trace. + * This fact should be reflected correctly in the resulting logical markings once all resulting + * events have been processed. * - * @param trace the given trace, which must be live + * @param trace the given trace * @param address the address in the trace (as viewed in the present) * @param length size of the breakpoint, may be ignored by debugger * @param kinds the kinds of breakpoint @@ -367,7 +365,7 @@ public interface DebuggerLogicalBreakpointService { CompletableFuture deleteAll(Collection col, Trace trace); /** - * Presuming the given locations are live, enable them + * Enable the given locations * * @param col the trace breakpoints * @return a future which completes when the command has been processed @@ -375,7 +373,7 @@ public interface DebuggerLogicalBreakpointService { CompletableFuture enableLocs(Collection col); /** - * Presuming the given locations are live, disable them + * Disable the given locations * * @param col the trace breakpoints * @return a future which completes when the command has been processed @@ -383,7 +381,7 @@ public interface DebuggerLogicalBreakpointService { CompletableFuture disableLocs(Collection col); /** - * Presuming the given locations are live, delete them + * Delete the given locations * * @param col the trace breakpoints * @return a future which completes when the command has been processed diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerStateEditingService.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerStateEditingService.java index aec0106e2f..345be70c51 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerStateEditingService.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerStateEditingService.java @@ -17,16 +17,12 @@ package ghidra.app.services; import java.util.concurrent.CompletableFuture; -import javax.swing.Icon; - import ghidra.app.plugin.core.debug.DebuggerCoordinates; -import ghidra.app.plugin.core.debug.gui.DebuggerResources; import ghidra.app.plugin.core.debug.service.editing.DebuggerStateEditingServicePlugin; import ghidra.framework.plugintool.ServiceInfo; import ghidra.pcode.utils.Utils; import ghidra.program.model.address.Address; -import ghidra.program.model.lang.Register; -import ghidra.program.model.lang.RegisterValue; +import ghidra.program.model.lang.*; import ghidra.program.model.mem.LiveMemoryHandler; import ghidra.trace.model.Trace; import ghidra.trace.model.program.TraceProgramView; @@ -35,47 +31,6 @@ import ghidra.trace.model.program.TraceProgramView; defaultProvider = DebuggerStateEditingServicePlugin.class, description = "Centralized service for modifying machine states") public interface DebuggerStateEditingService { - enum StateEditingMode { - READ_ONLY(DebuggerResources.NAME_EDIT_MODE_READ_ONLY, // - DebuggerResources.ICON_EDIT_MODE_READ_ONLY) { - @Override - public boolean canEdit(DebuggerCoordinates coordinates) { - return false; - } - }, - WRITE_TARGET(DebuggerResources.NAME_EDIT_MODE_WRITE_TARGET, // - DebuggerResources.ICON_EDIT_MODE_WRITE_TARGET) { - @Override - public boolean canEdit(DebuggerCoordinates coordinates) { - return coordinates.isAliveAndPresent(); - } - }, - WRITE_TRACE(DebuggerResources.NAME_EDIT_MODE_WRITE_TRACE, // - DebuggerResources.ICON_EDIT_MODE_WRITE_TRACE) { - @Override - public boolean canEdit(DebuggerCoordinates coordinates) { - return coordinates.getTrace() != null; - } - }, - WRITE_EMULATOR(DebuggerResources.NAME_EDIT_MODE_WRITE_EMULATOR, // - DebuggerResources.ICON_EDIT_MODE_WRITE_EMULATOR) { - @Override - public boolean canEdit(DebuggerCoordinates coordinates) { - return coordinates.getTrace() != null; - } - }; - - public final String name; - public final Icon icon; - - private StateEditingMode(String name, Icon icon) { - this.name = name; - this.icon = icon; - } - - public abstract boolean canEdit(DebuggerCoordinates coordinates); - } - interface StateEditor { DebuggerStateEditingService getService(); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerTraceManagerService.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerTraceManagerService.java index 23b9593227..101fa0b922 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerTraceManagerService.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerTraceManagerService.java @@ -38,6 +38,49 @@ import ghidra.util.TriConsumer; @ServiceInfo(defaultProvider = DebuggerTraceManagerServicePlugin.class) public interface DebuggerTraceManagerService { + /** + * The reason coordinates were activated + */ + public enum ActivationCause { + /** + * The change was driven by the user + * + *

    + * TODO: Distinguish between API and GUI? + */ + USER, + /** + * A trace was activated because a recording was started, usually when a target is launched + */ + START_RECORDING, + /** + * The change was driven by the model focus, possibly indirectly by the user + */ + SYNC_MODEL, + /** + * The change was driven by the recorder advancing a snapshot + */ + FOLLOW_PRESENT, + /** + * The change was caused by a change to the mapper selection, probably indirectly by the + * user + */ + MAPPER_CHANGED, + /** + * Some default coordinates were activated + * + *

    + * Please don't misunderstand this as the "default cause." Rather, e.g., when the current + * trace is closed, and the manager needs to activate new coordinates, it is activating + * "default coordinates." + */ + ACTIVATE_DEFAULT, + /** + * The tool is restoring its data state + */ + RESTORE_STATE, + } + /** * An adapter that works nicely with an {@link AsyncReference} * @@ -69,7 +112,7 @@ public interface DebuggerTraceManagerService { * Get the current coordinates * *

    - * This entails everything except the current address + * This entails everything except the current address. * * @return the current coordinates */ @@ -233,11 +276,22 @@ public interface DebuggerTraceManagerService { * thread for the desired trace. * * @param coordinates the desired coordinates + * @param cause the cause of the activation * @param syncTargetFocus true synchronize the current target to the same coordinates * @return a future which completes when emulation and navigation is complete */ CompletableFuture activateAndNotify(DebuggerCoordinates coordinates, - boolean syncTargetFocus); + ActivationCause cause, boolean syncTargetFocus); + + /** + * Activate the given coordinates, caused by the user + * + * @see #activate(DebuggerCoordinates, ActivationCause) + * @param coordinates the desired coordinates + */ + default void activate(DebuggerCoordinates coordinates) { + activate(coordinates, ActivationCause.USER); + } /** * Activate the given coordinates, synchronizing the current target, if possible @@ -247,8 +301,9 @@ public interface DebuggerTraceManagerService { * {@link #activateAndNotify(DebuggerCoordinates, boolean)}. * * @param coordinates the desired coordinates + * @param cause the cause of activation */ - void activate(DebuggerCoordinates coordinates); + void activate(DebuggerCoordinates coordinates, ActivationCause cause); /** * Resolve coordinates for the given trace using the manager's "best judgment" @@ -388,38 +443,6 @@ public interface DebuggerTraceManagerService { activate(resolveObject(object)); } - /** - * Control whether the trace manager automatically activates the "present snapshot" - * - *

    - * Auto activation only applies when the current trace advances. It never changes to another - * trace. - * - * @param enabled true to enable auto activation - */ - void setAutoActivatePresent(boolean enabled); - - /** - * Check if the trace manager automatically activate the "present snapshot" - * - * @return true if auto activation is enabled - */ - boolean isAutoActivatePresent(); - - /** - * Add a listener for changes to auto activation enablement - * - * @param listener the listener to receive change notifications - */ - void addAutoActivatePresentChangeListener(BooleanChangeAdapter listener); - - /** - * Remove a listener for changes to auto activation enablement - * - * @param listener the listener receiving change notifications - */ - void removeAutoActivatePresentChangeListener(BooleanChangeAdapter listener); - /** * Control whether trace activation is synchronized with debugger focus/select * diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/LogicalBreakpoint.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/LogicalBreakpoint.java index b770d1c814..c5b684825b 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/LogicalBreakpoint.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/LogicalBreakpoint.java @@ -30,6 +30,30 @@ import ghidra.trace.model.Trace; import ghidra.trace.model.breakpoint.TraceBreakpoint; import ghidra.trace.model.breakpoint.TraceBreakpointKind; +/** + * A logical breakpoint + * + *

    + * This is a collection of at most one program breakpoint, which is actually a bookmark with a + * special type, and any number of trace breakpoints. The program breakpoint represents the logical + * breakpoint, as this is the most stable anchor for keeping the user's breakpoint set. All + * breakpoints in the set correspond to the same address when considering the module map (or other + * source of static-to-dynamic mapping), which may involve relocation. They also share the same + * kinds and length, since these are more or less intrinsic to the breakpoints specification. Thus, + * more than one logical breakpoint may occupy the same address. A logical breakpoints having a + * program bookmark (or that at least has a static address) is called a "mapped" breakpoint. This is + * the ideal, ordinary case. A breakpoint that cannot be mapped to a static address (and thus cannot + * have a program bookmark) is called a "lone" breakpoint. + * + *

    + * WARNING: The lifecycle of a logical breakpoint is fairly volatile. It is generally not + * safe to "hold onto" a logical breakpoint, since with any event, the logical breakpoint service + * may discard and re-create the object, even if it's composed of the same program and trace + * breakpoints. If it is truly necessary to hold onto logical breakpoints, consider using + * {@link DebuggerLogicalBreakpointService#addChangeListener(LogicalBreakpointsChangeListener)}. A + * logical breakpoint is valid until the service invokes + * {@link LogicalBreakpointsChangeListener#breakpointRemoved(LogicalBreakpoint)}. + */ public interface LogicalBreakpoint { String BREAKPOINT_ENABLED_BOOKMARK_TYPE = "BreakpointEnabled"; String BREAKPOINT_DISABLED_BOOKMARK_TYPE = "BreakpointDisabled"; @@ -605,6 +629,22 @@ public interface LogicalBreakpoint { */ void setName(String name); + /** + * Get the sleigh injection when emulating this breakpoint + * + * @return the sleigh injection + * @see TraceBreakpoint#getEmuSleigh() + */ + String getEmuSleigh(); + + /** + * Set the sleigh injection when emulating this breakpoint + * + * @param sleigh the sleigh injection + * @see TraceBreakpoint#setEmuSleigh(String) + */ + void setEmuSleigh(String sleigh); + /** * If the logical breakpoint has a mapped program location, get that location. * diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/StateEditingMode.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/StateEditingMode.java new file mode 100644 index 0000000000..29254837a3 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/StateEditingMode.java @@ -0,0 +1,431 @@ +/* ### + * 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.nio.ByteBuffer; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + +import javax.swing.Icon; + +import generic.theme.GIcon; +import ghidra.app.plugin.core.debug.DebuggerCoordinates; +import ghidra.app.services.DebuggerTraceManagerService.ActivationCause; +import ghidra.async.AsyncUtils; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressRange; +import ghidra.program.model.lang.Language; +import ghidra.program.model.lang.Register; +import ghidra.program.model.mem.MemoryAccessException; +import ghidra.trace.model.Trace; +import ghidra.trace.model.guest.TracePlatform; +import ghidra.trace.model.memory.TraceMemoryOperations; +import ghidra.trace.model.memory.TraceMemorySpace; +import ghidra.trace.model.program.TraceVariableSnapProgramView; +import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.model.time.schedule.PatchStep; +import ghidra.trace.model.time.schedule.TraceSchedule; +import ghidra.trace.util.TraceRegisterUtils; +import ghidra.util.database.UndoableTransaction; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +/** + * The control / state editing modes + */ +public enum StateEditingMode { + /** + * Control actions, breakpoint commands are directed to the target, but state edits are + * rejected. + */ + RO_TARGET("Control Target w/ Edits Disabled", new GIcon("icon.debugger.edit.mode.ro.target")) { + @Override + public boolean followsPresent() { + return true; + } + + @Override + public boolean canEdit(DebuggerCoordinates coordinates) { + return false; + } + + @Override + public boolean isVariableEditable(DebuggerCoordinates coordinates, Address address, + int length) { + return false; + } + + @Override + public CompletableFuture setVariable(PluginTool tool, + DebuggerCoordinates coordinates, Address address, byte[] data) { + return CompletableFuture.failedFuture(new MemoryAccessException("Read-only mode")); + } + + @Override + public boolean useEmulatedBreakpoints() { + return false; + } + + @Override + public boolean isSelectable(DebuggerCoordinates coordinates) { + return coordinates.isAliveAndPresent(); + } + + @Override + public StateEditingMode getAlternative(DebuggerCoordinates coordinates) { + return RO_TRACE; + } + }, + /** + * Control actions, breakpoint commands, and state edits are all directed to the target. + */ + RW_TARGET("Control Target", new GIcon("icon.debugger.edit.mode.rw.target")) { + @Override + public boolean followsPresent() { + return true; + } + + @Override + public boolean canEdit(DebuggerCoordinates coordinates) { + return coordinates.isAliveAndPresent(); + } + + @Override + public boolean isVariableEditable(DebuggerCoordinates coordinates, Address address, + int length) { + if (!coordinates.isAliveAndPresent()) { + return false; + } + TraceRecorder recorder = coordinates.getRecorder(); + return recorder.isVariableOnTarget(coordinates.getPlatform(), + coordinates.getThread(), coordinates.getFrame(), address, length); + } + + @Override + public CompletableFuture setVariable(PluginTool tool, + DebuggerCoordinates coordinates, Address address, byte[] data) { + TraceRecorder recorder = coordinates.getRecorder(); + if (recorder == null) { + return CompletableFuture + .failedFuture(new MemoryAccessException("Trace has no live target")); + } + if (!coordinates.isPresent()) { + return CompletableFuture + .failedFuture(new MemoryAccessException("View is not the present")); + } + return recorder.writeVariable(coordinates.getPlatform(), coordinates.getThread(), + coordinates.getFrame(), address, data); + } + + @Override + public boolean useEmulatedBreakpoints() { + return false; + } + + @Override + public boolean isSelectable(DebuggerCoordinates coordinates) { + return coordinates.isAliveAndPresent(); + } + + @Override + public StateEditingMode getAlternative(DebuggerCoordinates coordinates) { + return RW_EMULATOR; + } + }, + /** + * Control actions activate trace snapshots, breakpoint commands are directed to the emulator, + * and state edits are rejected. + */ + RO_TRACE("Control Trace w/ Edits Disabled", new GIcon("icon.debugger.edit.mode.ro.trace")) { + @Override + public boolean followsPresent() { + return false; + } + + @Override + public boolean canEdit(DebuggerCoordinates coordinates) { + return false; + } + + @Override + public boolean isVariableEditable(DebuggerCoordinates coordinates, Address address, + int length) { + return false; + } + + @Override + public CompletableFuture setVariable(PluginTool tool, + DebuggerCoordinates coordinates, Address address, byte[] data) { + return CompletableFuture.failedFuture(new MemoryAccessException("Read-only mode")); + } + + @Override + public boolean useEmulatedBreakpoints() { + return true; + } + }, + /** + * Control actions activate trace snapshots, breakpoint commands are directed to the emulator, + * and state edits modify the current trace snapshot. + */ + RW_TRACE("Control Trace", new GIcon("icon.debugger.edit.mode.rw.trace")) { + @Override + public boolean followsPresent() { + return false; + } + + @Override + public boolean canEdit(DebuggerCoordinates coordinates) { + return coordinates.getTrace() != null; + } + + @Override + public boolean isVariableEditable(DebuggerCoordinates coordinates, Address address, + int length) { + return address.isMemoryAddress() || coordinates.getThread() != null; + } + + @Override + public CompletableFuture setVariable(PluginTool tool, + DebuggerCoordinates coordinates, Address guestAddress, byte[] data) { + Trace trace = coordinates.getTrace(); + TracePlatform platform = coordinates.getPlatform(); + long snap = coordinates.getViewSnap(); + Address hostAddress = platform.mapGuestToHost(guestAddress); + if (hostAddress == null) { + throw new IllegalArgumentException( + "Guest address " + guestAddress + " is not mapped"); + } + TraceMemoryOperations memOrRegs; + Address overlayAddress; + try (UndoableTransaction txid = + UndoableTransaction.start(trace, "Edit Variable")) { + if (hostAddress.isRegisterAddress()) { + TraceThread thread = coordinates.getThread(); + if (thread == null) { + throw new IllegalArgumentException("Register edits require a thread."); + } + TraceMemorySpace regs = trace.getMemoryManager() + .getMemoryRegisterSpace(thread, coordinates.getFrame(), + true); + memOrRegs = regs; + overlayAddress = regs.getAddressSpace().getOverlayAddress(hostAddress); + } + else { + memOrRegs = trace.getMemoryManager(); + overlayAddress = hostAddress; + } + if (memOrRegs.putBytes(snap, overlayAddress, + ByteBuffer.wrap(data)) != data.length) { + return CompletableFuture.failedFuture(new MemoryAccessException()); + } + } + return AsyncUtils.NIL; + } + + @Override + public boolean useEmulatedBreakpoints() { + return true; + } + }, + /** + * Control actions, breakpoint commands, and state edits are directed to the emulator. + * + *

    + * Edits are accomplished by appending patch steps to the current schedule and activating that + * schedule. + */ + RW_EMULATOR("Control Emulator", new GIcon("icon.debugger.edit.mode.rw.emulator")) { + @Override + public boolean followsPresent() { + return false; + } + + @Override + public boolean canEdit(DebuggerCoordinates coordinates) { + return coordinates.getTrace() != null; + } + + @Override + public boolean isVariableEditable(DebuggerCoordinates coordinates, Address address, + int length) { + if (coordinates.getThread() == null) { + // A limitation in TraceSchedule, which is used to manifest patches + return false; + } + if (!RW_TRACE.isVariableEditable(coordinates, address, length)) { + return false; + } + // TODO: Limitation from using Sleigh for patching + Register ctxReg = coordinates.getTrace().getBaseLanguage().getContextBaseRegister(); + if (ctxReg == Register.NO_CONTEXT) { + return true; + } + AddressRange ctxRange = TraceRegisterUtils.rangeForRegister(ctxReg); + if (ctxRange.contains(address)) { + return false; + } + return true; + } + + @Override + public CompletableFuture setVariable(PluginTool tool, + DebuggerCoordinates coordinates, Address address, byte[] data) { + if (!(coordinates.getView() instanceof TraceVariableSnapProgramView)) { + throw new IllegalArgumentException("Cannot emulate using a Fixed Program View"); + } + TraceThread thread = coordinates.getThread(); + if (thread == null) { + // TODO: Well, technically, only for register edits + // It's a limitation in TraceSchedule. Every step requires a thread + throw new IllegalArgumentException("Emulator edits require a thread."); + } + Language language = coordinates.getPlatform().getLanguage(); + TraceSchedule time = coordinates.getTime() + .patched(thread, language, + PatchStep.generateSleigh(language, address, data)); + + DebuggerCoordinates withTime = coordinates.time(time); + DebuggerTraceManagerService traceManager = + Objects.requireNonNull(tool.getService(DebuggerTraceManagerService.class), + "No trace manager service"); + Long found = traceManager.findSnapshot(withTime); + // Materialize it on the same thread (even if swing) + // It shouldn't take long, since we're only appending one step. + if (found == null) { + // TODO: Could still do it async on another thread, no? + // Not sure it buys anything, since program view will call .get on swing thread + DebuggerEmulationService emulationService = Objects.requireNonNull( + tool.getService(DebuggerEmulationService.class), "No emulation service"); + try { + emulationService.emulate(coordinates.getPlatform(), time, + TaskMonitor.DUMMY); + } + catch (CancelledException e) { + throw new AssertionError(e); + } + } + return traceManager.activateAndNotify(withTime, ActivationCause.USER, false); + } + + @Override + public boolean useEmulatedBreakpoints() { + return true; + } + }; + + public static final StateEditingMode DEFAULT = RO_TARGET; + + public final String name; + public final Icon icon; + + private StateEditingMode(String name, Icon icon) { + this.name = name; + this.icon = icon; + } + + /** + * Check if the UI should keep its active snapshot in sync with the recorder's latest. + * + * @return true to follow, false if not + */ + public abstract boolean followsPresent(); + + /** + * Check if (broadly speaking) the mode supports editing the given coordinates + * + * @param coordinates the coordinates to check + * @return true if editable, false if not + */ + public abstract boolean canEdit(DebuggerCoordinates coordinates); + + /** + * Check if the given variable can be edited under this mode + * + * @param coordinates the coordinates to check + * @param address the address of the variable + * @param length the length of the variable, in bytes + * @return true if editable, false if not + */ + public abstract boolean isVariableEditable(DebuggerCoordinates coordinates, Address address, + int length); + + /** + * Set the value of a variable + * + *

    + * Because the edit may be directed to a live target, the return value is a + * {@link CompletableFuture}. Additionally, when directed to the emulator, this allows the + * emulated state to be computed in the background. + * + * @param tool the tool requesting the edit + * @param coordinates the coordinates of the edit + * @param address the address of the variable + * @param data the desired value of the variable + * @return a future which completes when the edit is finished + */ + public abstract CompletableFuture setVariable(PluginTool tool, + DebuggerCoordinates coordinates, Address address, byte[] data); + + /** + * Check if this mode operates on target breakpoints or emulator breakpoints + * + * @return false for target, true for emulator + */ + public abstract boolean useEmulatedBreakpoints(); + + /** + * Check if this mode can be selected for the given coordinates + * + * @param coordinates the current coordinates + * @return true to enable selection, false to disable + */ + public boolean isSelectable(DebuggerCoordinates coordinates) { + return true; + } + + /** + * If the mode can no longer be selected for new coordinates, get the new mode + * + *

    + * For example, if a target terminates while the mode is {@link #RO_TARGET}, this specifies the + * new mode. + * + * @param coordinates the new coordinates + * @return the new mode + */ + public StateEditingMode getAlternative(DebuggerCoordinates coordinates) { + throw new AssertionError("INTERNAL: Non-selectable mode must provide alternative"); + } + + /** + * Find the new mode (or same) mode when activating the given coordinates + * + *

    + * The default is implemented using {@link #isSelectable(DebuggerCoordinates)} followed by + * {@link #getAlternative(DebuggerCoordinates)}. + * + * @param coordinates the new coordinates + * @return the mode + */ + public StateEditingMode modeOnChange(DebuggerCoordinates coordinates) { + if (isSelectable(coordinates)) { + return this; + } + return getAlternative(coordinates); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/debug/flatapi/FlatDebuggerAPI.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/debug/flatapi/FlatDebuggerAPI.java index 2a3ba7bb04..28501649fa 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/debug/flatapi/FlatDebuggerAPI.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/debug/flatapi/FlatDebuggerAPI.java @@ -30,7 +30,6 @@ import ghidra.app.plugin.core.debug.service.model.launch.DebuggerProgramLaunchOf import ghidra.app.script.GhidraScript; import ghidra.app.script.GhidraState; import ghidra.app.services.*; -import ghidra.app.services.DebuggerStateEditingService.StateEditingMode; import ghidra.app.services.DebuggerStateEditingService.StateEditor; import ghidra.dbg.AnnotatedDebuggerAttributeListener; import ghidra.dbg.DebuggerObjectModel; @@ -766,9 +765,9 @@ public interface FlatDebuggerAPI { } /** - * Apply the given SLEIGH patch to the emulator + * Apply the given Sleigh patch to the emulator * - * @param sleigh the SLEIGH source, without terminating semicolon + * @param sleigh the Sleigh source, without terminating semicolon * @param monitor a monitor for the emulation * @return true if successful, false otherwise * @throws CancelledException if the user cancelled via the given monitor diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/pcode/exec/DebuggerPcodeUtils.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/pcode/exec/DebuggerPcodeUtils.java index 55983a510d..b8948a7873 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/pcode/exec/DebuggerPcodeUtils.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/pcode/exec/DebuggerPcodeUtils.java @@ -16,12 +16,9 @@ package ghidra.pcode.exec; import java.math.BigInteger; -import java.util.HashMap; -import java.util.Map; +import java.util.*; import java.util.Map.Entry; -import org.bouncycastle.util.Arrays; - import ghidra.app.plugin.core.debug.DebuggerCoordinates; import ghidra.app.plugin.core.debug.service.emulation.*; import ghidra.app.plugin.core.debug.service.emulation.data.DefaultPcodeDebuggerAccess; @@ -214,7 +211,7 @@ public enum DebuggerPcodeUtils { if (this.bigEndian != that.bigEndian) { return false; } - return Arrays.areEqual(this.bytes, that.bytes); + return Arrays.equals(this.bytes, that.bytes); } /** diff --git a/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointsPluginScreenShots.java b/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointsPluginScreenShots.java index 458ceed800..803116cc5c 100644 --- a/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointsPluginScreenShots.java +++ b/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointsPluginScreenShots.java @@ -137,6 +137,18 @@ public class DebuggerBreakpointsPluginScreenShots extends GhidraScreenShotGenera new DefaultTraceLocation(trace3, null, Lifespan.nowOn(0), addr(trace3, 0x7fac0000)), new ProgramLocation(program, addr(program, 0x00400000)), 0x00010000, false); } + waitForSwing(); + + try (UndoableTransaction tid = UndoableTransaction.start(program, "Add breakpoint")) { + program.getBookmarkManager() + .setBookmark(addr(program, 0x00401234), + LogicalBreakpoint.BREAKPOINT_ENABLED_BOOKMARK_TYPE, "SW_EXECUTE;1", + "before connect"); + program.getBookmarkManager() + .setBookmark(addr(program, 0x00604321), + LogicalBreakpoint.BREAKPOINT_ENABLED_BOOKMARK_TYPE, "WRITE;4", + "write version"); + } TargetBreakpointSpecContainer bc1 = waitFor(() -> Unique.assertAtMostOne(recorder1.collectBreakpointContainers(null)), @@ -156,17 +168,6 @@ public class DebuggerBreakpointsPluginScreenShots extends GhidraScreenShotGenera trace3.getBreakpointManager() .getBreakpointsAt(recorder3.getSnap(), addr(trace3, 0x7fac1234)))); - try (UndoableTransaction tid = UndoableTransaction.start(program, "Add breakpoint")) { - program.getBookmarkManager() - .setBookmark(addr(program, 0x00401234), - LogicalBreakpoint.BREAKPOINT_ENABLED_BOOKMARK_TYPE, "SW_EXECUTE;1", - "before connect"); - program.getBookmarkManager() - .setBookmark(addr(program, 0x00604321), - LogicalBreakpoint.BREAKPOINT_ENABLED_BOOKMARK_TYPE, "WRITE;4", - "write version"); - } - waitForPass(() -> { Set allBreakpoints = breakpointService.getAllBreakpoints(); assertEquals(2, allBreakpoints.size()); diff --git a/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackPluginScreenShots.java b/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackPluginScreenShots.java index 953847f7e8..0ca315d77c 100644 --- a/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackPluginScreenShots.java +++ b/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackPluginScreenShots.java @@ -38,7 +38,6 @@ import ghidra.app.plugin.core.debug.stack.*; import ghidra.app.plugin.core.progmgr.ProgramManagerPlugin; import ghidra.app.services.*; import ghidra.app.services.DebuggerEmulationService.EmulationResult; -import ghidra.app.services.DebuggerStateEditingService.StateEditingMode; import ghidra.app.services.DebuggerStateEditingService.StateEditor; import ghidra.async.AsyncTestUtils; import ghidra.framework.model.DomainFolder; @@ -297,7 +296,7 @@ public class DebuggerStackPluginScreenShots extends GhidraScreenShotGenerator traceManager.activateThread(thread); waitForSwing(); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_TRACE); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_TRACE); StateEditor editor = editingService.createStateEditor(tb.trace); DebuggerCoordinates atSetup = traceManager.getCurrent(); diff --git a/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/stack/vars/VariableValueHoverPluginScreenShots.java b/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/stack/vars/VariableValueHoverPluginScreenShots.java index 109fedbd0a..faffaefba5 100644 --- a/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/stack/vars/VariableValueHoverPluginScreenShots.java +++ b/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/stack/vars/VariableValueHoverPluginScreenShots.java @@ -45,7 +45,6 @@ import ghidra.app.plugin.core.decompile.DecompilerProvider; import ghidra.app.plugin.core.progmgr.ProgramManagerPlugin; import ghidra.app.services.*; import ghidra.app.services.DebuggerEmulationService.EmulationResult; -import ghidra.app.services.DebuggerStateEditingService.StateEditingMode; import ghidra.app.services.DebuggerStateEditingService.StateEditor; import ghidra.app.util.viewer.listingpanel.ListingPanel; import ghidra.async.AsyncTestUtils; @@ -259,7 +258,7 @@ public class VariableValueHoverPluginScreenShots extends GhidraScreenShotGenerat } waitForDomainObject(tb.trace); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_TRACE); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_TRACE); StateEditor editor = editingService.createStateEditor(tb.trace); DebuggerCoordinates atSetup = traceManager.getCurrent(); diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/disassemble/DebuggerDisassemblyTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/disassemble/DebuggerDisassemblyTest.java index 2786eb68b2..9c76cad603 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/disassemble/DebuggerDisassemblyTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/disassemble/DebuggerDisassemblyTest.java @@ -39,7 +39,6 @@ import ghidra.app.plugin.core.debug.service.platform.DebuggerPlatformServicePlug import ghidra.app.plugin.core.debug.service.workflow.DebuggerWorkflowServiceProxyPlugin; import ghidra.app.plugin.core.debug.workflow.DisassembleAtPcDebuggerBot; import ghidra.app.services.*; -import ghidra.app.services.DebuggerStateEditingService.StateEditingMode; import ghidra.dbg.target.TargetEnvironment; import ghidra.dbg.target.schema.SchemaContext; import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName; @@ -411,7 +410,7 @@ public class DebuggerDisassemblyTest extends AbstractGhidraHeadedDebuggerGUITest createLegacyTrace("ARM:LE:32:v8", 0x00400000, () -> tb.buf(0x00, 0x00, 0x00, 0x00)); Address start = tb.addr(0x00400000); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_TRACE); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_TRACE); // Ensure the mapper is added to the trace assertNotNull(platformService.getMapper(tb.trace, null, 0)); @@ -444,7 +443,7 @@ public class DebuggerDisassemblyTest extends AbstractGhidraHeadedDebuggerGUITest // Don't cheat here and choose v8T! createLegacyTrace("ARM:LE:32:v8", 0x00400000, () -> tb.buf(0x00, 0x00)); Address start = tb.addr(0x00400000); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_TRACE); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_TRACE); // Ensure the mapper is added to the trace assertNotNull(platformService.getMapper(tb.trace, null, 0)); @@ -481,7 +480,7 @@ public class DebuggerDisassemblyTest extends AbstractGhidraHeadedDebuggerGUITest TraceObjectThread thread = createPolyglotTrace("armv8le", 0x00400000, () -> tb.buf(0x00, 0x00, 0x00, 0x00)); Address start = tb.addr(0x00400000); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_TRACE); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_TRACE); // Ensure the mapper is added to the trace assertNotNull(platformService.getMapper(tb.trace, thread.getObject(), 0)); @@ -516,7 +515,7 @@ public class DebuggerDisassemblyTest extends AbstractGhidraHeadedDebuggerGUITest TraceObjectThread thread = createPolyglotTrace("armv8le", 0x00400000, () -> tb.buf(0x00, 0x00)); Address start = tb.addr(0x00400000); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_TRACE); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_TRACE); // Ensure the mapper is added to the trace assertNotNull(platformService.getMapper(tb.trace, thread.getObject(), 0)); @@ -573,7 +572,7 @@ public class DebuggerDisassemblyTest extends AbstractGhidraHeadedDebuggerGUITest createLegacyTrace("ARM:LE:32:v8", 0x00400000, () -> tb.buf()); Address start = tb.addr(0x00400000); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_TRACE); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_TRACE); // Ensure the mapper is added to the trace assertNotNull(platformService.getMapper(tb.trace, null, 0)); @@ -593,7 +592,7 @@ public class DebuggerDisassemblyTest extends AbstractGhidraHeadedDebuggerGUITest TraceObjectThread thread = createPolyglotTrace("armv8le", 0x00400000, () -> tb.buf()); Address start = tb.addr(0x00400000); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_TRACE); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_TRACE); // Ensure the mapper is added to the trace assertNotNull(platformService.getMapper(tb.trace, thread.getObject(), 0)); @@ -613,7 +612,7 @@ public class DebuggerDisassemblyTest extends AbstractGhidraHeadedDebuggerGUITest TraceObjectThread thread = createPolyglotTrace("armv8le", 0x00400000, () -> tb.buf()); Address start = tb.addr(0x00400000); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_TRACE); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_TRACE); // Ensure the mapper is added to the trace assertNotNull(platformService.getMapper(tb.trace, thread.getObject(), 0)); diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/AbstractGhidraHeadedDebuggerGUITest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/AbstractGhidraHeadedDebuggerGUITest.java index d4f7a275b6..9b07978308 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/AbstractGhidraHeadedDebuggerGUITest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/AbstractGhidraHeadedDebuggerGUITest.java @@ -269,6 +269,24 @@ public abstract class AbstractGhidraHeadedDebuggerGUITest }, () -> lastError.get().getMessage()); } + public static T waitForPass(Supplier supplier) { + var locals = new Object() { + AssertionError lastError; + T value; + }; + waitForCondition(() -> { + try { + locals.value = supplier.get(); + return true; + } + catch (AssertionError e) { + locals.lastError = e; + return false; + } + }, () -> locals.lastError.getMessage()); + return locals.value; + } + protected static Set getMenuElementsText(MenuElement menu) { Set result = new HashSet<>(); for (MenuElement sub : menu.getSubElements()) { diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointMarkerPluginTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointMarkerPluginTest.java index c74c63eda0..45edec6ed1 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointMarkerPluginTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointMarkerPluginTest.java @@ -455,8 +455,9 @@ public class DebuggerBreakpointMarkerPluginTest extends AbstractGhidraHeadedDebu } waitForDomainObject(program); - performAction(breakpointMarkerPlugin.actionToggleBreakpoint, - staticCtx(addr(program, 0x00400123)), false); + ProgramLocationActionContext ctx = staticCtx(addr(program, 0x00400123)); + assertTrue(breakpointMarkerPlugin.actionToggleBreakpoint.isAddToPopup(ctx)); + performAction(breakpointMarkerPlugin.actionToggleBreakpoint, ctx, false); DebuggerPlaceBreakpointDialog dialog = waitForDialogComponent(DebuggerPlaceBreakpointDialog.class); runSwing(() -> dialog.okCallback()); @@ -482,8 +483,9 @@ public class DebuggerBreakpointMarkerPluginTest extends AbstractGhidraHeadedDebu } waitForDomainObject(program); - performAction(breakpointMarkerPlugin.actionToggleBreakpoint, - staticCtx(addr(program, 0x00400123)), false); + ProgramLocationActionContext ctx = staticCtx(addr(program, 0x00400123)); + assertTrue(breakpointMarkerPlugin.actionToggleBreakpoint.isAddToPopup(ctx)); + performAction(breakpointMarkerPlugin.actionToggleBreakpoint, ctx, false); DebuggerPlaceBreakpointDialog dialog = waitForDialogComponent(DebuggerPlaceBreakpointDialog.class); runSwing(() -> dialog.okCallback()); @@ -502,13 +504,14 @@ public class DebuggerBreakpointMarkerPluginTest extends AbstractGhidraHeadedDebu addMappedBreakpointOpenAndWait(); LogicalBreakpoint lb = Unique.assertOne(breakpointService.getAllBreakpoints()); - performAction(breakpointMarkerPlugin.actionToggleBreakpoint, - staticCtx(addr(program, 0x00400123)), false); + ProgramLocationActionContext ctx = staticCtx(addr(program, 0x00400123)); + assertTrue(breakpointMarkerPlugin.actionToggleBreakpoint.isAddToPopup(ctx)); + performAction(breakpointMarkerPlugin.actionToggleBreakpoint, ctx, false); waitForPass(() -> assertEquals(State.DISABLED, lb.computeState())); - performAction(breakpointMarkerPlugin.actionToggleBreakpoint, - staticCtx(addr(program, 0x00400123)), false); + assertTrue(breakpointMarkerPlugin.actionToggleBreakpoint.isAddToPopup(ctx)); + performAction(breakpointMarkerPlugin.actionToggleBreakpoint, ctx, false); waitForPass(() -> assertEquals(State.ENABLED, lb.computeState())); } @@ -522,13 +525,14 @@ public class DebuggerBreakpointMarkerPluginTest extends AbstractGhidraHeadedDebu // NB. addMappedBreakpointOpenAndWait already makes this assertion. Just a reminder: waitForPass(() -> assertEquals(State.ENABLED, lb.computeStateForTrace(trace))); - performAction(breakpointMarkerPlugin.actionToggleBreakpoint, - dynamicCtx(trace, addr(trace, 0x55550123)), true); + ProgramLocationActionContext ctx = dynamicCtx(trace, addr(trace, 0x55550123)); + assertTrue(breakpointMarkerPlugin.actionToggleBreakpoint.isAddToPopup(ctx)); + performAction(breakpointMarkerPlugin.actionToggleBreakpoint, ctx, true); waitForPass(() -> assertEquals(State.DISABLED, lb.computeStateForTrace(trace))); - performAction(breakpointMarkerPlugin.actionToggleBreakpoint, - dynamicCtx(trace, addr(trace, 0x55550123)), true); + assertTrue(breakpointMarkerPlugin.actionToggleBreakpoint.isAddToPopup(ctx)); + performAction(breakpointMarkerPlugin.actionToggleBreakpoint, ctx, true); waitForPass(() -> assertEquals(State.ENABLED, lb.computeStateForTrace(trace))); @@ -538,8 +542,8 @@ public class DebuggerBreakpointMarkerPluginTest extends AbstractGhidraHeadedDebu lb.disableForProgram(); waitForPass(() -> assertEquals(State.INCONSISTENT_ENABLED, lb.computeStateForTrace(trace))); - performAction(breakpointMarkerPlugin.actionToggleBreakpoint, - dynamicCtx(trace, addr(trace, 0x55550123)), true); + assertTrue(breakpointMarkerPlugin.actionToggleBreakpoint.isAddToPopup(ctx)); + performAction(breakpointMarkerPlugin.actionToggleBreakpoint, ctx, true); // NB. toggling from dynamic view, this toggles trace bpt, not logical/program bpt waitForPass(() -> assertEquals(State.DISABLED, lb.computeStateForTrace(trace))); @@ -548,8 +552,8 @@ public class DebuggerBreakpointMarkerPluginTest extends AbstractGhidraHeadedDebu waitForPass( () -> assertEquals(State.INCONSISTENT_DISABLED, lb.computeStateForTrace(trace))); - performAction(breakpointMarkerPlugin.actionToggleBreakpoint, - dynamicCtx(trace, addr(trace, 0x55550123)), true); + assertTrue(breakpointMarkerPlugin.actionToggleBreakpoint.isAddToPopup(ctx)); + performAction(breakpointMarkerPlugin.actionToggleBreakpoint, ctx, true); waitForPass(() -> assertEquals(State.ENABLED, lb.computeStateForTrace(trace))); } @@ -558,7 +562,9 @@ public class DebuggerBreakpointMarkerPluginTest extends AbstractGhidraHeadedDebu Set expectedKinds) throws Throwable { addMappedBreakpointOpenAndWait(); // Adds an unneeded breakpoint. Aw well. - performAction(action, staticCtx(addr(program, 0x0400321)), false); + ProgramLocationActionContext ctx = staticCtx(addr(program, 0x0400321)); + assertTrue(action.isAddToPopup(ctx)); + performAction(action, ctx, false); DebuggerPlaceBreakpointDialog dialog = waitForDialogComponent(DebuggerPlaceBreakpointDialog.class); runSwing(() -> { @@ -580,7 +586,9 @@ public class DebuggerBreakpointMarkerPluginTest extends AbstractGhidraHeadedDebu TraceRecorder recorder = addMappedBreakpointOpenAndWait(); // Adds an unneeded breakpoint. Aw well. Trace trace = recorder.getTrace(); - performAction(action, dynamicCtx(trace, addr(trace, 0x55550321)), false); + ProgramLocationActionContext ctx = dynamicCtx(trace, addr(trace, 0x55550321)); + assertTrue(action.isAddToPopup(ctx)); + performAction(action, ctx, false); DebuggerPlaceBreakpointDialog dialog = waitForDialogComponent(DebuggerPlaceBreakpointDialog.class); runSwing(() -> dialog.okCallback()); @@ -664,8 +672,9 @@ public class DebuggerBreakpointMarkerPluginTest extends AbstractGhidraHeadedDebu lb.disable(); waitForPass(() -> assertEquals(State.DISABLED, lb.computeState())); - performAction(breakpointMarkerPlugin.actionEnableBreakpoint, - staticCtx(addr(program, 0x00400123)), true); + ProgramLocationActionContext ctx = staticCtx(addr(program, 0x00400123)); + assertTrue(breakpointMarkerPlugin.actionEnableBreakpoint.isAddToPopup(ctx)); + performAction(breakpointMarkerPlugin.actionEnableBreakpoint, ctx, true); waitForPass(() -> assertEquals(State.ENABLED, lb.computeState())); } @@ -678,8 +687,9 @@ public class DebuggerBreakpointMarkerPluginTest extends AbstractGhidraHeadedDebu lb.disable(); waitForPass(() -> assertEquals(State.DISABLED, lb.computeState())); - performAction(breakpointMarkerPlugin.actionEnableBreakpoint, - dynamicCtx(trace, addr(trace, 0x55550123)), true); + ProgramLocationActionContext ctx = dynamicCtx(trace, addr(trace, 0x55550123)); + assertTrue(breakpointMarkerPlugin.actionEnableBreakpoint.isAddToPopup(ctx)); + performAction(breakpointMarkerPlugin.actionEnableBreakpoint, ctx, true); waitForPass(() -> assertEquals(State.ENABLED, lb.computeStateForTrace(trace))); // TODO: Same test but with multiple traces @@ -690,8 +700,9 @@ public class DebuggerBreakpointMarkerPluginTest extends AbstractGhidraHeadedDebu addMappedBreakpointOpenAndWait(); LogicalBreakpoint lb = Unique.assertOne(breakpointService.getAllBreakpoints()); - performAction(breakpointMarkerPlugin.actionDisableBreakpoint, - staticCtx(addr(program, 0x00400123)), true); + ProgramLocationActionContext ctx = staticCtx(addr(program, 0x00400123)); + assertTrue(breakpointMarkerPlugin.actionDisableBreakpoint.isAddToPopup(ctx)); + performAction(breakpointMarkerPlugin.actionDisableBreakpoint, ctx, true); waitForPass(() -> assertEquals(State.DISABLED, lb.computeState())); } @@ -702,8 +713,9 @@ public class DebuggerBreakpointMarkerPluginTest extends AbstractGhidraHeadedDebu Trace trace = recorder.getTrace(); LogicalBreakpoint lb = Unique.assertOne(breakpointService.getAllBreakpoints()); - performAction(breakpointMarkerPlugin.actionDisableBreakpoint, - dynamicCtx(trace, addr(trace, 0x55550123)), true); + ProgramLocationActionContext ctx = dynamicCtx(trace, addr(trace, 0x55550123)); + assertTrue(breakpointMarkerPlugin.actionDisableBreakpoint.isAddToPopup(ctx)); + performAction(breakpointMarkerPlugin.actionDisableBreakpoint, ctx, true); waitForPass(() -> assertEquals(State.DISABLED, lb.computeStateForTrace(trace))); // TODO: Same test but with multiple traces @@ -713,8 +725,9 @@ public class DebuggerBreakpointMarkerPluginTest extends AbstractGhidraHeadedDebu public void testActionClearBreakpointProgram() throws Throwable { addMappedBreakpointOpenAndWait(); - performAction(breakpointMarkerPlugin.actionClearBreakpoint, - staticCtx(addr(program, 0x00400123)), true); + ProgramLocationActionContext ctx = staticCtx(addr(program, 0x00400123)); + assertTrue(breakpointMarkerPlugin.actionClearBreakpoint.isAddToPopup(ctx)); + performAction(breakpointMarkerPlugin.actionClearBreakpoint, ctx, true); waitForPass(() -> assertTrue(breakpointService.getAllBreakpoints().isEmpty())); } @@ -725,8 +738,9 @@ public class DebuggerBreakpointMarkerPluginTest extends AbstractGhidraHeadedDebu Trace trace = recorder.getTrace(); LogicalBreakpoint lb = Unique.assertOne(breakpointService.getAllBreakpoints()); - performAction(breakpointMarkerPlugin.actionClearBreakpoint, - dynamicCtx(trace, addr(trace, 0x55550123)), true); + ProgramLocationActionContext ctx = dynamicCtx(trace, addr(trace, 0x55550123)); + assertTrue(breakpointMarkerPlugin.actionClearBreakpoint.isAddToPopup(ctx)); + performAction(breakpointMarkerPlugin.actionClearBreakpoint, ctx, true); waitForPass(() -> assertEquals(State.NONE, lb.computeStateForTrace(trace))); // TODO: Same test but with multiple traces diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointsProviderObjectTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointsProviderObjectTest.java index 6ebabf53af..592d1bf08a 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointsProviderObjectTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointsProviderObjectTest.java @@ -24,6 +24,7 @@ import ghidra.dbg.target.schema.SchemaContext; import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName; import ghidra.dbg.target.schema.XmlSchemaContext; import ghidra.trace.model.Trace; +import ghidra.trace.model.target.TraceObjectKeyPath; import ghidra.util.database.UndoableTransaction; @Category(NightlyCategory.class) @@ -57,46 +58,58 @@ public class DebuggerBreakpointsProviderObjectTest extends DebuggerBreakpointsPr // NOTE the use of index='...' allowing object-based managers to ID unique path // TODO: I guess this'll burn down if the naming scheme changes.... int index = tb.trace.getName().startsWith("[3]") ? 3 : 1; - ctx = XmlSchemaContext.deserialize("" + // - "" + // - " " + // - " " + // - " " + // - " " + // - " " + // <---- NOTE HERE - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - ""); + ctx = XmlSchemaContext.deserialize(String.format(""" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + """, index)); try (UndoableTransaction tid = tb.startTransaction()) { tb.trace.getObjectManager().createRootObject(ctx.getSchema(new SchemaName("Session"))); + tb.trace.getObjectManager() + .createObject( + TraceObjectKeyPath.of("Processes", "[" + index + "]", "Breakpoints")); } } } diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointsProviderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointsProviderTest.java index 76bf66db00..5e5d438475 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointsProviderTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointsProviderTest.java @@ -34,6 +34,8 @@ import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest; import ghidra.app.plugin.core.debug.gui.DebuggerResources.*; import ghidra.app.plugin.core.debug.gui.breakpoint.DebuggerBreakpointsProvider.LogicalBreakpointTableModel; import ghidra.app.plugin.core.debug.gui.console.DebuggerConsolePlugin; +import ghidra.app.plugin.core.debug.service.editing.DebuggerStateEditingServicePlugin; +import ghidra.app.plugin.core.debug.service.emulation.ProgramEmulationUtils; import ghidra.app.plugin.core.debug.service.modules.DebuggerStaticMappingUtils; import ghidra.app.services.*; import ghidra.app.services.LogicalBreakpoint.State; @@ -41,12 +43,14 @@ import ghidra.dbg.model.TestTargetProcess; import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; import ghidra.dbg.target.TargetBreakpointSpecContainer; import ghidra.framework.store.LockException; +import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressOverflowException; import ghidra.program.model.listing.Program; import ghidra.program.model.mem.MemoryConflictException; import ghidra.program.util.ProgramLocation; import ghidra.trace.model.*; import ghidra.trace.model.breakpoint.TraceBreakpoint; +import ghidra.trace.model.time.TraceSnapshot; import ghidra.util.SystemUtilities; import ghidra.util.database.UndoableTransaction; import ghidra.util.exception.CancelledException; @@ -682,4 +686,110 @@ public class DebuggerBreakpointsProviderTest extends AbstractGhidraHeadedDebugge // NOTE: With no selection, no actions (even table built-in) apply, so no menu } + + @Test + public void testEmuBreakpointState() throws Throwable { + addPlugin(tool, DebuggerStateEditingServicePlugin.class); + + createProgram(); + intoProject(program); + programManager.openProgram(program); + waitForSwing(); + + addStaticMemoryAndBreakpoint(); + waitForProgram(program); + + LogicalBreakpointRow row = waitForValue( + () -> Unique.assertAtMostOne(breakpointsProvider.breakpointTableModel.getModelData())); + assertEquals(State.INEFFECTIVE_ENABLED, row.getState()); + + // Do our own launch, so that object mode is enabled during load (region creation) + createTrace(program.getLanguageID().getIdAsString()); + try (UndoableTransaction startTransaction = tb.startTransaction()) { + TraceSnapshot initial = tb.trace.getTimeManager().getSnapshot(0, true); + ProgramEmulationUtils.loadExecutable(initial, program); + Address pc = program.getMinAddress(); + ProgramEmulationUtils.doLaunchEmulationThread(tb.trace, 0, program, pc, pc); + } + + traceManager.openTrace(tb.trace); + traceManager.activateTrace(tb.trace); + waitForSwing(); + waitOn(mappingService.changesSettled()); + waitOn(breakpointService.changesSettled()); + waitForSwing(); + + row = waitForValue( + () -> Unique.assertAtMostOne(breakpointsProvider.breakpointTableModel.getModelData())); + assertEquals(State.INEFFECTIVE_ENABLED, row.getState()); + + row.setEnabled(true); + waitForSwing(); + + row = waitForValue( + () -> Unique.assertAtMostOne(breakpointsProvider.breakpointTableModel.getModelData())); + assertEquals(State.ENABLED, row.getState()); + } + + @Test + public void testTablesAndStatesWhenhModeChanges() throws Throwable { + DebuggerStateEditingService editingService = + addPlugin(tool, DebuggerStateEditingServicePlugin.class); + + createTestModel(); + mb.createTestProcessesAndThreads(); + TraceRecorder recorder = modelService.recordTarget(mb.testProcess1, + createTargetTraceMapper(mb.testProcess1), ActionSource.AUTOMATIC); + Trace trace = recorder.getTrace(); + createProgramFromTrace(trace); + intoProject(trace); + intoProject(program); + + mb.testProcess1.addRegion("bin:.text", mb.rng(0x55550000, 0x55550fff), "rx"); + waitRecorder(recorder); + addMapping(trace, program); + addStaticMemoryAndBreakpoint(); + programManager.openProgram(program); + traceManager.openTrace(trace); + waitForSwing(); + + LogicalBreakpointRow lbRow1 = waitForPass(() -> { + LogicalBreakpointRow newRow = + Unique.assertOne(breakpointsProvider.breakpointTableModel.getModelData()); + LogicalBreakpoint lb = newRow.getLogicalBreakpoint(); + assertEquals(program, lb.getProgram()); + assertEquals(Set.of(trace), lb.getMappedTraces()); + assertEquals(Set.of(), lb.getParticipatingTraces()); + assertEquals(State.INEFFECTIVE_ENABLED, newRow.getState()); + return newRow; + }); + + editingService.setCurrentMode(trace, StateEditingMode.RW_EMULATOR); + lbRow1.setEnabled(true); + TraceBreakpoint emuBpt = waitForValue( + () -> Unique.assertAtMostOne(trace.getBreakpointManager().getAllBreakpoints())); + assertNull(recorder.getTargetBreakpoint(emuBpt)); + + LogicalBreakpointRow lbRow2 = + Unique.assertOne(breakpointsProvider.breakpointTableModel.getModelData()); + waitForPass(() -> assertEquals(State.ENABLED, lbRow2.getState())); + + waitForPass(() -> { + BreakpointLocationRow newRow = + Unique.assertOne(breakpointsProvider.locationTableModel.getModelData()); + assertEquals(State.ENABLED, newRow.getState()); + }); + + for (int i = 0; i < 3; i++) { + editingService.setCurrentMode(trace, StateEditingMode.RO_TARGET); + waitOn(breakpointService.changesSettled()); + waitForSwing(); + assertEquals(0, breakpointsProvider.locationTableModel.getModelData().size()); + + editingService.setCurrentMode(trace, StateEditingMode.RW_EMULATOR); + waitOn(breakpointService.changesSettled()); + waitForSwing(); + assertEquals(1, breakpointsProvider.locationTableModel.getModelData().size()); + } + } } diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/control/DebuggerControlPluginTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/control/DebuggerControlPluginTest.java index e3b7952eb4..5e045bf9df 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/control/DebuggerControlPluginTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/control/DebuggerControlPluginTest.java @@ -48,7 +48,6 @@ import ghidra.app.plugin.core.debug.service.emulation.DebuggerEmulationServicePl import ghidra.app.services.*; import ghidra.app.services.DebuggerEmulationService.CachedEmulator; import ghidra.app.services.DebuggerEmulationService.EmulationResult; -import ghidra.app.services.DebuggerStateEditingService.StateEditingMode; import ghidra.dbg.model.*; import ghidra.dbg.target.TargetObject; import ghidra.dbg.target.TargetSteppable.TargetStepKind; @@ -73,8 +72,8 @@ import ghidra.util.database.UndoableTransaction; * *

    * In these and other machine-state-editing integration tests, we use - * {@link StateEditingMode#WRITE_EMULATOR} as a stand-in for any mode. We also use - * {@link StateEditingMode#READ_ONLY} just to verify the mode is heeded. Other modes may be tested + * {@link StateEditingMode#RW_EMULATOR} as a stand-in for any mode. We also use + * {@link StateEditingMode#RO_TARGET} just to verify the mode is heeded. Other modes may be tested * if bugs crop up in various combinations. */ public class DebuggerControlPluginTest extends AbstractGhidraHeadedDebuggerGUITest { @@ -294,7 +293,7 @@ public class DebuggerControlPluginTest extends AbstractGhidraHeadedDebuggerGUITe @Test public void testEmulateResumeAction() throws Throwable { TraceThread thread = createToyLoopTrace(); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_EMULATOR); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_EMULATOR); traceManager.activateThread(thread); waitForSwing(); @@ -311,7 +310,7 @@ public class DebuggerControlPluginTest extends AbstractGhidraHeadedDebuggerGUITe @Test public void testEmulateInterruptAction() throws Throwable { TraceThread thread = createToyLoopTrace(); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_EMULATOR); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_EMULATOR); traceManager.activateThread(thread); waitForSwing(); @@ -333,7 +332,7 @@ public class DebuggerControlPluginTest extends AbstractGhidraHeadedDebuggerGUITe @Test public void testEmulateStepBackAction() throws Throwable { TraceThread thread = createToyLoopTrace(); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_EMULATOR); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_EMULATOR); traceManager.activateThread(thread); waitForSwing(); @@ -352,7 +351,7 @@ public class DebuggerControlPluginTest extends AbstractGhidraHeadedDebuggerGUITe @Test public void testEmulateStepIntoAction() throws Throwable { TraceThread thread = createToyLoopTrace(); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_EMULATOR); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_EMULATOR); traceManager.activateThread(thread); waitForSwing(); @@ -365,7 +364,7 @@ public class DebuggerControlPluginTest extends AbstractGhidraHeadedDebuggerGUITe @Test public void testEmulateSkipOverAction() throws Throwable { TraceThread thread = createToyLoopTrace(); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_EMULATOR); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_EMULATOR); traceManager.activateThread(thread); waitForSwing(); @@ -386,7 +385,7 @@ public class DebuggerControlPluginTest extends AbstractGhidraHeadedDebuggerGUITe @Test public void testTraceSnapBackwardAction() throws Throwable { create2SnapTrace(); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_TRACE); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_TRACE); traceManager.activateTrace(tb.trace); waitForSwing(); @@ -403,7 +402,7 @@ public class DebuggerControlPluginTest extends AbstractGhidraHeadedDebuggerGUITe @Test public void testTraceSnapForwardAction() throws Throwable { create2SnapTrace(); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_TRACE); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_TRACE); traceManager.activateTrace(tb.trace); waitForSwing(); @@ -444,14 +443,14 @@ public class DebuggerControlPluginTest extends AbstractGhidraHeadedDebuggerGUITe assertTrue(controlPlugin.actionEditMode.isEnabled()); runSwing(() -> controlPlugin.actionEditMode - .setCurrentActionStateByUserData(StateEditingMode.READ_ONLY)); - assertEquals(StateEditingMode.READ_ONLY, editingService.getCurrentMode(tb.trace)); + .setCurrentActionStateByUserData(StateEditingMode.RO_TARGET)); + assertEquals(StateEditingMode.RO_TARGET, editingService.getCurrentMode(tb.trace)); assertFalse( helper.patchInstructionAction.isAddToPopup(listingProvider.getActionContext(null))); runSwing(() -> controlPlugin.actionEditMode - .setCurrentActionStateByUserData(StateEditingMode.WRITE_EMULATOR)); - assertEquals(StateEditingMode.WRITE_EMULATOR, editingService.getCurrentMode(tb.trace)); + .setCurrentActionStateByUserData(StateEditingMode.RW_EMULATOR)); + assertEquals(StateEditingMode.RW_EMULATOR, editingService.getCurrentMode(tb.trace)); assertTrue( helper.patchInstructionAction.isAddToPopup(listingProvider.getActionContext(null))); @@ -494,13 +493,13 @@ public class DebuggerControlPluginTest extends AbstractGhidraHeadedDebuggerGUITe assertTrue(controlPlugin.actionEditMode.isEnabled()); runSwing(() -> controlPlugin.actionEditMode - .setCurrentActionStateByUserData(StateEditingMode.READ_ONLY)); - assertEquals(StateEditingMode.READ_ONLY, editingService.getCurrentMode(tb.trace)); + .setCurrentActionStateByUserData(StateEditingMode.RO_TARGET)); + assertEquals(StateEditingMode.RO_TARGET, editingService.getCurrentMode(tb.trace)); assertFalse(helper.patchDataAction.isAddToPopup(listingProvider.getActionContext(null))); runSwing(() -> controlPlugin.actionEditMode - .setCurrentActionStateByUserData(StateEditingMode.WRITE_EMULATOR)); - assertEquals(StateEditingMode.WRITE_EMULATOR, editingService.getCurrentMode(tb.trace)); + .setCurrentActionStateByUserData(StateEditingMode.RW_EMULATOR)); + assertEquals(StateEditingMode.RW_EMULATOR, editingService.getCurrentMode(tb.trace)); goTo(listingProvider.getListingPanel(), new ProgramLocation(view, tb.addr(0x00400123))); assertTrue(helper.patchDataAction.isAddToPopup(listingProvider.getActionContext(null))); @@ -545,15 +544,15 @@ public class DebuggerControlPluginTest extends AbstractGhidraHeadedDebuggerGUITe assertTrue(controlPlugin.actionEditMode.isEnabled()); runSwing(() -> controlPlugin.actionEditMode - .setCurrentActionStateByUserData(StateEditingMode.READ_ONLY)); - assertEquals(StateEditingMode.READ_ONLY, editingService.getCurrentMode(tb.trace)); + .setCurrentActionStateByUserData(StateEditingMode.RO_TARGET)); + assertEquals(StateEditingMode.RO_TARGET, editingService.getCurrentMode(tb.trace)); ctx = listingProvider.getActionContext(null); assertTrue(pasteAction.isAddToPopup(ctx)); assertFalse(pasteAction.isEnabledForContext(ctx)); runSwing(() -> controlPlugin.actionEditMode - .setCurrentActionStateByUserData(StateEditingMode.WRITE_EMULATOR)); - assertEquals(StateEditingMode.WRITE_EMULATOR, editingService.getCurrentMode(tb.trace)); + .setCurrentActionStateByUserData(StateEditingMode.RW_EMULATOR)); + assertEquals(StateEditingMode.RW_EMULATOR, editingService.getCurrentMode(tb.trace)); goTo(listingPlugin.getListingPanel(), new ProgramLocation(view, tb.addr(0x00400123))); ctx = listingProvider.getActionContext(null); diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerMemoryBytesProviderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerMemoryBytesProviderTest.java index 90856c40c3..f98fcdf479 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerMemoryBytesProviderTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerMemoryBytesProviderTest.java @@ -49,9 +49,7 @@ import ghidra.app.plugin.core.debug.gui.DebuggerResources.FollowsCurrentThreadAc import ghidra.app.plugin.core.debug.gui.action.*; import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingPlugin; import ghidra.app.plugin.core.debug.service.editing.DebuggerStateEditingServicePlugin; -import ghidra.app.services.DebuggerStateEditingService; -import ghidra.app.services.DebuggerStateEditingService.StateEditingMode; -import ghidra.app.services.TraceRecorder; +import ghidra.app.services.*; import ghidra.async.SwingExecutorService; import ghidra.program.model.address.*; import ghidra.program.model.lang.Register; @@ -510,6 +508,7 @@ public class DebuggerMemoryBytesProviderTest extends AbstractGhidraHeadedDebugge * Assure ourselves the block under test is not on screen */ waitForPass(() -> { + goToDyn(addr(trace, 0x55551800)); AddressSetView visible = memBytesProvider.readsMemTrait.getVisible(); assertFalse(visible.isEmpty()); assertFalse(visible.contains(addr(trace, 0x55550000))); @@ -1101,10 +1100,11 @@ public class DebuggerMemoryBytesProviderTest extends AbstractGhidraHeadedDebugge createTargetTraceMapper(mb.testProcess1)); Trace trace = recorder.getTrace(); - editingService.setCurrentMode(trace, StateEditingMode.WRITE_TARGET); + editingService.setCurrentMode(trace, StateEditingMode.RW_TARGET); DockingActionIf actionEdit = getAction(memBytesPlugin, "Enable/Disable Byteviewer Editing"); mb.testProcess1.addRegion("exe:.text", mb.rng(0x55550000, 0x5555ffff), "rx"); + waitRecorder(recorder); waitFor(() -> !trace.getMemoryManager().getAllRegions().isEmpty()); byte[] data = new byte[4]; @@ -1132,12 +1132,17 @@ public class DebuggerMemoryBytesProviderTest extends AbstractGhidraHeadedDebugge createTargetTraceMapper(mb.testProcess1)); Trace trace = recorder.getTrace(); - editingService.setCurrentMode(trace, StateEditingMode.WRITE_TRACE); + editingService.setCurrentMode(trace, StateEditingMode.RW_TRACE); DockingActionIf actionEdit = getAction(memBytesPlugin, "Enable/Disable Byteviewer Editing"); mb.testProcess1.addRegion("exe:.text", mb.rng(0x55550000, 0x5555ffff), "rx"); + waitRecorder(recorder); waitFor(() -> !trace.getMemoryManager().getAllRegions().isEmpty()); + // Because mode is RW_TRACE, we're not necessarily at recorder's snap + traceManager.activateSnap(recorder.getSnap()); + waitForSwing(); + byte[] data = new byte[4]; performAction(actionEdit); waitForPass(noExc(() -> { @@ -1174,7 +1179,7 @@ public class DebuggerMemoryBytesProviderTest extends AbstractGhidraHeadedDebugge createTargetTraceMapper(mb.testProcess1)); Trace trace = recorder.getTrace(); - editingService.setCurrentMode(trace, StateEditingMode.WRITE_TARGET); + editingService.setCurrentMode(trace, StateEditingMode.RW_TARGET); mb.testProcess1.addRegion("exe:.text", mb.rng(0x55550000, 0x5555ffff), "rx"); waitFor(() -> !trace.getMemoryManager().getAllRegions().isEmpty()); diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/register/DebuggerRegistersProviderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/register/DebuggerRegistersProviderTest.java index e738cdb0ac..09c3c79d59 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/register/DebuggerRegistersProviderTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/register/DebuggerRegistersProviderTest.java @@ -33,9 +33,7 @@ import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingPlugin; import ghidra.app.plugin.core.debug.gui.register.DebuggerRegistersProvider.RegisterDataSettingsDialog; import ghidra.app.plugin.core.debug.gui.register.DebuggerRegistersProvider.RegisterTableColumns; import ghidra.app.plugin.core.debug.service.editing.DebuggerStateEditingServicePlugin; -import ghidra.app.services.DebuggerStateEditingService; -import ghidra.app.services.DebuggerStateEditingService.StateEditingMode; -import ghidra.app.services.TraceRecorder; +import ghidra.app.services.*; import ghidra.docking.settings.FormatSettingsDefinition; import ghidra.docking.settings.Settings; import ghidra.program.model.address.AddressSpace; @@ -385,7 +383,7 @@ public class DebuggerRegistersProviderTest extends AbstractGhidraHeadedDebuggerG activateThread(thread); waitForSwing(); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_EMULATOR); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_EMULATOR); assertTrue(registersProvider.actionEnableEdits.isEnabled()); performAction(registersProvider.actionEnableEdits); @@ -423,7 +421,7 @@ public class DebuggerRegistersProviderTest extends AbstractGhidraHeadedDebuggerG activateThread(thread); waitForSwing(); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_EMULATOR); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_EMULATOR); assertTrue(registersProvider.actionEnableEdits.isEnabled()); performAction(registersProvider.actionEnableEdits); diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProviderLegacyTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProviderLegacyTest.java index 7d8f1256c5..0bc41451c9 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProviderLegacyTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProviderLegacyTest.java @@ -28,12 +28,10 @@ import org.junit.experimental.categories.Category; import generic.test.category.NightlyCategory; import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest; import ghidra.app.plugin.core.debug.gui.thread.DebuggerLegacyThreadsPanel.ThreadTableColumns; -import ghidra.app.services.TraceRecorder; import ghidra.trace.model.Lifespan; import ghidra.trace.model.Trace; import ghidra.trace.model.thread.TraceThread; import ghidra.trace.model.thread.TraceThreadManager; -import ghidra.trace.model.time.TraceSnapshot; import ghidra.trace.model.time.TraceTimeManager; import ghidra.util.database.UndoableTransaction; @@ -400,53 +398,4 @@ public class DebuggerThreadsProviderLegacyTest extends AbstractGhidraHeadedDebug assertEquals(6, threadsProvider.legacyPanel.headerRenderer.getCursorPosition().longValue()); } - @Test - public void testActionSeekTracePresent() throws Exception { - assertTrue(threadsProvider.actionSeekTracePresent.isSelected()); - - createAndOpenTrace(); - addThreads(); - traceManager.activateTrace(tb.trace); - waitForSwing(); - - assertEquals(0, traceManager.getCurrentSnap()); - - try (UndoableTransaction tid = tb.startTransaction()) { - tb.trace.getTimeManager().createSnapshot("Next snapshot"); - } - waitForDomainObject(tb.trace); - - // Not live, so no seek - assertEquals(0, traceManager.getCurrentSnap()); - - tb.close(); - - createTestModel(); - mb.createTestProcessesAndThreads(); - // Threads needs registers to be recognized by the recorder - mb.createTestThreadRegisterBanks(); - - TraceRecorder recorder = modelService.recordTargetAndActivateTrace(mb.testProcess1, - createTargetTraceMapper(mb.testProcess1)); - Trace trace = recorder.getTrace(); - - // Wait till two threads are observed in the database - waitForPass(() -> assertEquals(2, trace.getThreadManager().getAllThreads().size())); - waitForSwing(); - - TraceSnapshot snapshot = recorder.forceSnapshot(); - waitForDomainObject(trace); - - assertEquals(snapshot.getKey(), traceManager.getCurrentSnap()); - - performAction(threadsProvider.actionSeekTracePresent); - waitForSwing(); - - assertFalse(threadsProvider.actionSeekTracePresent.isSelected()); - - recorder.forceSnapshot(); - waitForSwing(); - - assertEquals(snapshot.getKey(), traceManager.getCurrentSnap()); - } } diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProviderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProviderTest.java index 8e08331c40..4e77bac93e 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProviderTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProviderTest.java @@ -31,13 +31,13 @@ import ghidra.app.plugin.core.debug.DebuggerCoordinates; import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest; import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel; import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.*; +import ghidra.app.plugin.core.debug.gui.model.QueryPanelTestHelper; import ghidra.app.plugin.core.debug.mapping.DebuggerTargetTraceMapper; import ghidra.app.plugin.core.debug.mapping.ObjectBasedDebuggerTargetTraceMapper; -import ghidra.app.plugin.core.debug.gui.model.QueryPanelTestHelper; import ghidra.app.services.TraceRecorder; import ghidra.dbg.target.TargetExecutionStateful; -import ghidra.dbg.target.TargetObject; import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; +import ghidra.dbg.target.TargetObject; import ghidra.dbg.target.schema.SchemaContext; import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName; import ghidra.dbg.target.schema.XmlSchemaContext; @@ -51,7 +51,6 @@ import ghidra.trace.model.target.TraceObject.ConflictResolution; import ghidra.trace.model.target.TraceObjectKeyPath; import ghidra.trace.model.target.TraceObjectManager; import ghidra.trace.model.thread.TraceObjectThread; -import ghidra.trace.model.time.TraceSnapshot; import ghidra.trace.model.time.TraceTimeManager; import ghidra.util.database.UndoableTransaction; import ghidra.util.table.GhidraTable; @@ -564,52 +563,4 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerGUI waitForPass(() -> assertEquals(Long.valueOf(6), renderer.getCursorPosition())); } - - @Test - public void testActionSeekTracePresent() throws Throwable { - assertTrue(provider.actionSeekTracePresent.isSelected()); - - createAndOpenTrace(); - addThreads(); - traceManager.activateTrace(tb.trace); - waitForTasks(); - - assertEquals(0, traceManager.getCurrentSnap()); - - try (UndoableTransaction tid = tb.startTransaction()) { - tb.trace.getTimeManager().createSnapshot("Next snapshot"); - } - waitForDomainObject(tb.trace); - waitForTasks(); - - // Not live, so no seek - waitForPass(() -> assertEquals(0, traceManager.getCurrentSnap())); - - tb.close(); - - createTestModel(); - mb.createTestProcessesAndThreads(); - // Threads needs registers to be recognized by the recorder - mb.createTestThreadRegisterBanks(); - - TraceRecorder recorder = recordAndWaitSync(); - traceManager.openTrace(tb.trace); - traceManager.activateTrace(tb.trace); - - TraceSnapshot snapshot = recorder.forceSnapshot(); - waitForDomainObject(tb.trace); - waitForTasks(); - - waitForPass(() -> assertEquals(snapshot.getKey(), traceManager.getCurrentSnap())); - - performEnabledAction(provider, provider.actionSeekTracePresent, true); - waitForTasks(); - - assertFalse(provider.actionSeekTracePresent.isSelected()); - - recorder.forceSnapshot(); - waitForTasks(); - - waitForPass(() -> assertEquals(snapshot.getKey(), traceManager.getCurrentSnap())); - } } diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/watch/DebuggerWatchesProviderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/watch/DebuggerWatchesProviderTest.java index 70a6b01b67..0b5e8da716 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/watch/DebuggerWatchesProviderTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/watch/DebuggerWatchesProviderTest.java @@ -35,7 +35,6 @@ import ghidra.app.plugin.core.debug.gui.watch.DebuggerWatchesProvider.WatchDataS import ghidra.app.plugin.core.debug.service.editing.DebuggerStateEditingServicePlugin; import ghidra.app.plugin.core.debug.service.modules.DebuggerStaticMappingServicePlugin; import ghidra.app.services.*; -import ghidra.app.services.DebuggerStateEditingService.StateEditingMode; import ghidra.dbg.model.TestTargetRegisterBankInThread; import ghidra.docking.settings.FormatSettingsDefinition; import ghidra.docking.settings.Settings; @@ -350,7 +349,7 @@ public class DebuggerWatchesProviderTest extends AbstractGhidraHeadedDebuggerGUI assertFalse(row.isRawValueEditable()); traceManager.openTrace(tb.trace); traceManager.activateThread(thread); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_EMULATOR); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_EMULATOR); waitForWatches(); assertNoErr(row); @@ -384,7 +383,7 @@ public class DebuggerWatchesProviderTest extends AbstractGhidraHeadedDebuggerGUI traceManager.openTrace(tb.trace); traceManager.activateThread(thread); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_EMULATOR); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_EMULATOR); waitForWatches(); performAction(watchesProvider.actionEnableEdits); @@ -547,7 +546,7 @@ public class DebuggerWatchesProviderTest extends AbstractGhidraHeadedDebuggerGUI traceManager.openTrace(trace); traceManager.activateThread(thread); - editingService.setCurrentMode(trace, StateEditingMode.WRITE_TARGET); + editingService.setCurrentMode(trace, StateEditingMode.RW_TARGET); waitForSwing(); performAction(watchesProvider.actionAdd); diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/breakpoint/DebuggerLogicalBreakpointServiceObjectTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/breakpoint/DebuggerLogicalBreakpointServiceObjectTest.java index acee8c2a9a..dbe87dcc97 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/breakpoint/DebuggerLogicalBreakpointServiceObjectTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/breakpoint/DebuggerLogicalBreakpointServiceObjectTest.java @@ -21,6 +21,7 @@ import ghidra.dbg.target.schema.SchemaContext; import ghidra.dbg.target.schema.XmlSchemaContext; import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName; import ghidra.trace.model.Trace; +import ghidra.trace.model.target.TraceObjectKeyPath; import ghidra.util.database.UndoableTransaction; public class DebuggerLogicalBreakpointServiceObjectTest @@ -54,46 +55,58 @@ public class DebuggerLogicalBreakpointServiceObjectTest // NOTE the use of index='...' allowing object-based managers to ID unique path // TODO: I guess this'll burn down if the naming scheme changes.... int index = tb.trace.getName().startsWith("[3]") ? 3 : 1; - ctx = XmlSchemaContext.deserialize("" + // - "" + // - " " + // - " " + // - " " + // - " " + // - " " + // <---- NOTE HERE - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - ""); + ctx = XmlSchemaContext.deserialize(String.format(""" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + """, index)); try (UndoableTransaction tid = tb.startTransaction()) { tb.trace.getObjectManager().createRootObject(ctx.getSchema(new SchemaName("Session"))); + tb.trace.getObjectManager() + .createObject( + TraceObjectKeyPath.of("Processes", "[" + index + "]", "Breakpoints")); } } } diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/breakpoint/DebuggerLogicalBreakpointServiceTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/breakpoint/DebuggerLogicalBreakpointServiceTest.java index e81e2856ba..291ac97444 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/breakpoint/DebuggerLogicalBreakpointServiceTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/breakpoint/DebuggerLogicalBreakpointServiceTest.java @@ -38,9 +38,10 @@ import ghidra.program.model.address.Address; import ghidra.program.model.listing.Bookmark; import ghidra.program.model.listing.Program; import ghidra.program.util.ProgramLocation; -import ghidra.trace.model.DefaultTraceLocation; -import ghidra.trace.model.Trace; +import ghidra.trace.database.ToyDBTraceBuilder; +import ghidra.trace.model.*; import ghidra.trace.model.breakpoint.*; +import ghidra.trace.model.memory.TraceMemoryFlag; import ghidra.trace.model.memory.TraceMemoryRegion; import ghidra.trace.model.modules.TraceStaticMapping; import ghidra.util.Msg; @@ -1552,4 +1553,125 @@ public class DebuggerLogicalBreakpointServiceTest extends AbstractGhidraHeadedDe waitForPass( () -> assertEquals(State.MIXED, lbEx.computeState().sameAdddress(lbRw.computeState()))); } + + protected void addTextMappingDead(Program p, ToyDBTraceBuilder tb) throws Throwable { + addProgramTextBlock(p); + try (UndoableTransaction tid = tb.startTransaction()) { + TraceMemoryRegion textRegion = tb.trace.getMemoryManager() + .addRegion("Processes[1].Memory[bin:.text]", Lifespan.nowOn(0), + tb.range(0x55550000, 0x55550fff), + Set.of(TraceMemoryFlag.READ, TraceMemoryFlag.EXECUTE)); + DebuggerStaticMappingUtils.addMapping( + new DefaultTraceLocation(tb.trace, null, textRegion.getLifespan(), + textRegion.getMinAddress()), + new ProgramLocation(p, addr(p, 0x00400000)), 0x1000, + false); + } + } + + protected void addEnabledProgramBreakpointWithSleigh(Program p) { + try (UndoableTransaction tid = + UndoableTransaction.start(p, "Create bookmark bp with sleigh")) { + enBm = p.getBookmarkManager() + .setBookmark(addr(p, 0x00400123), + LogicalBreakpoint.BREAKPOINT_ENABLED_BOOKMARK_TYPE, "SW_EXECUTE;1", + "{sleigh: 'r0=0xbeef;'}"); + } + } + + @Test + public void testMapThenAddProgramBreakpointWithSleighThenEnableOnTraceCopiesSleigh() + throws Throwable { + createTrace(); + traceManager.openTrace(tb.trace); + createProgramFromTrace(); + intoProject(program); + programManager.openProgram(program); + + addTextMappingDead(program, tb); + waitForSwing(); + + addEnabledProgramBreakpointWithSleigh(program); + LogicalBreakpoint lb = waitForValue(() -> Unique.assertAtMostOne( + breakpointService.getBreakpointsAt(program, addr(program, 0x00400123)))); + + assertEquals("r0=0xbeef;", lb.getEmuSleigh()); + + waitOn(lb.enable()); + waitForSwing(); + + TraceBreakpoint bpt = Unique.assertOne( + tb.trace.getBreakpointManager().getBreakpointsAt(0, tb.addr(0x55550123))); + assertEquals("r0=0xbeef;", bpt.getEmuSleigh()); + } + + @Test + public void testAddProgramBreakpointWithSleighThenMapThenEnableOnTraceCopiesSleigh() + throws Throwable { + createTrace(); + traceManager.openTrace(tb.trace); + createProgramFromTrace(); + intoProject(program); + programManager.openProgram(program); + + addEnabledProgramBreakpointWithSleigh(program); + LogicalBreakpoint lb = waitForValue(() -> Unique.assertAtMostOne( + breakpointService.getBreakpointsAt(program, addr(program, 0x00400123)))); + + assertEquals("r0=0xbeef;", lb.getEmuSleigh()); + + addTextMappingDead(program, tb); + lb = waitForPass(() -> { + LogicalBreakpoint newLb = Unique.assertOne( + breakpointService.getBreakpointsAt(program, addr(program, 0x00400123))); + assertTrue(newLb.getMappedTraces().contains(tb.trace)); + return newLb; + }); + + waitOn(lb.enable()); + waitForSwing(); + + TraceBreakpoint bpt = Unique.assertOne( + tb.trace.getBreakpointManager().getBreakpointsAt(0, tb.addr(0x55550123))); + assertEquals("r0=0xbeef;", bpt.getEmuSleigh()); + } + + @Test + public void testAddTraceBreakpointSetSleighThenMapThenSaveToProgramCopiesSleigh() + throws Throwable { + // TODO: What if already mapped? + // Not sure I care about tb.setEmuSleigh() out of band + + createTrace(); + traceManager.openTrace(tb.trace); + createProgramFromTrace(); + intoProject(program); + programManager.openProgram(program); + + try (UndoableTransaction tid = tb.startTransaction()) { + TraceBreakpoint bpt = tb.trace.getBreakpointManager() + .addBreakpoint("Processes[1].Breakpoints[0]", Lifespan.nowOn(0), + tb.addr(0x55550123), + Set.of(), Set.of(TraceBreakpointKind.SW_EXECUTE), + false /* emuEnabled defaults to true */, ""); + bpt.setEmuSleigh("r0=0xbeef;"); + } + LogicalBreakpoint lb = waitForValue(() -> Unique.assertAtMostOne( + breakpointService.getBreakpointsAt(tb.trace, tb.addr(0x55550123)))); + + assertEquals("r0=0xbeef;", lb.getEmuSleigh()); + + addTextMappingDead(program, tb); + lb = waitForPass(() -> { + LogicalBreakpoint newLb = Unique.assertOne( + breakpointService.getBreakpointsAt(program, addr(program, 0x00400123))); + assertTrue(newLb.getMappedTraces().contains(tb.trace)); + return newLb; + }); + + lb.enableForProgram(); + waitForSwing(); + + assertEquals("{\"sleigh\":\"r0\\u003d0xbeef;\"}", lb.getProgramBookmark().getComment()); + } } diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/editing/DebuggerStateEditingServiceTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/editing/DebuggerStateEditingServiceTest.java index eec650971e..142965fabf 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/editing/DebuggerStateEditingServiceTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/editing/DebuggerStateEditingServiceTest.java @@ -26,10 +26,8 @@ import org.junit.Test; import ghidra.app.plugin.assembler.*; import ghidra.app.plugin.core.debug.DebuggerCoordinates; import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest; -import ghidra.app.services.DebuggerStateEditingService; -import ghidra.app.services.DebuggerStateEditingService.StateEditingMode; +import ghidra.app.services.*; import ghidra.app.services.DebuggerStateEditingService.StateEditor; -import ghidra.app.services.TraceRecorder; import ghidra.async.AsyncUtils.TemperamentalRunnable; import ghidra.dbg.target.TargetRegisterBank; import ghidra.program.model.lang.*; @@ -56,6 +54,7 @@ public class DebuggerStateEditingServiceTest extends AbstractGhidraHeadedDebugge protected void activateTrace() { traceManager.activateTrace(tb.trace); + waitForSwing(); } protected TracePlatform getPlatform() { @@ -105,7 +104,7 @@ public class DebuggerStateEditingServiceTest extends AbstractGhidraHeadedDebugge createAndOpenTrace(); activateTrace(); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_EMULATOR); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_EMULATOR); StateEditor editor = createStateEditor(); assertFalse(editor.isVariableEditable(tb.addr(0x00400000), 4)); @@ -117,7 +116,7 @@ public class DebuggerStateEditingServiceTest extends AbstractGhidraHeadedDebugge @Test public void testWriteEmuRegisterNoThreadErr() throws Throwable { createAndOpenTrace(); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_EMULATOR); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_EMULATOR); activateTrace(); waitForSwing(); @@ -132,7 +131,7 @@ public class DebuggerStateEditingServiceTest extends AbstractGhidraHeadedDebugge @Test public void testWriteEmuMemory() throws Throwable { createAndOpenTrace(); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_EMULATOR); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_EMULATOR); try (UndoableTransaction tid = tb.startTransaction()) { // NB. TraceManager should automatically activate the first thread @@ -158,7 +157,7 @@ public class DebuggerStateEditingServiceTest extends AbstractGhidraHeadedDebugge @Test public void testWriteEmuRegister() throws Throwable { createAndOpenTrace(); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_EMULATOR); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_EMULATOR); TraceThread thread; try (UndoableTransaction tid = tb.startTransaction()) { @@ -187,7 +186,7 @@ public class DebuggerStateEditingServiceTest extends AbstractGhidraHeadedDebugge @Test public void testWriteEmuMemoryAfterStep() throws Throwable { createAndOpenTrace(); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_TRACE); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_TRACE); try (UndoableTransaction tid = tb.startTransaction()) { // NB. TraceManager should automatically activate the first thread @@ -200,7 +199,7 @@ public class DebuggerStateEditingServiceTest extends AbstractGhidraHeadedDebugge tb.exec(getPlatform(), 0, thread, 0, "pc = 0x00400000;"); } activateTrace(); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_EMULATOR); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_EMULATOR); waitForSwing(); TraceSchedule step1 = TraceSchedule.parse("0:t0-1"); @@ -225,7 +224,7 @@ public class DebuggerStateEditingServiceTest extends AbstractGhidraHeadedDebugge @Test public void testWriteEmuRegisterAfterStep() throws Throwable { createAndOpenTrace(); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_TRACE); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_TRACE); TraceThread thread; try (UndoableTransaction tid = tb.startTransaction()) { @@ -239,7 +238,7 @@ public class DebuggerStateEditingServiceTest extends AbstractGhidraHeadedDebugge tb.exec(getPlatform(), 0, thread, 0, "pc = 0x00400000;"); } activateTrace(); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_EMULATOR); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_EMULATOR); waitForSwing(); TraceSchedule step1 = TraceSchedule.parse("0:t0-1"); @@ -265,7 +264,7 @@ public class DebuggerStateEditingServiceTest extends AbstractGhidraHeadedDebugge @Test public void testWriteEmuMemoryTwice() throws Throwable { createAndOpenTrace(); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_EMULATOR); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_EMULATOR); try (UndoableTransaction tid = tb.startTransaction()) { // NB. TraceManager should automatically activate the first thread @@ -294,7 +293,7 @@ public class DebuggerStateEditingServiceTest extends AbstractGhidraHeadedDebugge @Test public void testWriteEmuRegisterTwice() throws Throwable { createAndOpenTrace(); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_EMULATOR); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_EMULATOR); TraceThread thread; try (UndoableTransaction tid = tb.startTransaction()) { @@ -325,7 +324,7 @@ public class DebuggerStateEditingServiceTest extends AbstractGhidraHeadedDebugge public void testWriteTraceMemory() throws Throwable { // NB. Definitely no thread required createAndOpenTrace(); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_TRACE); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_TRACE); activateTrace(); waitForSwing(); @@ -348,7 +347,7 @@ public class DebuggerStateEditingServiceTest extends AbstractGhidraHeadedDebugge public void testWriteTraceRegisterNoThreadErr() throws Throwable { // NB. Definitely no thread required createAndOpenTrace(); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_TRACE); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_TRACE); activateTrace(); waitForSwing(); @@ -364,7 +363,7 @@ public class DebuggerStateEditingServiceTest extends AbstractGhidraHeadedDebugge public void testWriteTraceRegister() throws Throwable { // NB. Definitely no thread required createAndOpenTrace(); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_TRACE); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_TRACE); TraceThread thread; try (UndoableTransaction tid = tb.startTransaction()) { @@ -397,7 +396,7 @@ public class DebuggerStateEditingServiceTest extends AbstractGhidraHeadedDebugge activateTrace(); traceManager.activateThread(recorder.getTraceThread(mb.testThread1)); waitForSwing(); - editingService.setCurrentMode(recorder.getTrace(), StateEditingMode.WRITE_TARGET); + editingService.setCurrentMode(recorder.getTrace(), StateEditingMode.RW_TARGET); StateEditor editor = createStateEditor(); assertTrue(editor.isVariableEditable(tb.addr(0x00400000), 4)); @@ -417,7 +416,7 @@ public class DebuggerStateEditingServiceTest extends AbstractGhidraHeadedDebugge waitForSwing(); traceManager.activateThread(recorder.getTraceThread(mb.testThread1)); waitForSwing(); - editingService.setCurrentMode(recorder.getTrace(), StateEditingMode.WRITE_TARGET); + editingService.setCurrentMode(recorder.getTrace(), StateEditingMode.RW_TARGET); StateEditor editor = createStateEditor(); assertTrue(editor.isRegisterEditable(r0)); @@ -436,7 +435,7 @@ public class DebuggerStateEditingServiceTest extends AbstractGhidraHeadedDebugge TraceThread thread = recorder.getTraceThread(mb.testThread1); traceManager.activateThread(thread); waitForSwing(); - editingService.setCurrentMode(recorder.getTrace(), StateEditingMode.WRITE_TARGET); + editingService.setCurrentMode(recorder.getTrace(), StateEditingMode.RW_TARGET); StateEditor editor = createStateEditor(); assertTrue(editor.isRegisterEditable(r0)); @@ -461,7 +460,7 @@ public class DebuggerStateEditingServiceTest extends AbstractGhidraHeadedDebugge activateTrace(); traceManager.activateThread(recorder.getTraceThread(mb.testThread1)); waitForSwing(); - editingService.setCurrentMode(recorder.getTrace(), StateEditingMode.WRITE_TARGET); + editingService.setCurrentMode(recorder.getTrace(), StateEditingMode.RW_TARGET); traceManager.activateSnap(traceManager.getCurrentSnap() - 1); @@ -479,7 +478,7 @@ public class DebuggerStateEditingServiceTest extends AbstractGhidraHeadedDebugge activateTrace(); traceManager.activateThread(recorder.getTraceThread(mb.testThread1)); waitForSwing(); - editingService.setCurrentMode(recorder.getTrace(), StateEditingMode.WRITE_TARGET); + editingService.setCurrentMode(recorder.getTrace(), StateEditingMode.RW_TARGET); traceManager.activateSnap(traceManager.getCurrentSnap() - 1); @@ -494,7 +493,7 @@ public class DebuggerStateEditingServiceTest extends AbstractGhidraHeadedDebugge public void testWriteTargetMemoryNotAliveErr() throws Throwable { createAndOpenTrace(); activateTrace(); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_TARGET); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_TARGET); StateEditor editor = createStateEditor(); assertFalse(editor.isVariableEditable(tb.addr(0x00400000), 4)); @@ -507,7 +506,7 @@ public class DebuggerStateEditingServiceTest extends AbstractGhidraHeadedDebugge public void testWriteTargetRegisterNotAliveErr() throws Throwable { createAndOpenTrace(); activateTrace(); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_TARGET); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_TARGET); StateEditor editor = createStateEditor(); assertFalse(editor.isRegisterEditable(r0)); @@ -520,7 +519,7 @@ public class DebuggerStateEditingServiceTest extends AbstractGhidraHeadedDebugge public void testWriteReadOnlyMemoryErr() throws Throwable { createAndOpenTrace(); activateTrace(); - editingService.setCurrentMode(tb.trace, StateEditingMode.READ_ONLY); + editingService.setCurrentMode(tb.trace, StateEditingMode.RO_TARGET); StateEditor editor = createStateEditor(); assertFalse(editor.isVariableEditable(tb.addr(0x00400000), 4)); @@ -533,7 +532,7 @@ public class DebuggerStateEditingServiceTest extends AbstractGhidraHeadedDebugge public void testWriteReadOnlyRegisterErr() throws Throwable { createAndOpenTrace(); activateTrace(); - editingService.setCurrentMode(tb.trace, StateEditingMode.READ_ONLY); + editingService.setCurrentMode(tb.trace, StateEditingMode.RO_TARGET); StateEditor editor = createStateEditor(); assertFalse(editor.isRegisterEditable(r0)); diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/emulation/DebuggerEmulationServiceTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/emulation/DebuggerEmulationServiceTest.java index ff687bd181..98ae36cdcd 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/emulation/DebuggerEmulationServiceTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/emulation/DebuggerEmulationServiceTest.java @@ -47,6 +47,7 @@ import ghidra.program.model.mem.Memory; import ghidra.program.model.mem.MemoryBlock; import ghidra.program.util.ProgramLocation; import ghidra.trace.model.*; +import ghidra.trace.model.breakpoint.TraceBreakpoint; import ghidra.trace.model.breakpoint.TraceBreakpointKind; import ghidra.trace.model.guest.TracePlatform; import ghidra.trace.model.memory.TraceMemoryManager; @@ -402,6 +403,132 @@ public class DebuggerEmulationServiceTest extends AbstractGhidraHeadedDebuggerGU regs.getViewValue(scratch, regR2).getUnsignedValue()); } + @Test + public void testRunAfterExecutionBreakpoint() throws Exception { + createProgram(); + intoProject(program); + Assembler asm = Assemblers.getAssembler(program); + Memory memory = program.getMemory(); + Address addrText = addr(program, 0x000400000); + Address addrI1; + Address addrI2; + try (UndoableTransaction tid = UndoableTransaction.start(program, "Initialize")) { + MemoryBlock blockText = memory.createInitializedBlock(".text", addrText, 0x1000, + (byte) 0, TaskMonitor.DUMMY, false); + blockText.setExecute(true); + InstructionIterator ii = asm.assemble(addrText, + "mov r0, r0", + "mov r0, r1", + "mov r2, r0"); + ii.next(); // addrText + addrI1 = ii.next().getMinAddress(); + addrI2 = ii.next().getMinAddress(); + } + + programManager.openProgram(program); + waitForSwing(); + codeBrowser.goTo(new ProgramLocation(program, addrText)); + waitForSwing(); + + performEnabledAction(codeBrowser.getProvider(), emulationPlugin.actionEmulateProgram, true); + + Trace trace = traceManager.getCurrentTrace(); + assertNotNull(trace); + + TraceThread thread = Unique.assertOne(trace.getThreadManager().getAllThreads()); + + try (UndoableTransaction tid = UndoableTransaction.start(trace, "Add breakpoint")) { + trace.getBreakpointManager() + .addBreakpoint("Breakpoints[0]", Lifespan.nowOn(0), addrText, Set.of(thread), + Set.of(TraceBreakpointKind.SW_EXECUTE), true, "test"); + trace.getBreakpointManager() + .addBreakpoint("Breakpoints[1]", Lifespan.nowOn(0), addrI1, Set.of(thread), + Set.of(TraceBreakpointKind.SW_EXECUTE), true, "test"); + trace.getBreakpointManager() + .addBreakpoint("Breakpoints[2]", Lifespan.nowOn(0), addrI2, Set.of(thread), + Set.of(TraceBreakpointKind.SW_EXECUTE), true, "test"); + } + + // This is already testing if the one set at the entry is ignored + EmulationResult result1 = emulationPlugin.run(trace.getPlatformManager().getHostPlatform(), + TraceSchedule.snap(0), monitor, Scheduler.oneThread(thread)); + assertEquals(TraceSchedule.parse("0:t0-1"), result1.schedule()); + assertTrue(result1.error() instanceof InterruptPcodeExecutionException); + + // This will test if the one just hit gets ignored + EmulationResult result2 = emulationPlugin.run(trace.getPlatformManager().getHostPlatform(), + result1.schedule(), monitor, Scheduler.oneThread(thread)); + assertEquals(TraceSchedule.parse("0:t0-2"), result2.schedule()); + assertTrue(result1.error() instanceof InterruptPcodeExecutionException); + } + + @Test + public void testExecutionInjection() throws Exception { + createProgram(); + intoProject(program); + Assembler asm = Assemblers.getAssembler(program); + Memory memory = program.getMemory(); + Address addrText = addr(program, 0x000400000); + Register regPC = program.getRegister("pc"); + Register regR0 = program.getRegister("r0"); + Register regR1 = program.getRegister("r1"); + Register regR2 = program.getRegister("r2"); + Address addrI2; + try (UndoableTransaction tid = UndoableTransaction.start(program, "Initialize")) { + MemoryBlock blockText = memory.createInitializedBlock(".text", addrText, 0x1000, + (byte) 0, TaskMonitor.DUMMY, false); + blockText.setExecute(true); + InstructionIterator ii = asm.assemble(addrText, + "mov r0, r1", + "mov r2, r0"); + ii.next(); + addrI2 = ii.next().getMinAddress(); + program.getProgramContext() + .setValue(regR1, addrText, addrText, new BigInteger("1234", 16)); + } + + programManager.openProgram(program); + waitForSwing(); + codeBrowser.goTo(new ProgramLocation(program, addrText)); + waitForSwing(); + + performEnabledAction(codeBrowser.getProvider(), emulationPlugin.actionEmulateProgram, true); + + Trace trace = traceManager.getCurrentTrace(); + assertNotNull(trace); + + TraceThread thread = Unique.assertOne(trace.getThreadManager().getAllThreads()); + TraceMemorySpace regs = trace.getMemoryManager().getMemoryRegisterSpace(thread, false); + + try (UndoableTransaction tid = UndoableTransaction.start(trace, "Add breakpoint")) { + TraceBreakpoint tb = trace.getBreakpointManager() + .addBreakpoint("Breakpoints[0]", Lifespan.nowOn(0), addrI2, Set.of(thread), + Set.of(TraceBreakpointKind.SW_EXECUTE), true, "test"); + tb.setEmuSleigh(""" + r1 = 0x5678; + emu_swi(); + emu_exec_decoded(); + """); + } + + EmulationResult result = emulationPlugin.run(trace.getPlatformManager().getHostPlatform(), + TraceSchedule.snap(0), TaskMonitor.DUMMY, Scheduler.oneThread(thread)); + + assertEquals(TraceSchedule.parse("0:t0-1.t0-2"), result.schedule()); + assertTrue(result.error() instanceof InterruptPcodeExecutionException); + + long scratch = result.snapshot(); + + assertEquals(new BigInteger("00400002", 16), + regs.getViewValue(scratch, regPC).getUnsignedValue()); + assertEquals(new BigInteger("1234", 16), + regs.getViewValue(scratch, regR0).getUnsignedValue()); + assertEquals(new BigInteger("5678", 16), + regs.getViewValue(scratch, regR1).getUnsignedValue()); + assertEquals(new BigInteger("0", 16), + regs.getViewValue(scratch, regR2).getUnsignedValue()); + } + @Test public void testAccessBreakpoint() throws Exception { createProgram(); diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServiceTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServiceTest.java index 8c8e8c2373..18502096f9 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServiceTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServiceTest.java @@ -19,14 +19,15 @@ import static org.junit.Assert.*; import java.util.*; +import org.junit.Before; import org.junit.Test; import org.junit.experimental.categories.Category; import generic.test.category.NightlyCategory; import ghidra.app.plugin.core.debug.DebuggerCoordinates; import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest; -import ghidra.app.services.ActionSource; -import ghidra.app.services.TraceRecorder; +import ghidra.app.plugin.core.debug.service.editing.DebuggerStateEditingServicePlugin; +import ghidra.app.services.*; import ghidra.dbg.model.TestTargetStack; import ghidra.dbg.model.TestTargetStackFrameHasRegisterBank; import ghidra.dbg.target.schema.SchemaContext; @@ -46,6 +47,14 @@ import ghidra.util.database.UndoableTransaction; @Category(NightlyCategory.class) // this may actually be an @PortSensitive test public class DebuggerTraceManagerServiceTest extends AbstractGhidraHeadedDebuggerGUITest { + protected DebuggerStateEditingService editingService; + + @Before + public void setUpTraceManagerTest() throws Exception { + addPlugin(tool, DebuggerStateEditingServicePlugin.class); + editingService = tool.getService(DebuggerStateEditingService.class); + } + @Test public void testGetOpenTraces() throws Exception { assertEquals(Set.of(), traceManager.getOpenTraces()); @@ -337,9 +346,7 @@ public class DebuggerTraceManagerServiceTest extends AbstractGhidraHeadedDebugge } @Test - public void testAutoActivatePresent() throws Throwable { - assertTrue(traceManager.isAutoActivatePresent()); - + public void testFollowPresent() throws Throwable { createTestModel(); mb.createTestProcessesAndThreads(); @@ -352,6 +359,7 @@ public class DebuggerTraceManagerServiceTest extends AbstractGhidraHeadedDebugge traceManager.activateTrace(trace); waitForSwing(); + assertEquals(StateEditingMode.RO_TARGET, editingService.getCurrentMode(trace)); long initSnap = recorder.getSnap(); assertEquals(initSnap, traceManager.getCurrentSnap()); @@ -361,7 +369,7 @@ public class DebuggerTraceManagerServiceTest extends AbstractGhidraHeadedDebugge assertEquals(initSnap + 1, recorder.getSnap()); assertEquals(initSnap + 1, traceManager.getCurrentSnap()); - traceManager.setAutoActivatePresent(false); + editingService.setCurrentMode(trace, StateEditingMode.RO_TRACE); recorder.forceSnapshot(); waitForSwing(); @@ -369,7 +377,11 @@ public class DebuggerTraceManagerServiceTest extends AbstractGhidraHeadedDebugge assertEquals(initSnap + 2, recorder.getSnap()); assertEquals(initSnap + 1, traceManager.getCurrentSnap()); - traceManager.setAutoActivatePresent(true); + editingService.setCurrentMode(trace, StateEditingMode.RO_TARGET); + waitForSwing(); + + assertEquals(initSnap + 2, recorder.getSnap()); + assertEquals(initSnap + 2, traceManager.getCurrentSnap()); recorder.forceSnapshot(); waitForSwing(); diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/stack/StackUnwinderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/stack/StackUnwinderTest.java index c8ba1092d7..1591c56a44 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/stack/StackUnwinderTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/stack/StackUnwinderTest.java @@ -49,7 +49,6 @@ import ghidra.app.plugin.core.decompile.DecompilerProvider; import ghidra.app.plugin.core.disassembler.DisassemblerPlugin; import ghidra.app.services.*; import ghidra.app.services.DebuggerEmulationService.EmulationResult; -import ghidra.app.services.DebuggerStateEditingService.StateEditingMode; import ghidra.app.services.DebuggerStateEditingService.StateEditor; import ghidra.app.util.viewer.field.FieldFactory; import ghidra.app.util.viewer.field.ListingField; @@ -692,7 +691,7 @@ public class StackUnwinderTest extends AbstractGhidraHeadedDebuggerGUITest { traceManager.activateThread(thread); waitForSwing(); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_TRACE); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_TRACE); StateEditor editor = editingService.createStateEditor(tb.trace); DebuggerCoordinates atSetup = traceManager.getCurrent(); @@ -752,7 +751,7 @@ public class StackUnwinderTest extends AbstractGhidraHeadedDebuggerGUITest { traceManager.activateThread(thread); waitForSwing(); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_TRACE); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_TRACE); StateEditor editor = editingService.createStateEditor(tb.trace); DebuggerCoordinates atSetup = traceManager.getCurrent(); @@ -895,7 +894,7 @@ public class StackUnwinderTest extends AbstractGhidraHeadedDebuggerGUITest { traceManager.activateThread(thread); waitForSwing(); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_TRACE); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_TRACE); StateEditor editor = editingService.createStateEditor(tb.trace); DebuggerCoordinates atSetup = traceManager.getCurrent(); @@ -945,7 +944,7 @@ public class StackUnwinderTest extends AbstractGhidraHeadedDebuggerGUITest { traceManager.activateThread(thread); waitForSwing(); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_TRACE); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_TRACE); StateEditor editor = editingService.createStateEditor(tb.trace); // Move stack where it shows in UI. Not required, but nice for debugging. Register sp = program.getCompilerSpec().getStackPointer(); @@ -989,7 +988,7 @@ public class StackUnwinderTest extends AbstractGhidraHeadedDebuggerGUITest { traceManager.activateThread(thread); waitForSwing(); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_TRACE); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_TRACE); StateEditor editor = editingService.createStateEditor(tb.trace); // Move stack where it shows in UI. Not required, but nice for debugging. Register sp = program.getCompilerSpec().getStackPointer(); @@ -1033,7 +1032,7 @@ public class StackUnwinderTest extends AbstractGhidraHeadedDebuggerGUITest { traceManager.activateThread(thread); waitForSwing(); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_TRACE); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_TRACE); StateEditor editor = editingService.createStateEditor(tb.trace); // Move stack where it shows in UI. Not required, but nice for debugging. Register sp = program.getCompilerSpec().getStackPointer(); diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/debug/flatapi/FlatDebuggerAPITest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/debug/flatapi/FlatDebuggerAPITest.java index 1682fe0355..0360f3ba75 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/debug/flatapi/FlatDebuggerAPITest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/debug/flatapi/FlatDebuggerAPITest.java @@ -44,7 +44,6 @@ import ghidra.app.plugin.core.debug.service.model.launch.DebuggerProgramLaunchOf import ghidra.app.plugin.core.debug.service.model.launch.DebuggerProgramLaunchOffer.LaunchResult; import ghidra.app.script.GhidraState; import ghidra.app.services.*; -import ghidra.app.services.DebuggerStateEditingService.StateEditingMode; import ghidra.app.services.LogicalBreakpoint.State; import ghidra.dbg.DebuggerModelFactory; import ghidra.dbg.DebuggerObjectModel; @@ -690,7 +689,7 @@ public class FlatDebuggerAPITest extends AbstractGhidraHeadedDebuggerGUITest { @Test public void testWriteMemoryGivenContext() throws Throwable { createTraceWithBinText(); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_TRACE); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_TRACE); assertTrue(flat.writeMemory(tb.trace, 0, tb.addr(0x00400123), tb.arr(3, 2, 1))); ByteBuffer buf = ByteBuffer.allocate(3); @@ -701,7 +700,7 @@ public class FlatDebuggerAPITest extends AbstractGhidraHeadedDebuggerGUITest { @Test public void testWriteMemoryCurrentContext() throws Throwable { createTraceWithBinText(); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_TRACE); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_TRACE); assertTrue(flat.writeMemory(tb.addr(0x00400123), tb.arr(3, 2, 1))); ByteBuffer buf = ByteBuffer.allocate(3); @@ -712,7 +711,7 @@ public class FlatDebuggerAPITest extends AbstractGhidraHeadedDebuggerGUITest { @Test public void testWriteRegisterGivenContext() throws Throwable { TraceThread thread = createTraceWithThreadAndStack(true); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_TRACE); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_TRACE); traceManager.activateThread(thread); waitForSwing(); @@ -728,7 +727,7 @@ public class FlatDebuggerAPITest extends AbstractGhidraHeadedDebuggerGUITest { @Test public void testWriteRegisterCurrentContext() throws Throwable { TraceThread thread = createTraceWithThreadAndStack(true); - editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_TRACE); + editingService.setCurrentMode(tb.trace, StateEditingMode.RW_TRACE); traceManager.activateThread(thread); waitForSwing(); diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/TraceSleighUtils.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/TraceSleighUtils.java index f029a85f81..b53f5929d7 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/TraceSleighUtils.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/TraceSleighUtils.java @@ -59,7 +59,7 @@ public enum TraceSleighUtils { new DirectBytesTracePcodeExecutorState(platform, snap, thread, frame); Language language = platform.getLanguage(); if (!(language instanceof SleighLanguage)) { - throw new IllegalArgumentException("TracePlatform must use a SLEIGH language"); + throw new IllegalArgumentException("TracePlatform must use a Sleigh language"); } return new PcodeExecutor<>((SleighLanguage) language, BytesPcodeArithmetic.forLanguage(language), state, Reason.INSPECT); @@ -99,7 +99,7 @@ public enum TraceSleighUtils { PcodeExecutorState> paired = state.withMemoryState(); Language language = platform.getLanguage(); if (!(language instanceof SleighLanguage)) { - throw new IllegalArgumentException("TracePlatform must use a SLEIGH language"); + throw new IllegalArgumentException("TracePlatform must use a Sleigh language"); } return new PcodeExecutor<>((SleighLanguage) language, new PairedPcodeArithmetic<>( BytesPcodeArithmetic.forLanguage(language), TraceMemoryStatePcodeArithmetic.INSTANCE), diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/breakpoint/DBTraceBreakpoint.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/breakpoint/DBTraceBreakpoint.java index 1415d917ef..0bdd7787a3 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/breakpoint/DBTraceBreakpoint.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/breakpoint/DBTraceBreakpoint.java @@ -19,6 +19,7 @@ import java.io.IOException; import java.util.*; import db.DBRecord; +import ghidra.pcode.exec.SleighUtils; import ghidra.program.model.address.*; import ghidra.trace.database.DBTrace; import ghidra.trace.database.DBTraceUtils; @@ -38,19 +39,21 @@ import ghidra.util.database.DBObjectColumn; import ghidra.util.database.annot.*; import ghidra.util.exception.DuplicateNameException; -@DBAnnotatedObjectInfo(version = 0) +@DBAnnotatedObjectInfo(version = 1) public class DBTraceBreakpoint extends AbstractDBTraceAddressSnapRangePropertyMapData implements TraceBreakpoint { protected static final String TABLE_NAME = "Breakpoints"; private static final byte ENABLED_MASK = (byte) (1 << 7); + private static final byte EMU_ENABLED_MASK = (byte) (1 << 6); static final String PATH_COLUMN_NAME = "Path"; static final String NAME_COLUMN_NAME = "Name"; static final String THREADS_COLUMN_NAME = "Threads"; static final String FLAGS_COLUMN_NAME = "Flags"; static final String COMMENT_COLUMN_NAME = "Comment"; + static final String SLEIGH_COLUMN_NAME = "Sleigh"; @DBAnnotatedColumn(PATH_COLUMN_NAME) static DBObjectColumn PATH_COLUMN; @@ -62,6 +65,8 @@ public class DBTraceBreakpoint static DBObjectColumn FLAGS_COLUMN; @DBAnnotatedColumn(COMMENT_COLUMN_NAME) static DBObjectColumn COMMENT_COLUMN; + @DBAnnotatedColumn(SLEIGH_COLUMN_NAME) + static DBObjectColumn SLEIGH_COLUMN; protected static String tableName(AddressSpace space, long threadKey) { return DBTraceUtils.tableName(TABLE_NAME, space, threadKey, 0); @@ -77,10 +82,13 @@ public class DBTraceBreakpoint private byte flagsByte; @DBAnnotatedField(column = COMMENT_COLUMN_NAME) private String comment; + @DBAnnotatedField(column = SLEIGH_COLUMN_NAME) + private String emuSleigh; private final Set kinds = EnumSet.noneOf(TraceBreakpointKind.class); private final Set kindsView = Collections.unmodifiableSet(kinds); private boolean enabled; + private boolean emuEnabled; protected final DBTraceBreakpointSpace space; @@ -108,7 +116,7 @@ public class DBTraceBreakpoint } } enabled = (flagsByte & ENABLED_MASK) != 0; - // Msg.debug(this, "trace: breakpoint " + this + " enabled=" + enabled + ", because doFresh"); + emuEnabled = (flagsByte & EMU_ENABLED_MASK) != 0; } @Override @@ -127,7 +135,8 @@ public class DBTraceBreakpoint } public void set(String path, String name, Collection threads, - Collection kinds, boolean enabled, String comment) { + Collection kinds, boolean enabled, boolean emuEnabled, + String comment) { // TODO: Check that the threads exist and that each's lifespan covers the breakpoint's // TODO: This would require additional validation any time those are updated // TODO: For efficiency, would also require index of breakpoints by thread @@ -150,9 +159,13 @@ public class DBTraceBreakpoint if (enabled) { this.flagsByte |= ENABLED_MASK; } + if (emuEnabled) { + this.flagsByte |= EMU_ENABLED_MASK; + } this.comment = comment; update(PATH_COLUMN, NAME_COLUMN, THREADS_COLUMN, FLAGS_COLUMN, COMMENT_COLUMN); this.enabled = enabled; + this.emuEnabled = emuEnabled; // Msg.debug(this, "trace: breakpoint " + this + " enabled=" + enabled + ", because set"); } @@ -360,6 +373,17 @@ public class DBTraceBreakpoint update(FLAGS_COLUMN); } + protected void doSetEmuEnabled(boolean emuEnabled) { + this.emuEnabled = emuEnabled; + if (emuEnabled) { + flagsByte |= EMU_ENABLED_MASK; + } + else { + flagsByte &= ~EMU_ENABLED_MASK; + } + update(FLAGS_COLUMN); + } + protected void doSetKinds(Collection kinds) { for (TraceBreakpointKind k : TraceBreakpointKind.values()) { if (kinds.contains(k)) { @@ -391,6 +415,23 @@ public class DBTraceBreakpoint } } + @Override + public void setEmuEnabled(boolean enabled) { + try (LockHold hold = LockHold.lock(space.lock.writeLock())) { + doSetEmuEnabled(enabled); + } + space.trace.setChanged(new TraceChangeRecord<>(TraceBreakpointChangeType.CHANGED, + space, this)); + } + + @Override + public boolean isEmuEnabled(long snap) { + // NB. Only object mode support per-snap emu-enablement + try (LockHold hold = LockHold.lock(space.lock.readLock())) { + return emuEnabled; + } + } + @Override public void setKinds(Collection kinds) { try (LockHold hold = LockHold.lock(space.lock.writeLock())) { @@ -424,6 +465,29 @@ public class DBTraceBreakpoint } } + @Override + public void setEmuSleigh(String emuSleigh) { + try (LockHold hold = LockHold.lock(space.lock.writeLock())) { + if (emuSleigh == null || SleighUtils.UNCONDITIONAL_BREAK.equals(emuSleigh)) { + this.emuSleigh = null; + } + else { + this.emuSleigh = emuSleigh.trim(); + } + update(SLEIGH_COLUMN); + } + space.trace.setChanged(new TraceChangeRecord<>(TraceBreakpointChangeType.CHANGED, + space, this)); + } + + @Override + public String getEmuSleigh() { + try (LockHold hold = LockHold.lock(space.lock.readLock())) { + return emuSleigh == null || emuSleigh.isBlank() ? SleighUtils.UNCONDITIONAL_BREAK + : emuSleigh; + } + } + @Override public void delete() { space.deleteBreakpoint(this); diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/breakpoint/DBTraceBreakpointSpace.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/breakpoint/DBTraceBreakpointSpace.java index ce09ca2efb..fa1296c007 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/breakpoint/DBTraceBreakpointSpace.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/breakpoint/DBTraceBreakpointSpace.java @@ -100,7 +100,7 @@ public class DBTraceBreakpointSpace implements DBTraceSpaceBased { } DBTraceBreakpoint breakpoint = breakpointMapSpace.put(new ImmutableTraceAddressSnapRange(range, lifespan), null); - breakpoint.set(path, path, threads, kinds, enabled, comment); + breakpoint.set(path, path, threads, kinds, enabled, true, comment); trace.setChanged( new TraceChangeRecord<>(TraceBreakpointChangeType.ADDED, this, breakpoint)); return breakpoint; diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/breakpoint/DBTraceObjectBreakpointLocation.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/breakpoint/DBTraceObjectBreakpointLocation.java index b0a8b1a59c..22e46a0df4 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/breakpoint/DBTraceObjectBreakpointLocation.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/breakpoint/DBTraceObjectBreakpointLocation.java @@ -15,13 +15,13 @@ */ package ghidra.trace.database.breakpoint; -import java.util.Collection; -import java.util.Set; +import java.util.*; import java.util.stream.Collectors; import ghidra.dbg.target.*; import ghidra.dbg.target.schema.TargetObjectSchema; import ghidra.dbg.util.PathMatcher; +import ghidra.pcode.exec.SleighUtils; import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressRange; import ghidra.trace.database.target.*; @@ -208,6 +208,11 @@ public class DBTraceObjectBreakpointLocation object.setValue(Lifespan.span(snap, getClearedSnap()), TargetBreakpointSpec.ENABLED_ATTRIBUTE_NAME, enabled); } + Set asSet = + kinds instanceof Set yes ? yes : Set.copyOf(kinds); + if (!Objects.equals(asSet, getKinds())) { + this.setKinds(Lifespan.span(snap, getClearedSnap()), asSet); + } return this; } } @@ -297,8 +302,55 @@ public class DBTraceObjectBreakpointLocation @Override public String getComment() { - return TraceObjectInterfaceUtils.getValue(object, getPlacedSnap(), KEY_COMMENT, - String.class, ""); + try (LockHold hold = object.getTrace().lockRead()) { + return TraceObjectInterfaceUtils.getValue(object, getPlacedSnap(), KEY_COMMENT, + String.class, ""); + } + } + + @Override + public void setEmuEnabled(Lifespan lifespan, boolean emuEnabled) { + object.setValue(lifespan, KEY_EMU_ENABLED, emuEnabled ? null : false); + } + + @Override + public void setEmuEnabled(boolean emuEnabled) { + try (LockHold hold = object.getTrace().lockWrite()) { + setEmuEnabled(getLifespan(), emuEnabled); + } + } + + @Override + public boolean isEmuEnabled(long snap) { + try (LockHold hold = object.getTrace().lockRead()) { + return TraceObjectInterfaceUtils.getValue(object, getPlacedSnap(), KEY_EMU_ENABLED, + Boolean.class, true); + } + } + + @Override + public void setEmuSleigh(Lifespan lifespan, String sleigh) { + if (sleigh == null || SleighUtils.UNCONDITIONAL_BREAK.equals(sleigh)) { + object.setValue(lifespan, KEY_EMU_SLEIGH, null); + } + else { + object.setValue(lifespan, KEY_EMU_SLEIGH, sleigh.trim()); + } + } + + @Override + public void setEmuSleigh(String sleigh) { + try (LockHold hold = object.getTrace().lockWrite()) { + setEmuSleigh(getLifespan(), sleigh); + } + } + + @Override + public String getEmuSleigh() { + try (LockHold hold = object.getTrace().lockRead()) { + return TraceObjectInterfaceUtils.getValue(object, getPlacedSnap(), KEY_EMU_SLEIGH, + String.class, SleighUtils.UNCONDITIONAL_BREAK); + } } @Override diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/breakpoint/DBTraceObjectBreakpointSpec.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/breakpoint/DBTraceObjectBreakpointSpec.java index 47248d4647..4adde4af9a 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/breakpoint/DBTraceObjectBreakpointSpec.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/breakpoint/DBTraceObjectBreakpointSpec.java @@ -131,7 +131,8 @@ public class DBTraceObjectBreakpointSpec @Override public void setEnabled(boolean enabled) { try (LockHold hold = object.getTrace().lockWrite()) { - object.setValue(getLifespan(), TargetBreakpointSpec.ENABLED_ATTRIBUTE_NAME, enabled); + object.setValue(getLifespan(), TargetBreakpointSpec.ENABLED_ATTRIBUTE_NAME, + enabled ? true : null); } } @@ -190,6 +191,26 @@ public class DBTraceObjectBreakpointSpec throw new UnsupportedOperationException("Ask a location instead"); } + @Override + public void setEmuEnabled(boolean enabled) { + throw new UnsupportedOperationException("Set on a location instead"); + } + + @Override + public boolean isEmuEnabled(long snap) { + throw new UnsupportedOperationException("Ask a location instead"); + } + + @Override + public void setEmuSleigh(String sleigh) { + throw new UnsupportedOperationException("Set on a location instead"); + } + + @Override + public String getEmuSleigh() { + throw new UnsupportedOperationException("Ask a location instead"); + } + @Override public void delete() { try (LockHold hold = object.getTrace().lockWrite()) { diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/Trace.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/Trace.java index 8b1dd3847e..e79a37e6d0 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/Trace.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/Trace.java @@ -53,6 +53,16 @@ import resources.ResourceManager; public interface Trace extends DataTypeManagerDomainObject { ImageIcon TRACE_ICON = ResourceManager.loadImage("images/video-x-generic16.png"); + /** + * TEMPORARY: An a/b switch while both table- (legacy) and object-mode traces are supported + * + * @param trace the trace, or null + * @return true if the trace is non-null and has no root schema + */ + public static boolean isLegacy(Trace trace) { + return trace != null && trace.getObjectManager().getRootSchema() == null; + } + public static final class TraceObjectChangeType extends DefaultTraceChangeType { /** diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/breakpoint/TraceBreakpoint.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/breakpoint/TraceBreakpoint.java index 370d7b57b2..320cf37b48 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/breakpoint/TraceBreakpoint.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/breakpoint/TraceBreakpoint.java @@ -28,7 +28,6 @@ import ghidra.util.exception.DuplicateNameException; * A breakpoint in a trace */ public interface TraceBreakpoint extends TraceUniqueObject { - /** * Get the trace containing this breakpoint * @@ -161,10 +160,30 @@ public interface TraceBreakpoint extends TraceUniqueObject { /** * Check whether this breakpoint is enabled or disabled at the given snap * + * @param snap the snap * @return true if enabled, false if disabled */ boolean isEnabled(long snap); + /** + * Set whether this breakpoint is enabled or disabled for emulation + * + *

    + * This change applies to the entire lifespan of the record. It's not intended to record a + * history, but to toggle the breakpoint in the integrated emulator. + * + * @param enabled true if enabled, false if disabled + */ + void setEmuEnabled(boolean enabled); + + /** + * Check whether this breakpoint is enabled or disabled for emulation at the given snap + * + * @param snap the snap + * @return true if enabled, false if disabled + */ + boolean isEmuEnabled(long snap); + /** * Set the kinds included in this breakpoint * @@ -213,6 +232,36 @@ public interface TraceBreakpoint extends TraceUniqueObject { */ String getComment(); + /** + * Set Sleigh source to replace the breakpointed instruction in emulation + * + *

    + * The default is simply "{@link PcodeEmulationLibrary#emu_swi() emu_swi()}; + * {@link PcodeEmulationLibrary#emu_exec_decoded() emu_exec_decoded()};", effectively a + * non-conditional breakpoint followed by execution of the actual instruction. Modifying this + * allows clients to create conditional breakpoints or simply override or inject additional + * logic into an emulated target. + * + *

    + * NOTE: This current has no effect on access breakpoints, but only execution + * breakpoints. + * + *

    + * If the specified source fails to compile during emulator set-up, this will fall back to + * {@link PcodeEmulationLibrary#emu_err + * + * @see #DEFAULT_SLEIGH + * @param sleigh the Sleigh source + */ + void setEmuSleigh(String sleigh); + + /** + * Get the Sleigh source that replaces the breakpointed instruction in emulation + * + * @return the Sleigh source + */ + String getEmuSleigh(); + /** * Delete this breakpoint from the trace */ diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/breakpoint/TraceBreakpointKind.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/breakpoint/TraceBreakpointKind.java index a5a63c64d6..4d6d0543f4 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/breakpoint/TraceBreakpointKind.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/breakpoint/TraceBreakpointKind.java @@ -37,6 +37,8 @@ public enum TraceBreakpointKind { HW_EXECUTE(1 << 2), SW_EXECUTE(1 << 3); + public static final int COUNT = values().length; + public static class TraceBreakpointKindSet extends AbstractSetDecorator { public static final TraceBreakpointKindSet SW_EXECUTE = of(TraceBreakpointKind.SW_EXECUTE); public static final TraceBreakpointKindSet HW_EXECUTE = of(TraceBreakpointKind.HW_EXECUTE); diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/breakpoint/TraceObjectBreakpointLocation.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/breakpoint/TraceObjectBreakpointLocation.java index 9751cf53f6..b9f20ee67d 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/breakpoint/TraceObjectBreakpointLocation.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/breakpoint/TraceObjectBreakpointLocation.java @@ -32,9 +32,12 @@ import ghidra.util.exception.DuplicateNameException; TargetObject.DISPLAY_ATTRIBUTE_NAME, TargetBreakpointLocation.RANGE_ATTRIBUTE_NAME, TraceObjectBreakpointLocation.KEY_COMMENT, + TraceObjectBreakpointLocation.KEY_EMU_ENABLED, }) public interface TraceObjectBreakpointLocation extends TraceBreakpoint, TraceObjectInterface { String KEY_COMMENT = "_comment"; + String KEY_EMU_ENABLED = "_emu_enabled"; + String KEY_EMU_SLEIGH = "_emu_sleigh"; TraceObjectBreakpointSpec getSpecification(); @@ -48,5 +51,9 @@ public interface TraceObjectBreakpointLocation extends TraceBreakpoint, TraceObj void setEnabled(Lifespan lifespan, boolean enabled); + void setEmuEnabled(Lifespan lifespan, boolean emuEnabled); + + void setEmuSleigh(Lifespan lifespan, String sleigh); + void setComment(Lifespan lifespan, String comment); } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/Scheduler.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/Scheduler.java index b45abe10aa..ee3b55aa27 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/Scheduler.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/Scheduler.java @@ -132,14 +132,16 @@ public interface Scheduler { } catch (PcodeExecutionException e) { completedSteps = completedSteps.steppedForward(eventThread, completedTicks); - if (emuThread.getInstruction() == null) { + PcodeFrame frame = emuThread.getFrame(); + if (frame == null) { return new RecordRunResult(completedSteps, e); } - PcodeFrame frame = e.getFrame(); // Rewind one so stepping retries the op causing the error - int count = frame.count() - 1; - if (frame == null || count == 0) { - // If we've decoded, but could execute the first op, just drop the p-code steps + frame.stepBack(); + int count = frame.count(); + if (count == 0) { + // If we've decoded, but could not execute the first op, just drop the p-code steps + emuThread.dropInstruction(); return new RecordRunResult(completedSteps, e); } // The +1 accounts for the decode step diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/TraceSchedule.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/TraceSchedule.java index a436e195af..12b71d2fdd 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/TraceSchedule.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/TraceSchedule.java @@ -18,6 +18,7 @@ package ghidra.trace.model.time.schedule; import java.util.*; import ghidra.pcode.emu.PcodeMachine; +import ghidra.pcode.emu.PcodeMachine.SwiMode; import ghidra.program.model.lang.Language; import ghidra.trace.model.Trace; import ghidra.trace.model.thread.TraceThread; @@ -25,9 +26,18 @@ import ghidra.trace.model.time.TraceSnapshot; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; +/** + * A sequence of emulator stepping commands, essentially comprising a "point in time." + */ public class TraceSchedule implements Comparable { public static final TraceSchedule ZERO = TraceSchedule.snap(0); + /** + * Create a schedule that consists solely of a snapshot + * + * @param snap the snapshot key + * @return the schedule + */ public static final TraceSchedule snap(long snap) { return new TraceSchedule(snap, new Sequence(), new Sequence()); } @@ -337,11 +347,10 @@ public class TraceSchedule implements Comparable { */ public void execute(Trace trace, PcodeMachine machine, TaskMonitor monitor) throws CancelledException { + machine.setSoftwareInterruptMode(SwiMode.IGNORE_ALL); TraceThread lastThread = getEventThread(trace); - lastThread = - steps.execute(trace, lastThread, machine, Stepper.instruction(), monitor); - lastThread = - pSteps.execute(trace, lastThread, machine, Stepper.pcode(), monitor); + lastThread = steps.execute(trace, lastThread, machine, Stepper.instruction(), monitor); + lastThread = pSteps.execute(trace, lastThread, machine, Stepper.pcode(), monitor); } /** @@ -380,6 +389,7 @@ public class TraceSchedule implements Comparable { TaskMonitor monitor) throws CancelledException { TraceThread lastThread = position.getLastThread(trace); Sequence remains = steps.relativize(position.steps); + machine.setSoftwareInterruptMode(SwiMode.IGNORE_ALL); if (remains.isNop()) { Sequence pRemains = this.pSteps.relativize(position.pSteps); lastThread = @@ -388,8 +398,7 @@ public class TraceSchedule implements Comparable { else { lastThread = remains.execute(trace, lastThread, machine, Stepper.instruction(), monitor); - lastThread = - pSteps.execute(trace, lastThread, machine, Stepper.pcode(), monitor); + lastThread = pSteps.execute(trace, lastThread, machine, Stepper.pcode(), monitor); } } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/pcode/exec/trace/AbstractTracePcodeEmulatorTest.java b/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/pcode/exec/trace/AbstractTracePcodeEmulatorTest.java index 5b91d5469d..a3aaf25d98 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/pcode/exec/trace/AbstractTracePcodeEmulatorTest.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/pcode/exec/trace/AbstractTracePcodeEmulatorTest.java @@ -46,8 +46,8 @@ public class AbstractTracePcodeEmulatorTest extends AbstractGhidraHeadlessIntegr *

    * This creates a relatively bare-bones trace with initial state for testing trace * emulation/interpolation. It adds ".text" and "stack" regions, creates a thread, assembles - * given instructions, and then executes the given SLEIGH source (in the context of the new - * thread) to finish initializing the trace. Note, though given first, the SLEIGH is executed + * given instructions, and then executes the given Sleigh source (in the context of the new + * thread) to finish initializing the trace. Note, though given first, the Sleigh is executed * after assembly. Thus, it can be used to modify the resulting machine code by modifying the * memory where it was assembled. * diff --git a/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/pcode/exec/trace/BytesTracePcodeEmulatorTest.java b/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/pcode/exec/trace/BytesTracePcodeEmulatorTest.java index d708d0f8e2..bced23293b 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/pcode/exec/trace/BytesTracePcodeEmulatorTest.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/pcode/exec/trace/BytesTracePcodeEmulatorTest.java @@ -312,7 +312,7 @@ public class BytesTracePcodeEmulatorTest extends AbstractTracePcodeEmulatorTest * This may not reflect the semantics of an actual processor in these situations, since they may * have instruction caching. Emulating such semantics is TODO, if at all. NB. This also tests * that PC-relative addressing works, since internally the emulator advances the counter after - * execution of each instruction. Addressing is computed by the SLEIGH instruction parser and + * execution of each instruction. Addressing is computed by the Sleigh instruction parser and * encoded as a constant deref in the p-code. */ @Test diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/RowWrappedEnumeratedColumnTableModel.java b/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/RowWrappedEnumeratedColumnTableModel.java index f9ca2c1a90..c335224f37 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/RowWrappedEnumeratedColumnTableModel.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/RowWrappedEnumeratedColumnTableModel.java @@ -17,6 +17,7 @@ package docking.widgets.table; import java.util.*; import java.util.function.Function; +import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -37,13 +38,15 @@ public class RowWrappedEnumeratedColumnTableModel & Enumerated extends DefaultEnumeratedColumnTableModel { private final Function keyFunc; private final Function wrapper; + private final Function getter; private final Map map = new HashMap<>(); public RowWrappedEnumeratedColumnTableModel(PluginTool tool, String name, Class colType, - Function keyFunc, Function wrapper) { + Function keyFunc, Function wrapper, Function getter) { super(tool, name, colType); this.keyFunc = keyFunc; this.wrapper = wrapper; + this.getter = getter; } protected synchronized R addRowFor(T t) { @@ -130,6 +133,11 @@ public class RowWrappedEnumeratedColumnTableModel & Enumerated return r; } + public synchronized void deleteItemsWith(Predicate predicate) { + List deleted = deleteWith(r -> predicate.test(getter.apply(r))); + map.values().removeAll(deleted); + } + public synchronized void deleteAllItems(Collection c) { deleteWith(getRows(c)::contains); map.keySet().removeAll(c.stream().map(keyFunc).collect(Collectors.toList())); diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/AbstractPcodeMachine.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/AbstractPcodeMachine.java index 286aaeed24..b01c9922c8 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/AbstractPcodeMachine.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/AbstractPcodeMachine.java @@ -64,6 +64,8 @@ public abstract class AbstractPcodeMachine implements PcodeMachine { protected final PcodeUseropLibrary stubLibrary; + protected SwiMode swiMode = SwiMode.ACTIVE; + /* for abstract thread access */ PcodeStateInitializer initializer; private PcodeExecutorState sharedState; protected final Map> threads = new LinkedHashMap<>(); @@ -145,12 +147,12 @@ public abstract class AbstractPcodeMachine implements PcodeMachine { protected abstract PcodeExecutorState createLocalState(PcodeThread thread); /** - * A factory method to create a stub library for compiling thread-local SLEIGH source + * A factory method to create a stub library for compiling thread-local Sleigh source * *

    * Because threads may introduce p-code userops using libraries unique to that thread, it * becomes necessary to at least export stub symbols, so that p-code programs can be compiled - * from SLEIGH source before the thread has necessarily been created. A side effect of this + * from Sleigh source before the thread has necessarily been created. A side effect of this * strategy is that all threads, though they may have independent libraries, must export * identically-named symbols. * @@ -160,6 +162,16 @@ public abstract class AbstractPcodeMachine implements PcodeMachine { return new DefaultPcodeThread.PcodeEmulationLibrary(null); } + @Override + public void setSoftwareInterruptMode(SwiMode mode) { + this.swiMode = mode; + } + + @Override + public SwiMode getSoftwareInterruptMode() { + return swiMode; + } + /** * A factory method to create a new thread in this machine * @@ -319,7 +331,17 @@ public abstract class AbstractPcodeMachine implements PcodeMachine { accessBreakpoints.clear(); } - protected void checkLoad(AddressSpace space, T offset) { + /** + * Perform checks on a requested LOAD + * + *

    + * Throw an exception if the LOAD should cause an interrupt. + * + * @param space the address space being accessed + * @param offset the offset being accessed + * @param size the size of the variable being accessed + */ + protected void checkLoad(AddressSpace space, T offset, int size) { if (accessBreakpoints.isEmpty()) { return; } @@ -334,7 +356,17 @@ public abstract class AbstractPcodeMachine implements PcodeMachine { } } - protected void checkStore(AddressSpace space, T offset) { + /** + * Perform checks on a requested STORE + * + *

    + * Throw an exception if the STORE should cause an interrupt. + * + * @param space the address space being accessed + * @param offset the offset being accessed + * @param size the size of the variable being accessed + */ + protected void checkStore(AddressSpace space, T offset, int size) { if (accessBreakpoints.isEmpty()) { return; } @@ -348,4 +380,23 @@ public abstract class AbstractPcodeMachine implements PcodeMachine { // Consider this not hitting any breakpoint } } + + /** + * Throw a software interrupt exception if those interrupts are active + */ + protected void swi() { + if (swiMode == SwiMode.ACTIVE) { + throw new InterruptPcodeExecutionException(null, null); + } + } + + /** + * Notify the machine a thread has been stepped, so that it may re-enable software interrupts, + * if applicable + */ + protected void stepped() { + if (swiMode == SwiMode.IGNORE_STEP) { + swiMode = SwiMode.ACTIVE; + } + } } diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/DefaultPcodeThread.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/DefaultPcodeThread.java index 25b0d275a9..9a0d908116 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/DefaultPcodeThread.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/DefaultPcodeThread.java @@ -121,7 +121,21 @@ public class DefaultPcodeThread implements PcodeThread { */ @PcodeUserop public void emu_swi() { - throw new InterruptPcodeExecutionException(null, null); + thread.swi(); + } + + /** + * Notify the client of a failed Sleigh inject compilation. + * + *

    + * To avoid pestering the client during emulator set-up, a service may effectively defer + * notifying the user of Sleigh compilation errors by replacing the erroneous injects with + * calls to this p-code op. Then, only if and when an erroneous inject is encountered will + * the client be notified. + */ + @PcodeUserop + public void emu_injection_err() { + throw new InjectionErrorPcodeExecutionException(null, null); } } @@ -162,16 +176,17 @@ public class DefaultPcodeThread implements PcodeThread { throw new SuspendedPcodeExecutionException(frame, null); } super.stepOp(op, frame, library); + thread.stepped(); } @Override - protected void checkLoad(AddressSpace space, T offset) { - thread.checkLoad(space, offset); + protected void checkLoad(AddressSpace space, T offset, int size) { + thread.checkLoad(space, offset, size); } @Override - protected void checkStore(AddressSpace space, T offset) { - thread.checkStore(space, offset); + protected void checkStore(AddressSpace space, T offset, int size) { + thread.checkStore(space, offset, size); } @Override @@ -623,11 +638,46 @@ public class DefaultPcodeThread implements PcodeThread { injects.clear(); } - protected void checkLoad(AddressSpace space, T offset) { - machine.checkLoad(space, offset); + /** + * Perform checks on a requested LOAD + * + *

    + * Throw an exception if the LOAD should cause an interrupt. + * + * @param space the address space being accessed + * @param offset the offset being accessed + * @param size the size of the variable being accessed + */ + protected void checkLoad(AddressSpace space, T offset, int size) { + machine.checkLoad(space, offset, size); } - protected void checkStore(AddressSpace space, T offset) { - machine.checkStore(space, offset); + /** + * Perform checks on a requested STORE + * + *

    + * Throw an exception if the STORE should cause an interrupt. + * + * @param space the address space being accessed + * @param offset the offset being accessed + * @param size the size of the variable being accessed + */ + protected void checkStore(AddressSpace space, T offset, int size) { + machine.checkStore(space, offset, size); + } + + /** + * Throw a software interrupt exception if those interrupts are active + */ + protected void swi() { + machine.swi(); + } + + /** + * Notify the machine a thread has been stepped, so that it may re-enable software interrupts, + * if applicable + */ + protected void stepped() { + machine.stepped(); } } diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/PcodeMachine.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/PcodeMachine.java index 1348ed1b4f..dc020c7b35 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/PcodeMachine.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/PcodeMachine.java @@ -20,9 +20,9 @@ import java.util.Collection; import ghidra.app.plugin.processors.sleigh.SleighLanguage; import ghidra.pcode.emu.DefaultPcodeThread.PcodeEmulationLibrary; import ghidra.pcode.exec.*; +import ghidra.pcode.exec.PcodeArithmetic.Purpose; import ghidra.pcode.exec.PcodeExecutorStatePiece.Reason; -import ghidra.program.model.address.Address; -import ghidra.program.model.address.AddressRange; +import ghidra.program.model.address.*; import ghidra.program.model.pcode.PcodeOp; import ghidra.program.model.pcode.Varnode; @@ -33,6 +33,28 @@ import ghidra.program.model.pcode.Varnode; */ public interface PcodeMachine { + /** + * Specifies whether or not to interrupt on p-code breakpoints + */ + enum SwiMode { + /** + * Heed {@link PcodeEmulationLibrary#emu_swi()} calls + */ + ACTIVE, + /** + * Ignore all {@link PcodeEmulationLibrary#emu_swi()} calls + */ + IGNORE_ALL, + /** + * Ignore {@link PcodeEmulationLibrary#emu_swi()} calls for one p-code step + * + *

    + * The mode is reset to {@link #ACTIVE} after one p-code step, whether or not that step + * causes an SWI. + */ + IGNORE_STEP, + } + /** * The kind of access breakpoint */ @@ -86,6 +108,25 @@ public interface PcodeMachine { */ PcodeArithmetic getArithmetic(); + /** + * Change the efficacy of p-code breakpoints + * + *

    + * This is used to prevent breakpoints from interrupting at inappropriate times, e.g., upon + * continuing from a breakpoint. + * + * @param mode the new mode + * @see #withSoftwareInterruptMode(SwiMode) + */ + void setSoftwareInterruptMode(SwiMode mode); + + /** + * Get the current software interrupt mode + * + * @return the mode + */ + SwiMode getSoftwareInterruptMode(); + /** * Get the userop library common to all threads in the machine. * diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/PcodeStateInitializer.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/PcodeStateInitializer.java index 8f71749332..44983120c4 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/PcodeStateInitializer.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/PcodeStateInitializer.java @@ -22,7 +22,7 @@ import ghidra.util.classfinder.ExtensionPoint; * An extension for preparing execution state for sleigh emulation * *

    - * As much as possible, it's highly-recommended to use SLEIGH execution to perform any + * As much as possible, it's highly-recommended to use Sleigh execution to perform any * modifications. This will help it remain agnostic to various state types. * *

    @@ -40,7 +40,7 @@ public interface PcodeStateInitializer extends ExtensionPoint { /** * The machine's memory state has just been initialized, and additional initialization is needed - * for SLEIGH execution + * for Sleigh execution * *

    * There's probably not much preparation of memory @@ -53,11 +53,11 @@ public interface PcodeStateInitializer extends ExtensionPoint { /** * The thread's register state has just been initialized, and additional initialization is - * needed for SLEIGH execution + * needed for Sleigh execution * *

    * Initialization generally consists of setting "virtual" registers using data from the real - * ones. Virtual registers are those specified in the SLEIGH, but which don't actually exist on + * ones. Virtual registers are those specified in the Sleigh, but which don't actually exist on * the target processor. Often, they exist to simplify static analysis, but unfortunately cause * a minor headache for dynamic execution. * diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/PcodeThread.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/PcodeThread.java index 5aaf8db45a..c858148988 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/PcodeThread.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/PcodeThread.java @@ -19,6 +19,7 @@ import java.util.List; import ghidra.app.plugin.processors.sleigh.SleighLanguage; import ghidra.pcode.emu.DefaultPcodeThread.PcodeEmulationLibrary; +import ghidra.pcode.emu.PcodeMachine.SwiMode; import ghidra.pcode.exec.*; import ghidra.program.model.address.Address; import ghidra.program.model.lang.Register; diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/SleighInstructionDecoder.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/SleighInstructionDecoder.java index 8c50ad1939..c613dffac2 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/SleighInstructionDecoder.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/SleighInstructionDecoder.java @@ -28,7 +28,7 @@ import ghidra.util.Msg; import ghidra.util.task.TaskMonitor; /** - * The default instruction decoder, based on SLEIGH + * The default instruction decoder, based on Sleigh * *

    * This simply uses a {@link Disassembler} on the machine's memory state. @@ -53,7 +53,7 @@ public class SleighInstructionDecoder implements InstructionDecoder { private PseudoInstruction instruction; /** - * Construct a SLEIGH instruction decoder + * Construct a Sleigh instruction decoder * * @see {@link DefaultPcodeThread#createInstructionDecoder(PcodeExecutorState)} * @param language the language to decoder diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/InjectionErrorPcodeExecutionException.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/InjectionErrorPcodeExecutionException.java new file mode 100644 index 0000000000..1b9897c866 --- /dev/null +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/InjectionErrorPcodeExecutionException.java @@ -0,0 +1,26 @@ +/* ### + * 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.pcode.exec; + +/** + * Exception thrown by {@link PcodeEmulationLibrary#emu_injection_err(), a p-code userop invoked + * when client-provided Sleigh code in an injection could not be compiled. + */ +public class InjectionErrorPcodeExecutionException extends PcodeExecutionException { + public InjectionErrorPcodeExecutionException(PcodeFrame frame, Throwable cause) { + super("Error compiling injected Sleigh source", frame, cause); + } +} diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeExecutor.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeExecutor.java index bb59ee9a30..6267adf898 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeExecutor.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeExecutor.java @@ -35,7 +35,7 @@ import ghidra.program.model.pcode.Varnode; * An executor of p-code programs * *

    - * This is the kernel of SLEIGH expression evaluation and p-code emulation. For a complete example + * This is the kernel of Sleigh expression evaluation and p-code emulation. For a complete example * of a p-code emulator, see {@link PcodeEmulator}. * * @param the type of values processed by the executor @@ -68,7 +68,7 @@ public class PcodeExecutor { } /** - * Get the executor's SLEIGH language (processor model) + * Get the executor's Sleigh language (processor model) * * @return the language */ @@ -334,8 +334,9 @@ public class PcodeExecutor { * * @param space the address space to be loaded from * @param offset the offset about to be loaded from + * @param size the size in bytes to be loaded */ - protected void checkLoad(AddressSpace space, T offset) { + protected void checkLoad(AddressSpace space, T offset, int size) { } /** @@ -348,8 +349,8 @@ public class PcodeExecutor { AddressSpace space = language.getAddressFactory().getAddressSpace(spaceID); Varnode inOffset = op.getInput(1); T offset = state.getVar(inOffset, reason); - checkLoad(space, offset); Varnode outVar = op.getOutput(); + checkLoad(space, offset, outVar.getSize()); T out = state.getVar(space, offset, outVar.getSize(), true, reason); T mod = arithmetic.modAfterLoad(outVar.getSize(), inOffset.getSize(), offset, @@ -362,8 +363,9 @@ public class PcodeExecutor { * * @param space the address space to be stored to * @param offset the offset about to be stored to + * @param size the size in bytes to be stored */ - protected void checkStore(AddressSpace space, T offset) { + protected void checkStore(AddressSpace space, T offset, int size) { } /** @@ -376,8 +378,8 @@ public class PcodeExecutor { AddressSpace space = language.getAddressFactory().getAddressSpace(spaceID); Varnode inOffset = op.getInput(1); T offset = state.getVar(inOffset, reason); - checkStore(space, offset); Varnode valVar = op.getInput(2); + checkStore(space, offset, valVar.getSize()); T val = state.getVar(valVar, reason); T mod = arithmetic.modBeforeStore(valVar.getSize(), inOffset.getSize(), offset, diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeExpression.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeExpression.java index 73f5f5af71..9d9a3e7971 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeExpression.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeExpression.java @@ -23,7 +23,7 @@ import ghidra.pcodeCPort.slghsymbol.UserOpSymbol; import ghidra.program.model.pcode.PcodeOp; /** - * A p-code program derived from, i.e., implementing, a SLEIGH expression + * A p-code program that evaluates a Sleigh expression */ public class PcodeExpression extends PcodeProgram { public static final String RESULT_NAME = "___result"; diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeFrame.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeFrame.java index fb130cd6a9..01b6ec9d73 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeFrame.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeFrame.java @@ -39,7 +39,7 @@ import ghidra.program.model.pcode.PcodeOp; *

    * A p-code emulator feeds p-code to an executor by decoding one instruction at a time. Thus, the * "current p-code program" comprises only those ops generated by a single instruction. Or else, it - * is a user-supplied p-code program, e.g., to evaluate a SLEIGH expression. The frame completes the + * is a user-supplied p-code program, e.g., to evaluate a Sleigh expression. The frame completes the * program by falling-through, i.e., stepping past the final op, or by branching externally, i.e., * to a different machine instruction. The emulator must then update its program counter accordingly * and proceed to the next instruction. @@ -222,7 +222,7 @@ public class PcodeFrame { * Get the name of the userop for the given number * * @param userop the userop number, as encoded in the first operand of {@link PcodeOp#CALLOTHER} - * @return the name of the userop, as expressed in the SLEIGH source + * @return the name of the userop, as expressed in the Sleigh source */ public String getUseropName(int userop) { return useropNames.get(userop); diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeUseropLibrary.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeUseropLibrary.java index 5b9a485d64..b9cba81972 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeUseropLibrary.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeUseropLibrary.java @@ -101,7 +101,7 @@ public interface PcodeUseropLibrary { * Get the name of the userop. * *

    - * This is the symbol assigned to the userop when compiling new SLEIGH code. It cannot + * This is the symbol assigned to the userop when compiling new Sleigh code. It cannot * conflict with existing userops (except those declared, but not defined, by the executor's * language) or other symbols of the executor's language. If this userop is to be used * generically across many languages, choose an unlikely name. Conventionally, these start diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/SleighPcodeUseropDefinition.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/SleighPcodeUseropDefinition.java index 9a0110575a..6236bb22a3 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/SleighPcodeUseropDefinition.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/SleighPcodeUseropDefinition.java @@ -22,7 +22,7 @@ import ghidra.pcode.exec.PcodeUseropLibrary.PcodeUseropDefinition; import ghidra.program.model.pcode.Varnode; /** - * A p-code userop defined using SLEIGH source + * A p-code userop defined using Sleigh source * * @param no type in particular, except to match any executor */ @@ -107,7 +107,7 @@ public class SleighPcodeUseropDefinition implements PcodeUseropDefinition *

    * NOTE: Compilation of the sleigh source is delayed until the first invocation, since the * compiler must know about the varnodes used as parameters. TODO: There may be some way to - * template it at the p-code level instead of the SLEIGH source level. + * template it at the p-code level instead of the Sleigh source level. * * @param no particular type, except to match the executor * @return the definition diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/SleighProgramCompiler.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/SleighProgramCompiler.java index d8d8986a3f..54e80fe237 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/SleighProgramCompiler.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/SleighProgramCompiler.java @@ -42,8 +42,8 @@ import ghidra.util.Msg; * resulting program. Many utility methods are declared public here because they, well, they have * utility. The main public methods of this class, however, all start with {@code compile}.... */ -public class SleighProgramCompiler { - +public enum SleighProgramCompiler { + ; private static final String EXPRESSION_SOURCE_NAME = "expression"; public static final String NIL_SYMBOL_NAME = "__nil"; diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/SleighUtils.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/SleighUtils.java new file mode 100644 index 0000000000..52332022fc --- /dev/null +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/SleighUtils.java @@ -0,0 +1,776 @@ +/* ### + * 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.pcode.exec; + +import java.io.*; +import java.util.*; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import org.antlr.runtime.*; +import org.antlr.runtime.tree.CommonTree; +import org.antlr.runtime.tree.Tree; + +import ghidra.sleigh.grammar.*; + +/** + * A collection of utilities for parsing and manipulating Sleigh semantic source + */ +public enum SleighUtils { + ; + + public static final String CONDITION_ALWAYS = "1:1"; + public static final String UNCONDITIONAL_BREAK = """ + emu_swi(); + emu_exec_decoded(); + """; + + /** + * A Sleigh parsing error + */ + public record SleighParseErrorEntry(String header, String message, int start, int stop) { + public String fullMessage() { + return header + " " + message; + } + } + + /** + * An exception carrying one or more Sleigh parsing errors + */ + public static class SleighParseError extends RuntimeException { + private final List errors; + + public SleighParseError(Collection errors) { + super(errors.stream().map(e -> e.fullMessage()).collect(Collectors.joining("\n"))); + this.errors = List.copyOf(errors); + } + + /** + * Get the actual errors + * + * @return the list of entries + */ + public List getErrors() { + return errors; + } + } + + /** + * A function representing a non-terminal in the Sleigh semantic grammar + * + * @param the return type + */ + public interface ParseFunction { + T apply(SleighParser parser) throws RecognitionException; + } + + /** + * Parse a non-terminal symbol from the Sleigh semantic grammar + * + *

    + * Because the ANTLR parsing function for the non-terminal symbol depends on the "follows" set + * to determine when it has finished, we can't just invoke the function in isolation without + * some hacking. If EOF is not in the non-terminal's follows set, then it won't recognize EOF as + * completing the non-terminal. Instead, we have to present some token that it will recognize. + * Furthermore, regardless of the follow token, we have to check that all of the given input was + * consumed by the parser. + * + * @param the type of result from parsing + * @param nt the function from the parser implementing the non-terminal symbol + * @param text the text to parse + * @param follow a token that would ordinarily follow the non-terminal symbol, or empty for EOF + * @return the parsed result + */ + public static T parseSleigh(ParseFunction nt, + String text, String follow) { + LineArrayListWriter writer = new LineArrayListWriter(); + ParsingEnvironment env = new ParsingEnvironment(writer); + + // inject pcode statement lines into writer (needed for error reporting) + BufferedReader r = new BufferedReader(new StringReader(text)); + String line; + try { + while ((line = r.readLine()) != null) { + writer.write(line); + writer.newLine(); + } + } + catch (IOException e) { + throw new AssertionError(e); + } + + String inputText = writer.toString().stripTrailing(); + CharStream input = new ANTLRStringStream(inputText + follow); + + env.getLocator().registerLocation(0, new Location("sleigh", 0)); + + SleighLexer lex = new SleighLexer(input); + lex.setEnv(env); + UnbufferedTokenStream tokens = new UnbufferedTokenStream(lex); + List errors = new ArrayList<>(); + SleighParser parser = new SleighParser(tokens) { + private void collectError(String[] tokenNames, RecognitionException e) { + String hdr = getErrorHeader(e); + String msg = getErrorMessage(e, tokenNames); + CommonToken ct = (CommonToken) e.token; + errors.add(new SleighParseErrorEntry(hdr, msg, ct.getStartIndex(), + ct.getStopIndex())); + } + + { + this.gSemanticParser = new SleighParser_SemanticParser(input, state, this) { + @Override + public void displayRecognitionError(String[] tokenNames, + RecognitionException e) { + collectError(tokenNames, e); + } + + @Override + public void emitErrorMessage(String msg) { + throw new AssertionError(); + } + }; + } + + @Override + public void displayRecognitionError(String[] tokenNames, RecognitionException e) { + collectError(tokenNames, e); + } + + @Override + public void emitErrorMessage(String msg) { + throw new AssertionError(); + } + }; + parser.setEnv(env); + parser.setLexer(lex); + lex.pushMode(SleighRecognizerConstants.SEMANTIC); + T t; + try { + t = nt.apply(parser); + } + catch (RecognitionException e) { + parser.reportError(e); + return null; + } + lex.popMode(); + + CommonToken lastTok = (CommonToken) tokens.elementAt(0); + if (follow.isEmpty()) { + if (!tokens.isEOF(lastTok)) { + parser.reportError(new UnwantedTokenException(Token.EOF, tokens)); + } + } + else { + if (inputText.length() != lastTok.getStartIndex()) { + parser.reportError(new UnwantedTokenException(Token.EOF, tokens)); + } + } + + if (!errors.isEmpty()) { + throw new SleighParseError(errors); + } + + return t; + } + + /** + * Parse a semantic block, that is a list of Sleigh semantic statements + * + * @param sleigh the source + * @return the parse tree + */ + public static Tree parseSleighSemantic(String sleigh) { + return parseSleigh(SleighParser::semantic, sleigh, "").getTree(); + } + + /** + * Parse a semantic expression + * + * @param expression the expression as a string + * @return the parse tree + */ + public static Tree parseSleighExpression(String expression) { + return parseSleigh(SleighParser::expr, expression, ";").getTree(); + } + + /** + * An exception indicating the parse tree did not match a pattern + */ + public static class MismatchException extends RuntimeException { + } + + private static String getIdentifier(Tree tree) { + if (tree.getType() != SleighParser.IDENTIFIER) { + throw new MismatchException(); + } + return tree.getText(); + } + + private static boolean isIdentifier(Tree tree, String id) { + return id.equals(getIdentifier(tree)); + } + + private static void matchIdentifier(Tree tree, String id) { + if (!isIdentifier(tree, id)) { + throw new MismatchException(); + } + } + + /** + * Get the children of a parse tree node + * + * @param tree the node + * @return the list of children + */ + public static List getChildren(Tree tree) { + final int count = tree.getChildCount(); + List children = Arrays.asList(new Tree[count]); + for (int i = 0; i < count; i++) { + children.set(i, tree.getChild(i)); + } + return children; + } + + /** + * Match the given tree to a given pattern + * + * @param tree the (sub-)tree to match, actually its root node + * @param type the expected type of the given node + * @param onChildren actions (usually sub-matching) to perform on the children + */ + public static void matchTree(Tree tree, int type, Consumer> onChildren) { + if (tree.getType() != type) { + throw new MismatchException(); + } + onChildren.accept(getChildren(tree)); + } + + /** + * Require (as part of pattern matching) that the given list of children has a particular size + * + * @param count the required size + * @param list the list of children + */ + public static void requireCount(int count, List list) { + if (count != list.size()) { + throw new MismatchException(); + } + } + + /** + * Match the given tree to a given pattern with per-child actions + * + * @param tree the (sub-)tree to match, actually its root node + * @param type the expected type of the given node + * @param onChild a list of actions (usually sub-matching) to perform on each corresponding + * child. The matcher will verify the number of children matches the number of + * actions. + */ + @SafeVarargs + public static void match(Tree tree, int type, Consumer... onChild) { + matchTree(tree, type, children -> { + requireCount(onChild.length, children); + for (int i = 0; i < onChild.length; i++) { + onChild[i].accept(children.get(i)); + } + }); + } + + /** + * Check if the given tree represents an unconditional breakpoint in the emulator + * + * @param tree the result of parsing a semantic block + * @return true if an unconditional breakpoint, false otherwise + */ + public static boolean isUnconditionalBreakpoint(Tree tree) { + try { + match(tree, SleighParser.OP_SEMANTIC, wantApplyEmuSwi -> { + match(wantApplyEmuSwi, SleighParser.OP_APPLY, wantId -> { + match(wantId, SleighParser.OP_IDENTIFIER, id -> { + matchIdentifier(id, "emu_swi"); + }); + }); + }, wantApplyEmuExecDecoded -> { + match(wantApplyEmuExecDecoded, SleighParser.OP_APPLY, wantId -> { + match(wantId, SleighParser.OP_IDENTIFIER, id -> { + matchIdentifier(id, "emu_exec_decoded"); + }); + }); + }); + return true; + } + catch (MismatchException e) { + return false; + } + } + + /** + * Check if the given tree represents a conditional breakpoint, and recover that condition + * + * @param tree the result of parsing a semantic block + * @return the condition if matched, null otherwise + */ + public static String recoverConditionFromBreakpoint(Tree tree) { + try { + var l = new Object() { + Tree cond; + String labelId; + }; + match(tree, SleighParser.OP_SEMANTIC, wantIf -> { + match(wantIf, SleighParser.OP_IF, cond -> { + l.cond = cond; + }, wantGotoLabel -> { + match(wantGotoLabel, SleighParser.OP_GOTO, wantJumpDest -> { + match(wantJumpDest, SleighParser.OP_JUMPDEST_LABEL, wantLabel -> { + match(wantLabel, SleighParser.OP_LABEL, wantId -> { + match(wantId, SleighParser.OP_IDENTIFIER, id -> { + l.labelId = getIdentifier(id); + }); + }); + }); + }); + }); + }, wantApplyEmuSwi -> { + match(wantApplyEmuSwi, SleighParser.OP_APPLY, wantId -> { + match(wantId, SleighParser.OP_IDENTIFIER, id -> { + matchIdentifier(id, "emu_swi"); + }); + }); + }, wantLabel -> { + match(wantLabel, SleighParser.OP_LABEL, wantId -> { + match(wantId, SleighParser.OP_IDENTIFIER, id -> { + matchIdentifier(id, l.labelId); + }); + }); + }, wantApplyEmuExecDecoded -> { + match(wantApplyEmuExecDecoded, SleighParser.OP_APPLY, wantId -> { + match(wantId, SleighParser.OP_IDENTIFIER, id -> { + matchIdentifier(id, "emu_exec_decoded"); + }); + }); + }); + return generateSleighExpression(notTree(l.cond)); + } + catch (MismatchException e) { + return null; + } + } + + /** + * Check if the given Sleigh semantic block implements a conditional breakpoint, and recover + * that condition + * + * @param sleigh the source for a Sleigh semantic block + * @return the condition if matched, null otherwise + */ + public static String recoverConditionFromBreakpoint(String sleigh) { + try { + Tree tree = parseSleighSemantic(sleigh); + if (isUnconditionalBreakpoint(tree)) { + return CONDITION_ALWAYS; + } + return recoverConditionFromBreakpoint(tree); + } + catch (SleighParseError e) { + return null; + } + } + + /** + * Synthesize a tree (node) + * + * @param type the type of the node + * @param text the "text" of the node + * @param children the children + * @return the new node + */ + public static Tree makeTree(int type, String text, List children) { + CommonTree tree = new CommonTree(new CommonToken(type, text)); + tree.addChildren(children); + return tree; + } + + private static void catChildrenWithSep(Tree tree, String sep, int chopFront, int chopBack, + StringBuilder sb) { + int count = tree.getChildCount() - chopFront - chopBack; + for (int i = 0; i < count; i++) { + if (i != 0) { + sb.append(sep); + } + generateSleighExpression(tree.getChild(i + chopFront), sb); + } + } + + private static void generateSleighExpression(Tree tree, StringBuilder sb) { + switch (tree.getType()) { + case SleighParser.BIN_INT: + case SleighParser.DEC_INT: + case SleighParser.HEX_INT: + case SleighParser.IDENTIFIER: + sb.append(tree.getText()); + break; + + case SleighParser.OP_BIN_CONSTANT: + case SleighParser.OP_DEC_CONSTANT: + case SleighParser.OP_HEX_CONSTANT: + case SleighParser.OP_IDENTIFIER: + generateSleighExpression(tree.getChild(0), sb); + break; + + case SleighParser.OP_NOT: + sb.append("!"); + generateSleighExpression(tree.getChild(0), sb); + break; + case SleighParser.OP_INVERT: + sb.append("~"); + generateSleighExpression(tree.getChild(0), sb); + break; + case SleighParser.OP_NEGATE: + sb.append("-"); + generateSleighExpression(tree.getChild(0), sb); + break; + case SleighParser.OP_FNEGATE: + sb.append("f-"); + generateSleighExpression(tree.getChild(0), sb); + break; + + case SleighParser.OP_ADD: + catChildrenWithSep(tree, " + ", 0, 0, sb); + break; + case SleighParser.OP_SUB: + catChildrenWithSep(tree, " - ", 0, 0, sb); + break; + case SleighParser.OP_MULT: + catChildrenWithSep(tree, " * ", 0, 0, sb); + break; + case SleighParser.OP_DIV: + catChildrenWithSep(tree, " / ", 0, 0, sb); + break; + case SleighParser.OP_REM: + catChildrenWithSep(tree, " % ", 0, 0, sb); + break; + + case SleighParser.OP_SDIV: + catChildrenWithSep(tree, " s/ ", 0, 0, sb); + break; + case SleighParser.OP_SREM: + catChildrenWithSep(tree, " s% ", 0, 0, sb); + break; + + case SleighParser.OP_FADD: + catChildrenWithSep(tree, " f+ ", 0, 0, sb); + break; + case SleighParser.OP_FSUB: + catChildrenWithSep(tree, " f- ", 0, 0, sb); + break; + case SleighParser.OP_FMULT: + catChildrenWithSep(tree, " f* ", 0, 0, sb); + break; + case SleighParser.OP_FDIV: + catChildrenWithSep(tree, " f/ ", 0, 0, sb); + break; + + case SleighParser.OP_LEFT: + catChildrenWithSep(tree, " << ", 0, 0, sb); + break; + case SleighParser.OP_RIGHT: + catChildrenWithSep(tree, " >> ", 0, 0, sb); + break; + case SleighParser.OP_SRIGHT: + catChildrenWithSep(tree, " s>> ", 0, 0, sb); + break; + + case SleighParser.OP_AND: + catChildrenWithSep(tree, " & ", 0, 0, sb); + break; + case SleighParser.OP_OR: + catChildrenWithSep(tree, " | ", 0, 0, sb); + break; + case SleighParser.OP_XOR: + catChildrenWithSep(tree, " ^ ", 0, 0, sb); + break; + case SleighParser.OP_BOOL_AND: + catChildrenWithSep(tree, " && ", 0, 0, sb); + break; + case SleighParser.OP_BOOL_OR: + catChildrenWithSep(tree, " || ", 0, 0, sb); + break; + case SleighParser.OP_BOOL_XOR: + catChildrenWithSep(tree, " ^^ ", 0, 0, sb); + break; + + case SleighParser.OP_EQUAL: + catChildrenWithSep(tree, " == ", 0, 0, sb); + break; + case SleighParser.OP_NOTEQUAL: + catChildrenWithSep(tree, " != ", 0, 0, sb); + break; + case SleighParser.OP_FEQUAL: + catChildrenWithSep(tree, " f== ", 0, 0, sb); + break; + case SleighParser.OP_FNOTEQUAL: + catChildrenWithSep(tree, " f!= ", 0, 0, sb); + break; + + case SleighParser.OP_LESS: + catChildrenWithSep(tree, " < ", 0, 0, sb); + break; + case SleighParser.OP_LESSEQUAL: + catChildrenWithSep(tree, " <= ", 0, 0, sb); + break; + case SleighParser.OP_GREATEQUAL: + catChildrenWithSep(tree, " >= ", 0, 0, sb); + break; + case SleighParser.OP_GREAT: + catChildrenWithSep(tree, " > ", 0, 0, sb); + break; + + case SleighParser.OP_SLESS: + catChildrenWithSep(tree, " s< ", 0, 0, sb); + break; + case SleighParser.OP_SLESSEQUAL: + catChildrenWithSep(tree, " s<= ", 0, 0, sb); + break; + case SleighParser.OP_SGREATEQUAL: + catChildrenWithSep(tree, " s>= ", 0, 0, sb); + break; + case SleighParser.OP_SGREAT: + catChildrenWithSep(tree, " s> ", 0, 0, sb); + break; + + case SleighParser.OP_FLESS: + catChildrenWithSep(tree, " f< ", 0, 0, sb); + break; + case SleighParser.OP_FLESSEQUAL: + catChildrenWithSep(tree, " f<= ", 0, 0, sb); + break; + case SleighParser.OP_FGREATEQUAL: + catChildrenWithSep(tree, " f>= ", 0, 0, sb); + break; + case SleighParser.OP_FGREAT: + catChildrenWithSep(tree, " f> ", 0, 0, sb); + break; + + case SleighParser.OP_DEREFERENCE: + if (tree.getChildCount() == 3) { + sb.append("*["); + generateSleighExpression(tree.getChild(0), sb); + sb.append("]:"); + generateSleighExpression(tree.getChild(1), sb); + sb.append(" "); + generateSleighExpression(tree.getChild(2), sb); + } + else if (tree.getChildCount() == 2) { + Tree child0 = tree.getChild(0); + switch (child0.getType()) { + case SleighParser.OP_IDENTIFIER: + sb.append("*["); + generateSleighExpression(child0, sb); + sb.append("] "); + generateSleighExpression(tree.getChild(1), sb); + break; + case SleighParser.OP_BIN_CONSTANT: + case SleighParser.OP_DEC_CONSTANT: + case SleighParser.OP_HEX_CONSTANT: + sb.append("*:"); + generateSleighExpression(child0, sb); + sb.append(" "); + generateSleighExpression(tree.getChild(1), sb); + break; + default: + throw new AssertionError( + "OP_DEREFERENCE with 2 children where child[0] is " + + SleighParser.tokenNames[child0.getType()]); + } + } + else if (tree.getChildCount() == 1) { + sb.append("*"); + generateSleighExpression(tree.getChild(0), sb); + } + else { + throw new AssertionError( + "OP_DEREFERENCE with " + tree.getChildCount() + " children"); + } + break; + case SleighParser.OP_ADDRESS_OF: + if (tree.getChildCount() == 2) { + sb.append("&"); + generateSleighExpression(tree.getChild(0), sb); + sb.append(" "); + generateSleighExpression(tree.getChild(1), sb); + } + else if (tree.getChildCount() == 1) { + sb.append("&"); + generateSleighExpression(tree.getChild(0), sb); + } + else { + throw new AssertionError( + "OP_ADDRESS_OF with " + tree.getChildCount() + " children"); + } + break; + case SleighParser.OP_SIZING_SIZE: + sb.append(":"); + generateSleighExpression(tree.getChild(0), sb); + break; + case SleighParser.OP_APPLY: + generateSleighExpression(tree.getChild(0), sb); + sb.append("("); + catChildrenWithSep(tree, ", ", 1, 0, sb); + sb.append(")"); + break; + case SleighParser.OP_TRUNCATION_SIZE: + generateSleighExpression(tree.getChild(0), sb); + sb.append(":"); + generateSleighExpression(tree.getChild(1), sb); + break; + case SleighParser.OP_BITRANGE: + generateSleighExpression(tree.getChild(0), sb); + sb.append("["); + generateSleighExpression(tree.getChild(1), sb); + sb.append(","); + generateSleighExpression(tree.getChild(2), sb); + sb.append("]"); + break; + case SleighParser.OP_BITRANGE2: + generateSleighExpression(tree.getChild(0), sb); + sb.append(":"); + generateSleighExpression(tree.getChild(1), sb); + break; + case SleighParser.OP_ARGUMENTS: + catChildrenWithSep(tree, ", ", 0, 0, sb); + break; + case SleighParser.OP_PARENTHESIZED: + sb.append("("); + generateSleighExpression(tree.getChild(0), sb); + sb.append(")"); + break; + default: + throw new AssertionError("type = " + SleighParser.tokenNames[tree.getType()]); + } + } + + /** + * Generate source for the given Sleigh parse tree + * + *

    + * Currently, only nodes that could appear in a Sleigh expression are supported. + * + * @param tree the expression tree + * @return the generated string + */ + public static String generateSleighExpression(Tree tree) { + StringBuilder sb = new StringBuilder(); + generateSleighExpression(tree, sb); + return sb.toString(); + } + + /** + * Remove parenthesis from the root of the given tree + * + *

    + * If the root is parenthesis, this simply gets the child. This is applied recursively until a + * non-parenthesis child is encountered. + * + * @param tree the result of parsing a Sleigh expression + * @return the same or sub-tree + */ + public static Tree removeParenthesisTree(Tree tree) { + if (tree.getType() == SleighParser.OP_PARENTHESIZED) { + return removeParenthesisTree(tree.getChild(0)); + } + return tree; + } + + /** + * Apply the boolean "not" operator to a Sleigh expression + * + *

    + * This will attempt to invert the expression when possible, e.g., by changing a top-level + * "equals" to "not equals." If that is not possible, the this adds parenthesis and applies the + * actual Sleigh boolean "not" operator. + * + * @param boolExpr the result of parsing a Sleigh expression + * @return the tree for the inverted expression + */ + public static Tree notTree(Tree boolExpr) { + boolExpr = removeParenthesisTree(boolExpr); + switch (boolExpr.getType()) { + case SleighParser.OP_EQUAL: + return makeTree(SleighParser.OP_NOTEQUAL, "!=", getChildren(boolExpr)); + case SleighParser.OP_NOTEQUAL: + return makeTree(SleighParser.OP_EQUAL, "==", getChildren(boolExpr)); + case SleighParser.OP_FEQUAL: + return makeTree(SleighParser.OP_FNOTEQUAL, "f!=", getChildren(boolExpr)); + case SleighParser.OP_FNOTEQUAL: + return makeTree(SleighParser.OP_FEQUAL, "f==", getChildren(boolExpr)); + + case SleighParser.OP_LESS: + return makeTree(SleighParser.OP_GREATEQUAL, ">=", getChildren(boolExpr)); + case SleighParser.OP_LESSEQUAL: + return makeTree(SleighParser.OP_GREAT, ">", getChildren(boolExpr)); + case SleighParser.OP_GREATEQUAL: + return makeTree(SleighParser.OP_LESS, "<", getChildren(boolExpr)); + case SleighParser.OP_GREAT: + return makeTree(SleighParser.OP_LESSEQUAL, "<=", getChildren(boolExpr)); + + case SleighParser.OP_SLESS: + return makeTree(SleighParser.OP_SGREATEQUAL, "s>=", getChildren(boolExpr)); + case SleighParser.OP_SLESSEQUAL: + return makeTree(SleighParser.OP_SGREAT, "s>", getChildren(boolExpr)); + case SleighParser.OP_SGREATEQUAL: + return makeTree(SleighParser.OP_SLESS, "s<", getChildren(boolExpr)); + case SleighParser.OP_SGREAT: + return makeTree(SleighParser.OP_SLESSEQUAL, "s<=", getChildren(boolExpr)); + + case SleighParser.OP_FLESS: + return makeTree(SleighParser.OP_FGREATEQUAL, "f>=", getChildren(boolExpr)); + case SleighParser.OP_FLESSEQUAL: + return makeTree(SleighParser.OP_FGREAT, "f>", getChildren(boolExpr)); + case SleighParser.OP_FGREATEQUAL: + return makeTree(SleighParser.OP_FLESS, "f<", getChildren(boolExpr)); + case SleighParser.OP_FGREAT: + return makeTree(SleighParser.OP_FLESSEQUAL, "f<=", getChildren(boolExpr)); + + case SleighParser.OP_NOT: + return removeParenthesisTree(boolExpr.getChild(0)); + default: + return makeTree(SleighParser.OP_NOT, "!", List.of( + makeTree(SleighParser.OP_PARENTHESIZED, "(...)", List.of( + boolExpr)))); + } + } + + /** + * Generate Sleigh source for a breakpoint predicated on the given condition + * + * @param condition a Sleigh expression + * @return the Sleigh source + */ + public static String sleighForConditionalBreak(String condition) { + if (CONDITION_ALWAYS.equals(condition)) { + return UNCONDITIONAL_BREAK; + } + Tree tree = parseSleighExpression(condition); + String negCond = generateSleighExpression(notTree(tree)); + return String.format(""" + if %s goto ; + emu_swi(); + + emu_exec_decoded(); + """, negCond); + } +} diff --git a/Ghidra/Debug/ProposedUtils/src/test/java/ghidra/pcode/exec/SleighUtilsTest.java b/Ghidra/Debug/ProposedUtils/src/test/java/ghidra/pcode/exec/SleighUtilsTest.java new file mode 100644 index 0000000000..38c90810ef --- /dev/null +++ b/Ghidra/Debug/ProposedUtils/src/test/java/ghidra/pcode/exec/SleighUtilsTest.java @@ -0,0 +1,300 @@ +/* ### + * 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.pcode.exec; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import org.antlr.runtime.RecognitionException; +import org.antlr.runtime.tree.Tree; +import org.junit.Test; + +import ghidra.pcode.exec.SleighUtils.SleighParseError; + +public class SleighUtilsTest { + @Test + public void testParseSleighSemantic() throws RecognitionException { + String mySleigh = """ + if !(RAX == 0) goto ; + emu_swi(); + + emu_exec_decoded(); + """; + Tree tree = SleighUtils.parseSleighSemantic(mySleigh); + assertEquals( + "(OP_SEMANTIC (if (! ((...) (== (IDENTIFIER RAX) (DEC_INT 0)))) (goto " + + "(OP_JUMPDEST_LABEL (< (IDENTIFIER L1))))) (OP_APPLY (IDENTIFIER emu_swi)) " + + "(< (IDENTIFIER L1)) (OP_APPLY (IDENTIFIER emu_exec_decoded)))", + tree.toStringTree()); + } + + @Test + public void testParseSleighSemanticErr() throws RecognitionException { + String mySleigh = """ + if (!(RAX == 0)) goto ; + emu_swi(); + + emu_exec_decoded + """; + try { + SleighUtils.parseSleighSemantic(mySleigh); + fail(); + } + catch (SleighParseError e) { + assertEquals(""" + sleigh line 4: no viable alternative on EOF (missing semi-colon after this?): + + emu_exec_decoded + ----------------^ + """, e.getMessage()); + } + } + + @Test + public void testRecoverConditionEqDec() { + assertEquals("RAX == 0", + SleighUtils.recoverConditionFromBreakpoint(""" + if !(RAX == 0) goto ; + emu_swi(); + + emu_exec_decoded(); + """)); + } + + @Test + public void testRecoverConditionNeqHex() { + assertEquals("RAX != 0x3", + SleighUtils.recoverConditionFromBreakpoint(""" + if RAX == 0x3 goto ; + emu_swi(); + + emu_exec_decoded(); + """)); + } + + @Test + public void testRecoverConditionUserop() { + assertEquals("userop(a, b, c)", + SleighUtils.recoverConditionFromBreakpoint(""" + if !(userop(a,b,c)) goto ; + emu_swi(); + + emu_exec_decoded(); + """)); + } + + @Test + public void testRecoverConditionAddition() { + assertEquals("RAX + RBX + RCX == 0", + SleighUtils.recoverConditionFromBreakpoint(""" + if RAX + RBX + RCX != 0 goto ; + emu_swi(); + + emu_exec_decoded(); + """)); + } + + @Test + public void testRecoverConditionDeref() { + assertEquals("*RAX == 0", + SleighUtils.recoverConditionFromBreakpoint(""" + if *RAX != 0 goto ; + emu_swi(); + + emu_exec_decoded(); + """)); + } + + @Test + public void testRecoverConditionSizedDeref() { + assertEquals("*:4 RAX == 0", + SleighUtils.recoverConditionFromBreakpoint(""" + if *:4 RAX != 0 goto ; + emu_swi(); + + emu_exec_decoded(); + """)); + } + + @Test + public void testRecoverConditionSpacedSizedDeref() { + assertEquals("*[ram]:4 RAX == 0", + SleighUtils.recoverConditionFromBreakpoint(""" + if *[ram]:4 RAX != 0 goto ; + emu_swi(); + + emu_exec_decoded(); + """)); + } + + @Test + public void testRecoverConditionSpacedDeref() { + assertEquals("*[ram] RAX == 0", + SleighUtils.recoverConditionFromBreakpoint(""" + if *[ram] RAX != 0 goto ; + emu_swi(); + + emu_exec_decoded(); + """)); + } + + @Test + public void testRecoverConditionSizedAddressOf() { + assertEquals("&:8 RAX == 0", + SleighUtils.recoverConditionFromBreakpoint(""" + if &:8 RAX != 0 goto ; + emu_swi(); + + emu_exec_decoded(); + """)); + } + + @Test + public void testRecoverConditionAddressOf() { + assertEquals("&RAX == 0", + SleighUtils.recoverConditionFromBreakpoint(""" + if &RAX != 0 goto ; + emu_swi(); + + emu_exec_decoded(); + """)); + } + + @Test + public void testRecoverConditionSizedConst() { + assertEquals("!(1:1)", + SleighUtils.recoverConditionFromBreakpoint(""" + if 1:1 goto ; + emu_swi(); + + emu_exec_decoded(); + """)); + } + + @Test + public void testRecoverConditionNotSizedConst() { + assertEquals("1:1", + SleighUtils.recoverConditionFromBreakpoint(""" + if !(1:1) goto ; + emu_swi(); + + emu_exec_decoded(); + """)); + } + + @Test + public void testRecoverConditionBitRange() { + assertEquals("RAX[0,1]", + SleighUtils.recoverConditionFromBreakpoint(""" + if !(RAX[0,1]) goto ; + emu_swi(); + + emu_exec_decoded(); + """)); + } + + @Test + public void testRecoverConditionBitRange2() { + assertEquals("RAX:4", + SleighUtils.recoverConditionFromBreakpoint(""" + if !(RAX:4) goto ; + emu_swi(); + + emu_exec_decoded(); + """)); + } + + @Test + public void testRecoverConditionAlways() { + assertEquals("1:1", + SleighUtils.recoverConditionFromBreakpoint(""" + emu_swi(); + emu_exec_decoded(); + """)); + } + + @Test + public void testParseSleighExpression() throws RecognitionException { + assertEquals("(|| (== (IDENTIFIER RAX) (DEC_INT 0)) (== (IDENTIFIER RBX) (DEC_INT 7)))", + SleighUtils.parseSleighExpression("RAX == 0 || RBX == 7").toStringTree()); + } + + @Test + public void testParseSleighExpressionErr() throws RecognitionException { + try { + SleighUtils.parseSleighExpression("RAX RBX RCX"); + fail(); + } + catch (SleighParseError e) { + assertEquals(""" + sleigh line 1: no viable alternative on IDENTIFIER: 'RBX': + + RAX RBX RCX + ----^ + """, e.getMessage()); + } + } + + @Test + public void testParseSleighExpressionTooMuch() throws RecognitionException { + try { + SleighUtils.parseSleighExpression("RAX == 0;"); + fail(); + } + catch (SleighParseError e) { + assertEquals(""" + sleigh line 1: extraneous input ';' expecting EOF: + + RAX == 0; + --------^ + """, e.getMessage()); + } + } + + @Test + public void testParseSleighExpressionTooLittle() throws RecognitionException { + try { + SleighUtils.parseSleighExpression("RAX =="); + fail(); + } + catch (SleighParseError e) { + assertEquals(""" + sleigh line 1: no viable alternative on SEMI: ';': + + RAX == + ------^ + """, e.getMessage()); + } + } + + @Test + public void testSleighForConditionalBreakpointAlways() throws RecognitionException { + assertEquals(""" + emu_swi(); + emu_exec_decoded(); + """, SleighUtils.sleighForConditionalBreak("1:1")); + } + + @Test + public void testSleighForConditionalBreakpoint() throws RecognitionException { + assertEquals(""" + if RAX != 0 goto ; + emu_swi(); + + emu_exec_decoded(); + """, SleighUtils.sleighForConditionalBreak("RAX == 0")); + } +}