diff --git a/Ghidra/Debug/Debugger/data/ExtensionPoint.manifest b/Ghidra/Debug/Debugger/data/ExtensionPoint.manifest index b5ed39bc86..f19255fbc1 100644 --- a/Ghidra/Debug/Debugger/data/ExtensionPoint.manifest +++ b/Ghidra/Debug/Debugger/data/ExtensionPoint.manifest @@ -7,4 +7,4 @@ DebuggerPlatformOpinion DebuggerProgramLaunchOpinion DebuggerRegisterColumnFactory DisassemblyInject -LocationTrackingSpec +LocationTrackingSpecFactory diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerListingPlugin/DebuggerListingPlugin.html b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerListingPlugin/DebuggerListingPlugin.html index 3b590a8135..b9633e8e18 100644 --- a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerListingPlugin/DebuggerListingPlugin.html +++ b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerListingPlugin/DebuggerListingPlugin.html @@ -110,8 +110,8 @@
  • Track Program Counter (by stack) - navigates this listing to the current program counter as recorded from the debugger's stack trace. This option is not recommended for regular use, - especially for emulation, since the emulator does not provide stack trace records. It is - recommended for debugger connection developers to verify the stack records are being property + especially for emulation, since the emulator does not provide stack trace records. Its use is + recommended for debugger connection developers to verify the stack records are being properly interpreted by the GUI.
  • Track Program Counter (by register) - navigates this listing to the current program @@ -119,10 +119,14 @@ Program Counter" option is recommended, since the stack may record the program counter in some cases where the registers do not. For example, when unwinding a stack frame, the debugger may report the frame's program counter without reporting the frame's registers. This - option may be desired if/when stack frames are recorded incorrectly.
  • + option may be desired if stack frames are recorded incorrectly. -
  • Track Stack Pointer - navigates this listing to the current stack pointer. Again, +
  • Track Stack Pointer - navigates this listing to the current stack pointer. Note that platforms may vary in how they define the stack pointer.
  • + +
  • Track address of watch - navigates this listing to the address of the expression's value. + This option is replicated for each watch in the Watches window.
  • Go To (G)

    diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerMemoryBytesPlugin/DebuggerMemoryBytesPlugin.html b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerMemoryBytesPlugin/DebuggerMemoryBytesPlugin.html index d71d5643d3..8db10e4bb6 100644 --- a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerMemoryBytesPlugin/DebuggerMemoryBytesPlugin.html +++ b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerMemoryBytesPlugin/DebuggerMemoryBytesPlugin.html @@ -73,38 +73,9 @@

    Track Location

    -

    This action is always available on all memory windows. It configures automatic navigation - for the window. When location tracking is enabled, the window is automatically navigated to an - address computed from the trace's or target's machine state. NOTE: This feature is - disabled when the edit toggle is on. The address is also highlighted in green. The computed - address is affected by the tool's current "coordinates," that is the selected thread, frame, - and point in time. The options are pluggable, but currently consist of:

    - - +

    This action is equivalent to the same action in the Dynamic + Listing window. NOTE: This feature is disabled when the edit toggle is on.

    Go To (G)

    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 ea344a108f..2df1201df0 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 @@ -863,6 +863,7 @@ public interface DebuggerResources { String NAME_PC_BY_STACK = "Track Program Counter (by Stack)"; String NAME_SP = "Track Stack Pointer"; String NAME_NONE = "Do Not Track"; + String NAME_PREFIX_WATCH = "Track address of watch: "; // TODO: Separate icons for Program Counter and Stack Pointer Icon ICON_PC = ICON_REGISTER_MARKER; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/BasicLocationTrackingSpecFactory.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/BasicLocationTrackingSpecFactory.java new file mode 100644 index 0000000000..0e5c991870 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/BasicLocationTrackingSpecFactory.java @@ -0,0 +1,50 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.debug.gui.action; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import ghidra.framework.plugintool.PluginTool; + +/** + * The factory for the basic location tracking specs: NONE, PC, SP + */ +public class BasicLocationTrackingSpecFactory implements LocationTrackingSpecFactory { + public static final List ALL = List.of( + NoneLocationTrackingSpec.INSTANCE, + PCLocationTrackingSpec.INSTANCE, + PCByRegisterLocationTrackingSpec.INSTANCE, + PCByStackLocationTrackingSpec.INSTANCE, + SPLocationTrackingSpec.INSTANCE); + + public static final Map BY_CONFIG_NAME = ALL.stream() + .collect(Collectors.toUnmodifiableMap( + LocationTrackingSpec::getConfigName, + Function.identity())); + + @Override + public List getSuggested(PluginTool tool) { + return ALL; + } + + @Override + public LocationTrackingSpec parseSpec(String name) { + return BY_CONFIG_NAME.get(name); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/DebuggerTrackLocationAction.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/DebuggerTrackLocationAction.java index 41c7cc2a92..62924c29c6 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/DebuggerTrackLocationAction.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/DebuggerTrackLocationAction.java @@ -21,14 +21,10 @@ import ghidra.framework.plugintool.Plugin; public interface DebuggerTrackLocationAction extends TrackLocationAction { - // TODO: Update the action when new specs enter the class path? static MultiStateActionBuilder builder(Plugin owner) { MultiStateActionBuilder builder = TrackLocationAction.builder(owner); builder.toolBarGroup(owner.getName()); builder.useCheckboxForIcons(true); - for (LocationTrackingSpec spec : LocationTrackingSpec.allSpecs().values()) { - builder.addState(spec.getMenuName(), spec.getMenuIcon(), spec); - } return builder; } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/DebuggerTrackLocationTrait.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/DebuggerTrackLocationTrait.java index 9cdf3a2412..7d3e1b7e6a 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/DebuggerTrackLocationTrait.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/DebuggerTrackLocationTrait.java @@ -18,8 +18,8 @@ package ghidra.app.plugin.core.debug.gui.action; import java.awt.Color; import java.lang.invoke.MethodHandles; import java.math.BigInteger; -import java.util.List; -import java.util.Objects; +import java.util.*; +import java.util.concurrent.CompletableFuture; import docking.ActionContext; import docking.ComponentProvider; @@ -36,12 +36,12 @@ import ghidra.app.plugin.core.debug.gui.colors.MultiSelectionBlendedLayoutBackgr import ghidra.app.plugin.core.debug.gui.listing.DebuggerTrackedRegisterListingBackgroundColorModel; import ghidra.app.util.viewer.listingpanel.ListingBackgroundColorModel; import ghidra.app.util.viewer.listingpanel.ListingPanel; +import ghidra.async.AsyncUtils; import ghidra.framework.options.AutoOptions; import ghidra.framework.options.SaveState; import ghidra.framework.options.annotation.AutoOptionConsumed; import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.annotation.AutoConfigStateField; -import ghidra.program.model.address.Address; import ghidra.program.util.ProgramLocation; import ghidra.trace.model.*; import ghidra.trace.model.Trace.TraceMemoryBytesChangeType; @@ -49,6 +49,7 @@ import ghidra.trace.model.Trace.TraceStackChangeType; import ghidra.trace.model.stack.TraceStack; import ghidra.trace.model.thread.TraceThread; import ghidra.trace.util.TraceAddressSpace; +import ghidra.util.Msg; public class DebuggerTrackLocationTrait { protected static final AutoConfigState.ClassHandler CONFIG_STATE_HANDLER = @@ -67,7 +68,7 @@ public class DebuggerTrackLocationTrait { // Should only happen during transitional times, if at all. return; } - if (!spec.affectedByRegisterChange(space, range, current)) { + if (!tracker.affectedByBytesChange(space, range, current)) { return; } doTrack(); @@ -78,7 +79,7 @@ public class DebuggerTrackLocationTrait { // Should only happen during transitional times, if at all. return; } - if (!spec.affectedByStackChange(stack, current)) { + if (!tracker.affectedByStackChange(stack, current)) { return; } doTrack(); @@ -134,11 +135,11 @@ public class DebuggerTrackLocationTrait { protected MultiStateDockingAction action; - private final LocationTrackingSpec defaultSpec = - LocationTrackingSpec.fromConfigName(PCLocationTrackingSpec.CONFIG_NAME); + private final LocationTrackingSpec defaultSpec = PCLocationTrackingSpec.INSTANCE; @AutoConfigStateField(codec = TrackingSpecConfigFieldCodec.class) protected LocationTrackingSpec spec = defaultSpec; + protected LocationTracker tracker = spec.getTracker(); protected final PluginTool tool; protected final Plugin plugin; @@ -209,10 +210,9 @@ public class DebuggerTrackLocationTrait { } public MultiStateDockingAction installAction() { - // TODO: Add "other" option, and present most-recent in menu, too - // TODO: "other" as in arbitrary expression? - // Only those applicable to the current thread's registers, though. + // TODO: Only those Sleigh expressions applicable to the current thread's registers? action = DebuggerTrackLocationAction.builder(plugin) + .stateGenerator(this::getStates) .onAction(this::clickedSpecButton) .onActionStateChanged(this::clickedSpecMenu) .buildAndInstallLocal(provider); @@ -220,6 +220,21 @@ public class DebuggerTrackLocationTrait { return action; } + public List> getStates() { + Map> states = new TreeMap<>(); + for (LocationTrackingSpec spec : LocationTrackingSpecFactory + .allSuggested(tool) + .values()) { + states.put(spec.getConfigName(), + new ActionState<>(spec.getMenuName(), spec.getMenuIcon(), spec)); + } + ActionState current = action.getCurrentState(); + if (current != null) { + states.put(current.getUserData().getConfigName(), current); + } + return List.copyOf(states.values()); + } + protected void clickedSpecButton(ActionContext ctx) { doTrack(); } @@ -232,12 +247,13 @@ public class DebuggerTrackLocationTrait { protected void doSetSpec(LocationTrackingSpec spec) { if (this.spec != spec) { this.spec = spec; + this.tracker = spec.getTracker(); specChanged(spec); } doTrack(); } - protected ProgramLocation computeTrackedLocation() { + protected CompletableFuture computeTrackedLocation() { // Change of register values (for current frame) // Change of stack pc (for current frame) // Change of current view (if not caused by goTo) @@ -248,16 +264,22 @@ public class DebuggerTrackLocationTrait { DebuggerCoordinates cur = current; TraceThread thread = cur.getThread(); if (thread == null || spec == null) { - return null; + return AsyncUtils.nil(); } // NB: view's snap may be forked for emulation - Address address = spec.computeTraceAddress(tool, cur); - return address == null ? null : new ProgramLocation(cur.getView(), address); + return tracker.computeTraceAddress(tool, cur).thenApply(address -> { + return address == null ? null : new ProgramLocation(cur.getView(), address); + }); } protected void doTrack() { - trackedLocation = computeTrackedLocation(); - locationTracked(); + computeTrackedLocation().thenAccept(loc -> { + trackedLocation = loc; + locationTracked(); + }).exceptionally(ex -> { + Msg.error(this, "Error while computing location: " + ex); + return null; + }); } protected void addNewListeners() { @@ -296,7 +318,7 @@ public class DebuggerTrackLocationTrait { public void readConfigState(SaveState saveState) { CONFIG_STATE_HANDLER.readConfigState(this, saveState); - + tracker = spec.getTracker(); action.setCurrentActionStateByUserData(spec); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/LocationTracker.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/LocationTracker.java new file mode 100644 index 0000000000..6fa2bb3acc --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/LocationTracker.java @@ -0,0 +1,78 @@ +/* ### + * 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.action; + +import java.util.concurrent.CompletableFuture; + +import ghidra.app.plugin.core.debug.DebuggerCoordinates; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.address.Address; +import ghidra.trace.model.TraceAddressSnapRange; +import ghidra.trace.model.guest.TracePlatform; +import ghidra.trace.model.stack.TraceStack; +import ghidra.trace.util.TraceAddressSpace; + +/** + * The actual tracking logic for a location tracking spec + * + *

    + * In simple cases, the spec can implement this interface and return itself in + * {@link LocationTrackingSpec#getTracker()}. If the tracker needs some state, then the spec should + * create a separate tracker. + */ +public interface LocationTracker { + + /** + * Compute the trace address to "goto" + * + *

    + * If the coordinates indicate emulation, i.e., the schedule is non-empty, the trace manager + * will already have performed the emulation and stored the results in a "scratch" snap. In + * general, the location should be computed using that snap, i.e., + * {@link DebuggerCoordinates#getViewSnap()} rather than {@link DebuggerCoordinates#getSnap()}. + * The address returned must be in the host platform's language, i.e., please use + * {@link TracePlatform#mapGuestToHost(Address)}. + * + * @param tool the tool containing the provider + * @param coordinates the trace, thread, snap, etc., of the tool + * @return the address to navigate to + */ + CompletableFuture

    computeTraceAddress(PluginTool tool, + DebuggerCoordinates coordinates); + + // TODO: Is there a way to generalize these so that other dependencies need not + // have their own bespoke methods? + + /** + * Check if the address should be recomputed given the indicated value change + * + * @param space the space (address space, thread, frame) where the change occurred + * @param range the range (time and space) where the change occurred + * @param coordinates the provider's current coordinates + * @return true if re-computation and "goto" is warranted + */ + boolean affectedByBytesChange(TraceAddressSpace space, + TraceAddressSnapRange range, DebuggerCoordinates coordinates); + + /** + * Check if the address should be recomputed given the indicated stack change + * + * @param stack the stack that changed (usually it's PC / return offset) + * @param coordinates the provider's current coordinates + * @return true if re-computation and "goto" is warranted + */ + boolean affectedByStackChange(TraceStack stack, DebuggerCoordinates coordinates); +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/LocationTrackingSpec.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/LocationTrackingSpec.java index c228b3f6ab..53ca90bd58 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/LocationTrackingSpec.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/LocationTrackingSpec.java @@ -15,26 +15,14 @@ */ package ghidra.app.plugin.core.debug.gui.action; -import java.util.Map; -import java.util.TreeMap; - import javax.swing.Icon; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; import ghidra.app.plugin.core.debug.DebuggerCoordinates; -import ghidra.app.plugin.core.debug.utils.MiscellaneousUtils; import ghidra.framework.options.SaveState; import ghidra.framework.plugintool.AutoConfigState.ConfigFieldCodec; -import ghidra.framework.plugintool.PluginTool; -import ghidra.program.model.address.Address; import ghidra.trace.model.TraceAddressSnapRange; -import ghidra.trace.model.guest.TracePlatform; import ghidra.trace.model.memory.TraceMemorySpace; -import ghidra.trace.model.stack.TraceStack; import ghidra.trace.util.TraceAddressSpace; -import ghidra.util.classfinder.ClassSearcher; -import ghidra.util.classfinder.ExtensionPoint; /** * A specification for automatic navigation of the dynamic listing @@ -45,32 +33,18 @@ import ghidra.util.classfinder.ExtensionPoint; * want 3 different common expressions readily available in the drop-down list. It might make sense * to generate a tracking specification from each Watch. */ -public interface LocationTrackingSpec extends ExtensionPoint { - enum Private { - INSTANCE; - - private final Map specsByName = new TreeMap<>(); - private final ChangeListener classListener = this::classesChanged; - - private Private() { - ClassSearcher.addChangeListener(classListener); - } - - private synchronized void classesChanged(ChangeEvent evt) { - MiscellaneousUtils.collectUniqueInstances(LocationTrackingSpec.class, specsByName, - LocationTrackingSpec::getConfigName); - } - } - - Private PRIVATE = Private.INSTANCE; +public interface LocationTrackingSpec { + /** + * Codec for saving/restoring the tracking specification + */ public static class TrackingSpecConfigFieldCodec implements ConfigFieldCodec { @Override public LocationTrackingSpec read(SaveState state, String name, LocationTrackingSpec current) { String specName = state.getString(name, null); - return fromConfigName(specName); + return LocationTrackingSpecFactory.fromConfigName(specName); } @Override @@ -112,29 +86,6 @@ public interface LocationTrackingSpec extends ExtensionPoint { return true; } - /** - * Get the specification for the given configuration name - * - * @param name the name - * @return the spec, or null - */ - static LocationTrackingSpec fromConfigName(String name) { - synchronized (PRIVATE) { - return PRIVATE.specsByName.get(name); - } - } - - /** - * Get a copy of all the known specifications - * - * @return the specifications by configuration name - */ - static Map allSpecs() { - synchronized (PRIVATE) { - return new TreeMap<>(PRIVATE.specsByName); - } - } - /** * Get the configuration name * @@ -171,42 +122,13 @@ public interface LocationTrackingSpec extends ExtensionPoint { String computeTitle(DebuggerCoordinates coordinates); /** - * Compute the trace address to "goto" + * Get (or create) the actual location tracking logic * *

    - * If the coordinates indicate emulation, i.e., the schedule is non-empty, the trace manager - * will already have performed the emulation and stored the results in a "scratch" snap. In - * general, the location should be computed using that snap, i.e., - * {@link DebuggerCoordinates#getViewSnap()} rather than {@link DebuggerCoordinates#getSnap()}. - * The address returned must be in the host platform's language, i.e., please use - * {@link TracePlatform#mapGuestToHost(Address)}. + * Having a separate object from the spec gives implementations the option of keeping state on a + * per-window basis. * - * @param tool the tool containing the provider - * @param coordinates the trace, thread, snap, etc., of the tool - * @return the address to navigate to + * @return the tracker */ - Address computeTraceAddress(PluginTool tool, DebuggerCoordinates coordinates); - - // TODO: Is there a way to generalize these so that other dependencies need not - // have their own bespoke methods? - - /** - * Check if the address should be recomputed given the indicated register value change - * - * @param space the space (address space, thread, frame) where the change occurred - * @param range the range (time and space) where the change occurred - * @param coordinates the provider's current coordinates - * @return true if re-computation and "goto" is warranted - */ - boolean affectedByRegisterChange(TraceAddressSpace space, - TraceAddressSnapRange range, DebuggerCoordinates coordinates); - - /** - * Check if the address should be recomputed given the indicated stack change - * - * @param stack the stack that changed (usually it's PC / return offset) - * @param coordinates the provider's current coordinates - * @return true if re-computation and "goto" is warranted - */ - boolean affectedByStackChange(TraceStack stack, DebuggerCoordinates coordinates); + LocationTracker getTracker(); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/LocationTrackingSpecFactory.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/LocationTrackingSpecFactory.java new file mode 100644 index 0000000000..2678d75f84 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/LocationTrackingSpecFactory.java @@ -0,0 +1,77 @@ +/* ### + * 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.action; + +import java.util.*; + +import ghidra.framework.plugintool.PluginTool; +import ghidra.util.classfinder.ClassSearcher; +import ghidra.util.classfinder.ExtensionPoint; + +/** + * A discoverable factory of tracking specifications + */ +public interface LocationTrackingSpecFactory extends ExtensionPoint { + + /** + * Get the specification for the given configuration name + * + * @param name the name + * @return the spec, or null + */ + static LocationTrackingSpec fromConfigName(String name) { + for (LocationTrackingSpecFactory factory : ClassSearcher + .getInstances(LocationTrackingSpecFactory.class)) { + LocationTrackingSpec spec = factory.parseSpec(name); + if (spec != null) { + return spec; + } + } + return null; + } + + /** + * Get a copy of all the known specifications + * + * @return the specifications by configuration name + */ + static Map allSuggested(PluginTool tool) { + Map all = new TreeMap<>(); + for (LocationTrackingSpecFactory factory : ClassSearcher + .getInstances(LocationTrackingSpecFactory.class)) { + for (LocationTrackingSpec spec : factory.getSuggested(tool)) { + all.put(spec.getConfigName(), spec); + } + } + return all; + } + + /** + * Get all the specifications currently suggested by this factory + * + * @param tool the plugin tool or context + * @return the list of suggested specifications + */ + List getSuggested(PluginTool tool); + + /** + * Attempt to parse the given configuration name as as specification + * + * @param name the configuration name, usually including a prefix unique to each factory + * @return the specification, or null if this factory cannot parse it + */ + LocationTrackingSpec parseSpec(String name); +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/NoneLocationTrackingSpec.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/NoneLocationTrackingSpec.java index e013e2a62c..c7392f0c00 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/NoneLocationTrackingSpec.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/NoneLocationTrackingSpec.java @@ -15,17 +15,22 @@ */ package ghidra.app.plugin.core.debug.gui.action; +import java.util.concurrent.CompletableFuture; + import javax.swing.Icon; import ghidra.app.plugin.core.debug.DebuggerCoordinates; import ghidra.app.plugin.core.debug.gui.DebuggerResources.TrackLocationAction; +import ghidra.async.AsyncUtils; import ghidra.framework.plugintool.PluginTool; import ghidra.program.model.address.Address; import ghidra.trace.model.TraceAddressSnapRange; import ghidra.trace.model.stack.TraceStack; import ghidra.trace.util.TraceAddressSpace; -public class NoneLocationTrackingSpec implements LocationTrackingSpec { +public enum NoneLocationTrackingSpec implements LocationTrackingSpec, LocationTracker { + INSTANCE; + public static final String CONFIG_NAME = "TRACK_NONE"; @Override @@ -49,12 +54,18 @@ public class NoneLocationTrackingSpec implements LocationTrackingSpec { } @Override - public Address computeTraceAddress(PluginTool tool, DebuggerCoordinates coordinates) { - return null; + public LocationTracker getTracker() { + return this; } @Override - public boolean affectedByRegisterChange(TraceAddressSpace space, + public CompletableFuture

    computeTraceAddress(PluginTool tool, + DebuggerCoordinates coordinates) { + return AsyncUtils.nil(); + } + + @Override + public boolean affectedByBytesChange(TraceAddressSpace space, TraceAddressSnapRange range, DebuggerCoordinates coordinates) { return false; } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/PCByRegisterLocationTrackingSpec.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/PCByRegisterLocationTrackingSpec.java index 400e0b7f01..7bbe7cf764 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/PCByRegisterLocationTrackingSpec.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/PCByRegisterLocationTrackingSpec.java @@ -23,7 +23,9 @@ import ghidra.program.model.address.AddressSpace; import ghidra.program.model.lang.Register; import ghidra.trace.model.guest.TracePlatform; -public class PCByRegisterLocationTrackingSpec implements RegisterLocationTrackingSpec { +public enum PCByRegisterLocationTrackingSpec implements RegisterLocationTrackingSpec { + INSTANCE; + public static final String CONFIG_NAME = "TRACK_PC_BY_REGISTER"; @Override diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/PCByStackLocationTrackingSpec.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/PCByStackLocationTrackingSpec.java index ad69988204..f05acdb08b 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/PCByStackLocationTrackingSpec.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/PCByStackLocationTrackingSpec.java @@ -15,6 +15,8 @@ */ package ghidra.app.plugin.core.debug.gui.action; +import java.util.concurrent.CompletableFuture; + import javax.swing.Icon; import ghidra.app.plugin.core.debug.DebuggerCoordinates; @@ -28,7 +30,9 @@ import ghidra.trace.model.stack.TraceStackFrame; import ghidra.trace.model.thread.TraceThread; import ghidra.trace.util.TraceAddressSpace; -public class PCByStackLocationTrackingSpec implements LocationTrackingSpec { +public enum PCByStackLocationTrackingSpec implements LocationTrackingSpec, LocationTracker { + INSTANCE; + public static final String CONFIG_NAME = "TRACK_PC_BY_STACK"; @Override @@ -52,7 +56,11 @@ public class PCByStackLocationTrackingSpec implements LocationTrackingSpec { } @Override - public Address computeTraceAddress(PluginTool tool, DebuggerCoordinates coordinates) { + public LocationTracker getTracker() { + return this; + } + + public Address doComputeTraceAddress(PluginTool tool, DebuggerCoordinates coordinates) { Trace trace = coordinates.getTrace(); TraceThread thread = coordinates.getThread(); long snap = coordinates.getSnap(); @@ -68,6 +76,12 @@ public class PCByStackLocationTrackingSpec implements LocationTrackingSpec { return frame.getProgramCounter(snap); } + @Override + public CompletableFuture
    computeTraceAddress(PluginTool tool, + DebuggerCoordinates coordinates) { + return CompletableFuture.supplyAsync(() -> doComputeTraceAddress(tool, coordinates)); + } + // Note it does no good to override affectByRegChange. It must do what we'd avoid anyway. @Override public boolean affectedByStackChange(TraceStack stack, DebuggerCoordinates coordinates) { @@ -89,7 +103,7 @@ public class PCByStackLocationTrackingSpec implements LocationTrackingSpec { } @Override - public boolean affectedByRegisterChange(TraceAddressSpace space, TraceAddressSnapRange range, + public boolean affectedByBytesChange(TraceAddressSpace space, TraceAddressSnapRange range, DebuggerCoordinates coordinates) { return false; } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/PCLocationTrackingSpec.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/PCLocationTrackingSpec.java index 783ac0bd2b..87084b6863 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/PCLocationTrackingSpec.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/PCLocationTrackingSpec.java @@ -15,6 +15,8 @@ */ package ghidra.app.plugin.core.debug.gui.action; +import java.util.concurrent.CompletableFuture; + import javax.swing.Icon; import ghidra.app.plugin.core.debug.DebuggerCoordinates; @@ -25,13 +27,15 @@ import ghidra.trace.model.TraceAddressSnapRange; import ghidra.trace.model.stack.TraceStack; import ghidra.trace.util.TraceAddressSpace; -public class PCLocationTrackingSpec implements LocationTrackingSpec { +public enum PCLocationTrackingSpec implements LocationTrackingSpec, LocationTracker { + INSTANCE; + public static final String CONFIG_NAME = "TRACK_PC"; private static final PCByRegisterLocationTrackingSpec BY_REG = - new PCByRegisterLocationTrackingSpec(); + PCByRegisterLocationTrackingSpec.INSTANCE; private static final PCByStackLocationTrackingSpec BY_STACK = - new PCByStackLocationTrackingSpec(); + PCByStackLocationTrackingSpec.INSTANCE; @Override public String getConfigName() { @@ -54,14 +58,22 @@ public class PCLocationTrackingSpec implements LocationTrackingSpec { } @Override - public Address computeTraceAddress(PluginTool tool, DebuggerCoordinates coordinates) { - if (coordinates.getTime().isSnapOnly()) { - Address pc = BY_STACK.computeTraceAddress(tool, coordinates); - if (pc != null) { - return pc; + public LocationTracker getTracker() { + return this; + } + + @Override + public CompletableFuture
    computeTraceAddress(PluginTool tool, + DebuggerCoordinates coordinates) { + return CompletableFuture.supplyAsync(() -> { + if (coordinates.getTime().isSnapOnly()) { + Address pc = BY_STACK.doComputeTraceAddress(tool, coordinates); + if (pc != null) { + return pc; + } } - } - return BY_REG.computeTraceAddress(tool, coordinates); + return BY_REG.doComputeTraceAddress(tool, coordinates); + }); } // Note it does no good to override affectByRegChange. It must do what we'd avoid anyway. @@ -71,8 +83,8 @@ public class PCLocationTrackingSpec implements LocationTrackingSpec { } @Override - public boolean affectedByRegisterChange(TraceAddressSpace space, TraceAddressSnapRange range, + public boolean affectedByBytesChange(TraceAddressSpace space, TraceAddressSnapRange range, DebuggerCoordinates coordinates) { - return BY_REG.affectedByRegisterChange(space, range, coordinates); + return BY_REG.affectedByBytesChange(space, range, coordinates); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/RegisterLocationTrackingSpec.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/RegisterLocationTrackingSpec.java index 6583f9f2cd..8af97f5b7c 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/RegisterLocationTrackingSpec.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/RegisterLocationTrackingSpec.java @@ -15,6 +15,8 @@ */ package ghidra.app.plugin.core.debug.gui.action; +import java.util.concurrent.CompletableFuture; + import ghidra.app.plugin.core.debug.DebuggerCoordinates; import ghidra.framework.plugintool.PluginTool; import ghidra.program.model.address.*; @@ -29,8 +31,7 @@ import ghidra.trace.model.stack.TraceStack; import ghidra.trace.model.thread.TraceThread; import ghidra.trace.util.TraceAddressSpace; -// TODO: Use this, or allow arbitrary expressions -public interface RegisterLocationTrackingSpec extends LocationTrackingSpec { +public interface RegisterLocationTrackingSpec extends LocationTrackingSpec, LocationTracker { Register computeRegister(DebuggerCoordinates coordinates); AddressSpace computeDefaultAddressSpace(DebuggerCoordinates coordinates); @@ -45,7 +46,11 @@ public interface RegisterLocationTrackingSpec extends LocationTrackingSpec { } @Override - default Address computeTraceAddress(PluginTool tool, DebuggerCoordinates coordinates) { + default LocationTracker getTracker() { + return this; + } + + default Address doComputeTraceAddress(PluginTool tool, DebuggerCoordinates coordinates) { Trace trace = coordinates.getTrace(); TracePlatform platform = coordinates.getPlatform(); TraceThread thread = coordinates.getThread(); @@ -81,7 +86,13 @@ public interface RegisterLocationTrackingSpec extends LocationTrackingSpec { } @Override - default boolean affectedByRegisterChange(TraceAddressSpace space, + default CompletableFuture
    computeTraceAddress(PluginTool tool, + DebuggerCoordinates coordinates) { + return CompletableFuture.supplyAsync(() -> doComputeTraceAddress(tool, coordinates)); + } + + @Override + default boolean affectedByBytesChange(TraceAddressSpace space, TraceAddressSnapRange range, DebuggerCoordinates coordinates) { if (!LocationTrackingSpec.changeIsCurrent(space, range, coordinates)) { return false; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/SPLocationTrackingSpec.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/SPLocationTrackingSpec.java index b38dd679cd..8d8680adec 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/SPLocationTrackingSpec.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/SPLocationTrackingSpec.java @@ -23,7 +23,9 @@ import ghidra.program.model.address.AddressSpace; import ghidra.program.model.lang.Register; import ghidra.trace.model.guest.TracePlatform; -public class SPLocationTrackingSpec implements RegisterLocationTrackingSpec { +public enum SPLocationTrackingSpec implements RegisterLocationTrackingSpec { + INSTANCE; + public static final String CONFIG_NAME = "TRACK_SP"; @Override diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/WatchLocationTrackingSpec.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/WatchLocationTrackingSpec.java new file mode 100644 index 0000000000..e9ae639cdb --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/WatchLocationTrackingSpec.java @@ -0,0 +1,151 @@ +/* ### + * 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.action; + +import java.util.Objects; +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.gui.DebuggerResources.TrackLocationAction; +import ghidra.app.plugin.core.debug.gui.watch.WatchRow; +import ghidra.app.plugin.processors.sleigh.SleighLanguage; +import ghidra.async.AsyncUtils; +import ghidra.framework.plugintool.PluginTool; +import ghidra.pcode.exec.*; +import ghidra.pcode.exec.DebuggerPcodeUtils.WatchValue; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressSetView; +import ghidra.program.model.lang.Language; +import ghidra.trace.model.TraceAddressSnapRange; +import ghidra.trace.model.stack.TraceStack; +import ghidra.trace.util.TraceAddressSpace; + +/** + * A tracking specification for the address of a given Sleigh expression + */ +public class WatchLocationTrackingSpec implements LocationTrackingSpec { + + public static final String CONFIG_PREFIX = "TRACK_WATCH_"; + + private final String expression; + + /** + * Derive a tracking specification from the given watch + * + * @param watch the watch who address to follow + * @return the tracking specification + */ + public static WatchLocationTrackingSpec fromWatch(WatchRow watch) { + return new WatchLocationTrackingSpec(watch.getExpression()); + } + + /** + * Create a tracking specification from the given expression + * + * @param expression the Sleigh expression whose address to follow + */ + public WatchLocationTrackingSpec(String expression) { + this.expression = expression; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof WatchLocationTrackingSpec that)) { + return false; + } + return this.expression.equals(that.expression); + } + + @Override + public String getConfigName() { + return CONFIG_PREFIX + expression; + } + + @Override + public String getMenuName() { + return TrackLocationAction.NAME_PREFIX_WATCH + expression; + } + + @Override + public Icon getMenuIcon() { + return DebuggerResources.ICON_REGISTER_MARKER; + } + + @Override + public String computeTitle(DebuggerCoordinates coordinates) { + return "&(" + expression + ")"; + } + + /** + * The tracking logic for a watch (Sleigh expression) + */ + class WatchLocationTracker implements LocationTracker { + private AddressSetView reads; + private DebuggerCoordinates current = DebuggerCoordinates.NOWHERE; + private PcodeExecutor asyncExec = null; + private PcodeExpression compiled; + + @Override + public CompletableFuture
    computeTraceAddress(PluginTool tool, + DebuggerCoordinates coordinates) { + if (!Objects.equals(current, coordinates) || asyncExec == null) { + current = coordinates; + asyncExec = current.getPlatform() == null ? null + : DebuggerPcodeUtils.buildWatchExecutor(tool, coordinates); + } + else { + asyncExec.getState().clear(); + } + if (current.getTrace() == null) { + return AsyncUtils.nil(); + } + return CompletableFuture.supplyAsync(() -> { + Language language = current.getPlatform().getLanguage(); + if (!(language instanceof SleighLanguage slang)) { + return null; + } + if (compiled == null || compiled.getLanguage() != language) { + compiled = SleighProgramCompiler.compileExpression(slang, expression); + } + WatchValue value = compiled.evaluate(asyncExec); + return value == null ? null : value.address(); + }); + } + + @Override + public boolean affectedByBytesChange(TraceAddressSpace space, TraceAddressSnapRange range, + DebuggerCoordinates coordinates) { + return LocationTrackingSpec.changeIsCurrent(space, range, coordinates) && + (reads == null || reads.intersects(range.getX1(), range.getX2())); + } + + @Override + public boolean affectedByStackChange(TraceStack stack, DebuggerCoordinates coordinates) { + return false; + } + } + + @Override + public LocationTracker getTracker() { + return new WatchLocationTracker(); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/WatchLocationTrackingSpecFactory.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/WatchLocationTrackingSpecFactory.java new file mode 100644 index 0000000000..c5f4c7ad50 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/WatchLocationTrackingSpecFactory.java @@ -0,0 +1,53 @@ +/* ### + * 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.action; + +import java.util.List; +import java.util.stream.Collectors; + +import ghidra.app.services.DebuggerWatchesService; +import ghidra.framework.plugintool.PluginTool; + +/** + * The factory for tracking specifications based on watches + * + *

    + * This will generate an "address-of-watch" tracking specification for each watch currently in the + * watches service, i.e., configured in the Watches window. + */ +public class WatchLocationTrackingSpecFactory implements LocationTrackingSpecFactory { + + @Override + public List getSuggested(PluginTool tool) { + DebuggerWatchesService watchesService = tool.getService(DebuggerWatchesService.class); + if (watchesService == null) { + return List.of(); + } + return watchesService.getWatches() + .stream() + .map(WatchLocationTrackingSpec::fromWatch) + .collect(Collectors.toList()); + } + + @Override + public LocationTrackingSpec parseSpec(String name) { + if (!name.startsWith(WatchLocationTrackingSpec.CONFIG_PREFIX)) { + return null; + } + String expression = name.substring(WatchLocationTrackingSpec.CONFIG_PREFIX.length()); + return new WatchLocationTrackingSpec(expression); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingPlugin.java index 1165bfffaf..4002f5fcf4 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingPlugin.java @@ -471,8 +471,7 @@ public class DebuggerListingPlugin extends AbstractCodeBrowserPlugin asyncWatchExecutor; // name is reminder to use asynchronously + PcodeExecutor prevValueExecutor; + // TODO: We could do better, but the tests can't sync if we do multi-threaded evaluation + ExecutorService workQueue = Executors.newSingleThreadExecutor(); @AutoServiceConsumed private DebuggerListingService listingService; // For goto and selection @@ -790,14 +805,24 @@ public class DebuggerWatchesProvider extends ComponentProviderAdapter { watchRow.setDataType(regRow.getDataType()); } + @Override public WatchRow addWatch(String expression) { WatchRow row = new WatchRow(this, ""); - row.setCoordinates(current); watchTableModel.add(row); row.setExpression(expression); return row; } + @Override + public void removeWatch(WatchRow row) { + watchTableModel.delete(row); + } + + @Override + public synchronized List getWatches() { + return List.copyOf(watchTableModel.getModelData()); + } + @Override public JComponent getComponent() { return mainPanel; @@ -831,44 +856,37 @@ public class DebuggerWatchesProvider extends ComponentProviderAdapter { current = coordinates; return; } + previous = current; current = coordinates; doSetTrace(current.getTrace()); - setRowsContext(coordinates); - - if (current.isAliveAndReadsPresent()) { - readTarget(); + TracePlatform platform = current.getPlatform(); + Language lang = platform == null ? null : platform.getLanguage(); + if (lang instanceof SleighLanguage slang) { + language = slang; } + else { + language = null; + } + + asyncWatchExecutor = current.getPlatform() == null ? null + : DebuggerPcodeUtils.buildWatchExecutor(tool, current); + prevValueExecutor = current.getPlatform() == null || previous.getPlatform() == null ? null + : TraceSleighUtils.buildByteExecutor(previous.getPlatform(), + previous.getViewSnap(), previous.getThread(), previous.getFrame()); reevaluate(); - Swing.runIfSwingOrRunLater(() -> watchTableModel.fireTableDataChanged()); - } - - public synchronized void setRowsContext(DebuggerCoordinates coordinates) { - for (WatchRow row : watchTableModel.getModelData()) { - row.setCoordinates(coordinates); - } - } - - public synchronized void readTarget() { - for (WatchRow row : watchTableModel.getModelData()) { - row.doTargetReads(); - } } public synchronized void doCheckDepsAndReevaluate() { + asyncWatchExecutor.getState().clear(); for (WatchRow row : watchTableModel.getModelData()) { AddressSetView reads = row.getReads(); if (reads == null || reads.intersects(changed)) { - row.doTargetReads(); row.reevaluate(); } } changed.clear(); - Swing.runIfSwingOrRunLater(() -> { - watchTableModel.fireTableDataChanged(); - contextChanged(); - }); } public void reevaluate() { @@ -914,4 +932,14 @@ public class DebuggerWatchesProvider extends ComponentProviderAdapter { public void goToTime(TraceSchedule time) { traceManager.activateTime(time); } + + public void waitEvaluate(int timeoutMs) { + try { + CompletableFuture.runAsync(() -> { + }, workQueue).get(timeoutMs, TimeUnit.MILLISECONDS); + } + catch (ExecutionException | InterruptedException | TimeoutException e) { + throw new AssertionError(e); + } + } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/watch/WatchRow.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/watch/WatchRow.java index 486b1f3fed..9bf72032b1 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/watch/WatchRow.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/watch/WatchRow.java @@ -19,8 +19,6 @@ import java.math.BigInteger; import java.util.*; import java.util.concurrent.CompletableFuture; -import org.apache.commons.lang3.tuple.Pair; - import com.google.common.collect.Range; import ghidra.app.plugin.core.debug.DebuggerCoordinates; @@ -28,17 +26,16 @@ import ghidra.app.plugin.processors.sleigh.SleighLanguage; import ghidra.app.services.DataTypeManagerService; import ghidra.app.services.DebuggerStateEditingService; import ghidra.app.services.DebuggerStateEditingService.StateEditor; +import ghidra.async.AsyncUtils; import ghidra.docking.settings.Settings; import ghidra.docking.settings.SettingsImpl; import ghidra.framework.options.SaveState; import ghidra.pcode.exec.*; -import ghidra.pcode.exec.PcodeExecutorStatePiece.Reason; -import ghidra.pcode.exec.trace.*; +import ghidra.pcode.exec.DebuggerPcodeUtils.WatchValue; import ghidra.pcode.utils.Utils; import ghidra.program.model.address.*; import ghidra.program.model.data.DataType; import ghidra.program.model.data.DataTypeEncodeException; -import ghidra.program.model.lang.Language; import ghidra.program.model.listing.Function; import ghidra.program.model.listing.Program; import ghidra.program.model.mem.ByteMemBufferImpl; @@ -49,8 +46,8 @@ import ghidra.trace.model.*; import ghidra.trace.model.guest.TracePlatform; import ghidra.trace.model.memory.TraceMemoryState; import ghidra.trace.model.symbol.TraceLabelSymbol; -import ghidra.trace.model.thread.TraceThread; -import ghidra.util.*; +import ghidra.util.Msg; +import ghidra.util.NumericUtilities; public class WatchRow { public static final int TRUNCATE_BYTES_LENGTH = 64; @@ -59,12 +56,6 @@ public class WatchRow { private static final String KEY_SETTINGS = "settings"; private final DebuggerWatchesProvider provider; - private Trace trace; - private DebuggerCoordinates coordinates; - private SleighLanguage language; - private PcodeExecutor> executorWithState; - private ReadDepsPcodeExecutor executorWithAddress; - private PcodeExecutor asyncExecutor; // name is reminder to use asynchronously private String expression; private String typePath; @@ -101,14 +92,14 @@ public class WatchRow { protected void recompile() { compiled = null; error = null; + if (provider.language == null) { + return; + } if (expression == null || expression.length() == 0) { return; } - if (language == null) { - return; - } try { - compiled = SleighProgramCompiler.compileExpression(language, expression); + compiled = SleighProgramCompiler.compileExpression(provider.language, expression); } catch (Exception e) { error = e; @@ -116,203 +107,73 @@ public class WatchRow { } } - protected void doTargetReads() { - if (compiled != null && asyncExecutor != null) { - CompletableFuture asyncEvaluation = - CompletableFuture.supplyAsync(() -> compiled.evaluate(asyncExecutor)); - asyncEvaluation.exceptionally(ex -> { - error = ex; - Swing.runIfSwingOrRunLater(() -> { - provider.watchTableModel.notifyUpdated(this); - }); - return null; - }); - // NB. Re-evaluation triggered by database changes, or called separately - } - } - protected void reevaluate() { blank(); - if (trace == null || compiled == null) { + SleighLanguage language = provider.language; + PcodeExecutor executor = provider.asyncWatchExecutor; + PcodeExecutor prevExec = provider.prevValueExecutor; + if (executor == null) { + provider.contextChanged(); return; } - try { - Pair valueWithState = compiled.evaluate(executorWithState); - Pair valueWithAddress = compiled.evaluate(executorWithAddress); + CompletableFuture.runAsync(() -> { + if (compiled == null || compiled.getLanguage() != language) { + recompile(); + } + if (compiled == null) { + provider.contextChanged(); + return; + } + + WatchValue fullValue = compiled.evaluate(executor); + prevValue = prevExec == null ? null : compiled.evaluate(prevExec); TracePlatform platform = provider.current.getPlatform(); - value = valueWithState.getLeft(); + value = fullValue.bytes(); error = null; - state = valueWithState.getRight(); + state = fullValue.state(); // TODO: Optional column for guest address? - address = platform.mapGuestToHost(valueWithAddress.getRight()); + address = platform.mapGuestToHost(fullValue.address()); symbol = computeSymbol(); - reads = platform.mapGuestToHost(executorWithAddress.getReads()); + // reads piece uses trace access to translate to host/overlay already + reads = fullValue.reads(); valueObj = parseAsDataTypeObj(); valueString = parseAsDataTypeStr(); - } - catch (Exception e) { + }, provider.workQueue).exceptionally(e -> { error = e; - } + provider.contextChanged(); + return null; + }).thenRunAsync(() -> { + provider.watchTableModel.fireTableDataChanged(); + provider.contextChanged(); + }, AsyncUtils.SWING_EXECUTOR); } protected String parseAsDataTypeStr() { if (dataType == null || value == null) { return ""; } - MemBuffer buffer = new ByteMemBufferImpl(address, value, language.isBigEndian()); + MemBuffer buffer = new ByteMemBufferImpl(address, value, provider.language.isBigEndian()); return dataType.getRepresentation(buffer, settings, value.length); } - // TODO: DataType settings - protected Object parseAsDataTypeObj() { if (dataType == null || value == null) { return null; } - MemBuffer buffer = new ByteMemBufferImpl(address, value, language.isBigEndian()); + MemBuffer buffer = new ByteMemBufferImpl(address, value, provider.language.isBigEndian()); return dataType.getValue(buffer, SettingsImpl.NO_SETTINGS, value.length); } - public static class ReadDepsTraceBytesPcodeExecutorStatePiece - extends DirectBytesTracePcodeExecutorStatePiece { - private AddressSet reads = new AddressSet(); - - public ReadDepsTraceBytesPcodeExecutorStatePiece(TracePlatform platform, long snap, - TraceThread thread, int frame) { - super(DirectBytesTracePcodeExecutorState.getDefaultThreadAccess(platform, snap, thread, - frame)); - } - - @Override - public byte[] getVar(AddressSpace space, long offset, int size, boolean quantize, - Reason reason) { - byte[] data = super.getVar(space, offset, size, quantize, reason); - if (space.isMemorySpace()) { - offset = quantizeOffset(space, offset); - } - if (space.isMemorySpace() || space.isRegisterSpace()) { - try { - reads.add(new AddressRangeImpl(space.getAddress(offset), data.length)); - } - catch (AddressOverflowException | AddressOutOfBoundsException e) { - throw new AssertionError(e); - } - } - return data; - } - - @Override - protected void setInSpace(AddressSpace space, long offset, int size, byte[] val) { - throw new UnsupportedOperationException("Expression cannot write to trace"); - } - - public void reset() { - reads = new AddressSet(); - } - - public AddressSet getReads() { - return new AddressSet(reads); - } - } - - public static class ReadDepsPcodeExecutor - extends PcodeExecutor> { - private ReadDepsTraceBytesPcodeExecutorStatePiece depsPiece; - - public ReadDepsPcodeExecutor(ReadDepsTraceBytesPcodeExecutorStatePiece depsState, - SleighLanguage language, PairedPcodeArithmetic arithmetic, - PcodeExecutorState> state) { - super(language, arithmetic, state, Reason.INSPECT); - this.depsPiece = depsState; - } - - @Override - public PcodeFrame execute(PcodeProgram program, - PcodeUseropLibrary> library) { - depsPiece.reset(); - return super.execute(program, library); - } - - public AddressSet getReads() { - return depsPiece.getReads(); - } - } - - /** - * Build an executor that can compute three things simultaneously - * - *

    - * This computes the concrete value, its address, and the set of physical addresses involved in - * the computation. The resulting pair gives the value and its address. To get the addresses - * involved, invoke {@link ReadDepsPcodeExecutor#getReads()} after evaluation. - * - * @param coordinates the coordinates providing context for the evaluation - * @return an executor for evaluating the watch - */ - protected static ReadDepsPcodeExecutor buildAddressDepsExecutor( - DebuggerCoordinates coordinates) { - TracePlatform platform = coordinates.getPlatform(); - ReadDepsTraceBytesPcodeExecutorStatePiece piece = - new ReadDepsTraceBytesPcodeExecutorStatePiece(platform, coordinates.getViewSnap(), - coordinates.getThread(), coordinates.getFrame()); - Language language = platform.getLanguage(); - if (!(language instanceof SleighLanguage slang)) { - throw new IllegalArgumentException("Watch expressions require a Sleigh language"); - } - PcodeExecutorState> paired = new DefaultPcodeExecutorState<>(piece) - .paired(new AddressOfPcodeExecutorStatePiece(language)); - PairedPcodeArithmetic arithmetic = new PairedPcodeArithmetic<>( - BytesPcodeArithmetic.forLanguage(language), AddressOfPcodeArithmetic.INSTANCE); - return new ReadDepsPcodeExecutor(piece, slang, arithmetic, paired); - } - - public void setCoordinates(DebuggerCoordinates coordinates) { - // NB. Caller has already verified coordinates actually changed - prevValue = value; - trace = coordinates.getTrace(); - this.coordinates = coordinates; - updateType(); - if (trace == null) { - blank(); - return; - } - Language newLanguage = trace.getBaseLanguage(); - if (language != newLanguage) { - if (!(newLanguage instanceof SleighLanguage)) { - error = new RuntimeException("Not a sleigh-based language"); - return; - } - language = (SleighLanguage) newLanguage; - recompile(); - } - if (coordinates.isAliveAndReadsPresent()) { - asyncExecutor = - DebuggerPcodeUtils.executorForCoordinates(provider.getTool(), coordinates); - } - executorWithState = TraceSleighUtils.buildByteWithStateExecutor(trace, - coordinates.getViewSnap(), coordinates.getThread(), coordinates.getFrame()); - executorWithAddress = buildAddressDepsExecutor(coordinates); - } - public void setExpression(String expression) { if (!Objects.equals(this.expression, expression)) { prevValue = null; // NB. Allow fall-through so user can re-evaluate via nop edit. } this.expression = expression; - blank(); - recompile(); - if (error != null) { - provider.contextChanged(); - return; - } - if (asyncExecutor != null) { - doTargetReads(); - } + this.compiled = null; reevaluate(); - provider.contextChanged(); } public String getExpression() { @@ -325,6 +186,7 @@ public class WatchRow { return; } // Try from the trace first + Trace trace = provider.current.getTrace(); if (trace != null) { dataType = trace.getDataTypeManager().getDataType(typePath); if (dataType != null) { @@ -378,7 +240,7 @@ public class WatchRow { return settings; } - public void settingsChanged() { + protected void settingsChanged() { if (dataType != null) { savedSettings.write(dataType.getSettingsDefinitions(), dataType.getDefaultSettings()); } @@ -406,12 +268,12 @@ public class WatchRow { } public String getRawValueString() { - if (value == null) { + if (value == null || provider.language == null) { return "??"; } if (address == null || !address.getAddressSpace().isMemorySpace()) { - BigInteger asBigInt = - Utils.bytesToBigInteger(value, value.length, language.isBigEndian(), false); + BigInteger asBigInt = Utils.bytesToBigInteger(value, value.length, + provider.language.isBigEndian(), false); return "0x" + asBigInt.toString(16); } if (value.length > TRUNCATE_BYTES_LENGTH) { @@ -437,6 +299,10 @@ public class WatchRow { return state; } + public byte[] getValue() { + return value; + } + public String getValueString() { return valueString; } @@ -456,7 +322,7 @@ public class WatchRow { if (editingService == null) { return false; } - StateEditor editor = editingService.createStateEditor(coordinates); + StateEditor editor = editingService.createStateEditor(provider.current); return editor.isVariableEditable(address, getValueLength()); } @@ -488,7 +354,7 @@ public class WatchRow { val = new BigInteger(intString, 10); } setRawValueBytes( - Utils.bigIntegerToBytes(val, value.length, trace.getBaseLanguage().isBigEndian())); + Utils.bigIntegerToBytes(val, value.length, provider.language.isBigEndian())); } public void setRawValueBytes(byte[] bytes) { @@ -507,7 +373,7 @@ public class WatchRow { if (editingService == null) { throw new AssertionError("No editing service"); } - StateEditor editor = editingService.createStateEditor(coordinates); + StateEditor editor = editingService.createStateEditor(provider.current); editor.setVariable(address, bytes).exceptionally(ex -> { Msg.showError(this, null, "Write Failed", "Could not modify watch value (on target)", ex); @@ -523,7 +389,7 @@ public class WatchRow { } try { byte[] encoded = dataType.encodeRepresentation(valueString, - new ByteMemBufferImpl(address, value, language.isBigEndian()), + new ByteMemBufferImpl(address, value, provider.language.isBigEndian()), SettingsImpl.NO_SETTINGS, value.length); setRawValueBytes(encoded); } @@ -550,8 +416,10 @@ public class WatchRow { if (address == null || !address.isMemoryAddress()) { return null; } + DebuggerCoordinates current = provider.current; + Trace trace = current.getTrace(); Collection labels = - trace.getSymbolManager().labels().getAt(coordinates.getSnap(), null, address, false); + trace.getSymbolManager().labels().getAt(current.getSnap(), null, address, false); if (!labels.isEmpty()) { return labels.iterator().next(); } @@ -560,7 +428,7 @@ public class WatchRow { return null; } TraceLocation dloc = - new DefaultTraceLocation(trace, null, Range.singleton(coordinates.getSnap()), address); + new DefaultTraceLocation(trace, null, Range.singleton(current.getSnap()), address); ProgramLocation sloc = provider.mappingService.getOpenMappedLocation(dloc); if (sloc == null) { return null; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/data/DefaultPcodeDebuggerMemoryAccess.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/data/DefaultPcodeDebuggerMemoryAccess.java index 874ac9f995..fc5679c9b0 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/data/DefaultPcodeDebuggerMemoryAccess.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/data/DefaultPcodeDebuggerMemoryAccess.java @@ -65,7 +65,7 @@ public class DefaultPcodeDebuggerMemoryAccess extends DefaultPcodeTraceMemoryAcc @Override public boolean isLive() { - return recorder != null && recorder.isRecording() && recorder.getSnap() == snap; + return InternalPcodeDebuggerDataAccess.super.isLive(); } @Override diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/data/DefaultPcodeDebuggerRegistersAccess.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/data/DefaultPcodeDebuggerRegistersAccess.java index 345ad31b5b..9cd01ca33b 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/data/DefaultPcodeDebuggerRegistersAccess.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/data/DefaultPcodeDebuggerRegistersAccess.java @@ -60,7 +60,7 @@ public class DefaultPcodeDebuggerRegistersAccess extends DefaultPcodeTraceRegist @Override public boolean isLive() { - return recorder != null && recorder.isRecording() && recorder.getSnap() == snap; + return InternalPcodeDebuggerDataAccess.super.isLive(); } @Override @@ -75,7 +75,7 @@ public class DefaultPcodeDebuggerRegistersAccess extends DefaultPcodeTraceRegist @Override public CompletableFuture readFromTargetRegisters(AddressSetView guestView) { - if (!isLive()) { + if (guestView.isEmpty() || !isLive()) { return CompletableFuture.completedFuture(false); } Set toRead = new HashSet<>(); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/data/InternalPcodeDebuggerDataAccess.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/data/InternalPcodeDebuggerDataAccess.java index bfcd98d914..06805d3ddf 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/data/InternalPcodeDebuggerDataAccess.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/data/InternalPcodeDebuggerDataAccess.java @@ -19,10 +19,25 @@ import ghidra.app.services.TraceRecorder; import ghidra.framework.plugintool.PluginTool; import ghidra.lifecycle.Internal; import ghidra.pcode.exec.trace.data.InternalPcodeTraceDataAccess; +import ghidra.trace.model.TraceTimeViewport; @Internal public interface InternalPcodeDebuggerDataAccess extends InternalPcodeTraceDataAccess { PluginTool getTool(); TraceRecorder getRecorder(); + + default boolean isLive() { + TraceRecorder recorder = getRecorder(); + if (recorder == null || !recorder.isRecording()) { + return false; + } + TraceTimeViewport viewport = getViewport(); + for (long s : viewport.getReversedSnaps()) { + if (recorder.getSnap() == s) { + return true; + } + } + return false; + } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerWatchesService.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerWatchesService.java new file mode 100644 index 0000000000..b183f3a0a0 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerWatchesService.java @@ -0,0 +1,52 @@ +/* ### + * 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.util.Collection; + +import ghidra.app.plugin.core.debug.gui.watch.DebuggerWatchesPlugin; +import ghidra.app.plugin.core.debug.gui.watch.WatchRow; +import ghidra.framework.plugintool.ServiceInfo; + +/** + * A service interface for controlling the Watches window + */ +@ServiceInfo( + defaultProvider = DebuggerWatchesPlugin.class, + description = "Service for managing watches") +public interface DebuggerWatchesService { + /** + * Add a watch + * + * @param expression the Sleigh expression + * @return the new row + */ + WatchRow addWatch(String expression); + + /** + * Remove a watch + * + * @param watch the row to remove + */ + void removeWatch(WatchRow watch); + + /** + * Get the current watches + * + * @return the unmodifiable collection of watches + */ + Collection getWatches(); +} 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 ba8a8e4237..440eaf1f3d 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 @@ -21,10 +21,18 @@ import ghidra.app.plugin.core.debug.service.emulation.data.DefaultPcodeDebuggerA import ghidra.app.plugin.processors.sleigh.SleighLanguage; import ghidra.framework.plugintool.PluginTool; import ghidra.pcode.emu.ThreadPcodeExecutorState; +import ghidra.pcode.exec.PcodeArithmetic.Purpose; import ghidra.pcode.exec.PcodeExecutorStatePiece.Reason; +import ghidra.pcode.exec.trace.*; +import ghidra.pcode.exec.trace.data.DefaultPcodeTraceAccess; +import ghidra.pcode.exec.trace.data.PcodeTraceDataAccess; +import ghidra.program.model.address.*; +import ghidra.program.model.lang.Endian; import ghidra.program.model.lang.Language; +import ghidra.program.model.mem.MemBuffer; import ghidra.trace.model.Trace; import ghidra.trace.model.guest.TracePlatform; +import ghidra.trace.model.memory.TraceMemoryState; /** * Utilities for evaluating or executing Sleigh/p-code in the Debugger @@ -39,13 +47,12 @@ public enum DebuggerPcodeUtils { * If a thread is included, the executor state will have access to both the memory and registers * in the context of that thread. Otherwise, only memory access is permitted. * - * @param tool the plugin tool. TODO: This shouldn't be required + * @param tool the plugin tool * @param coordinates the coordinates * @return the state */ public static PcodeExecutorState executorStateForCoordinates(PluginTool tool, DebuggerCoordinates coordinates) { - // TODO: Make platform part of coordinates Trace trace = coordinates.getTrace(); if (trace == null) { throw new IllegalArgumentException("Coordinates have no trace"); @@ -57,7 +64,7 @@ public enum DebuggerPcodeUtils { "Given trace or platform does not use a Sleigh language"); } DefaultPcodeDebuggerAccess access = new DefaultPcodeDebuggerAccess(tool, - coordinates.getRecorder(), platform, coordinates.getSnap()); + coordinates.getRecorder(), platform, coordinates.getViewSnap()); PcodeExecutorState shared = new RWTargetMemoryPcodeExecutorState(access.getDataForSharedState(), Mode.RW); if (coordinates.getThread() == null) { @@ -87,4 +94,261 @@ public enum DebuggerPcodeUtils { return new PcodeExecutor<>(slang, BytesPcodeArithmetic.forLanguage(slang), state, Reason.INSPECT); } + + /** + * The value of a watch expression including its state, address, and addresses read + */ + public record WatchValue(byte[] bytes, TraceMemoryState state, Address address, + AddressSetView reads) { + } + + /** + * A p-code arithmetic on watch values + * + *

    + * This is just a composition of four arithmetics. Using Pair> would be + * unwieldy. + */ + public enum WatchValuePcodeArithmetic implements PcodeArithmetic { + BIG_ENDIAN(BytesPcodeArithmetic.BIG_ENDIAN), + LITTLE_ENDIAN(BytesPcodeArithmetic.LITTLE_ENDIAN); + + public static WatchValuePcodeArithmetic forEndian(boolean isBigEndian) { + return isBigEndian ? BIG_ENDIAN : LITTLE_ENDIAN; + } + + public static WatchValuePcodeArithmetic forLanguage(Language language) { + return forEndian(language.isBigEndian()); + } + + private static final TraceMemoryStatePcodeArithmetic STATE = + TraceMemoryStatePcodeArithmetic.INSTANCE; + private static final AddressOfPcodeArithmetic ADDRESS = + AddressOfPcodeArithmetic.INSTANCE; + private static final AddressesReadPcodeArithmetic READS = + AddressesReadPcodeArithmetic.INSTANCE; + + private final BytesPcodeArithmetic bytes; + + private WatchValuePcodeArithmetic(BytesPcodeArithmetic bytes) { + this.bytes = bytes; + } + + @Override + public Endian getEndian() { + return bytes.getEndian(); + } + + @Override + public WatchValue unaryOp(int opcode, int sizeout, int sizein1, WatchValue in1) { + return new WatchValue( + bytes.unaryOp(opcode, sizeout, sizein1, in1.bytes), + STATE.unaryOp(opcode, sizeout, sizein1, in1.state), + ADDRESS.unaryOp(opcode, sizeout, sizein1, in1.address), + READS.unaryOp(opcode, sizeout, sizein1, in1.reads)); + } + + @Override + public WatchValue binaryOp(int opcode, int sizeout, int sizein1, WatchValue in1, + int sizein2, WatchValue in2) { + return new WatchValue( + bytes.binaryOp(opcode, sizeout, sizein1, in1.bytes, sizein2, in2.bytes), + STATE.binaryOp(opcode, sizeout, sizein1, in1.state, sizein2, in2.state), + ADDRESS.binaryOp(opcode, sizeout, sizein1, in1.address, sizein2, in2.address), + READS.binaryOp(opcode, sizeout, sizein1, in1.reads, sizein2, in2.reads)); + } + + @Override + public WatchValue modBeforeStore(int sizeout, int sizeinAddress, WatchValue inAddress, + int sizeinValue, WatchValue inValue) { + return new WatchValue( + bytes.modBeforeStore(sizeout, sizeinAddress, inAddress.bytes, + sizeinValue, inValue.bytes), + STATE.modBeforeStore(sizeout, sizeinAddress, inAddress.state, + sizeinValue, inValue.state), + ADDRESS.modBeforeStore(sizeout, sizeinAddress, inAddress.address, + sizeinValue, inValue.address), + READS.modBeforeStore(sizeout, sizeinAddress, inAddress.reads, + sizeinValue, inValue.reads)); + } + + @Override + public WatchValue modAfterLoad(int sizeout, int sizeinAddress, WatchValue inAddress, + int sizeinValue, WatchValue inValue) { + return new WatchValue( + bytes.modAfterLoad(sizeout, sizeinAddress, inAddress.bytes, + sizeinValue, inValue.bytes), + STATE.modAfterLoad(sizeout, sizeinAddress, inAddress.state, + sizeinValue, inValue.state), + ADDRESS.modAfterLoad(sizeout, sizeinAddress, inAddress.address, + sizeinValue, inValue.address), + READS.modAfterLoad(sizeout, sizeinAddress, inAddress.reads, + sizeinValue, inValue.reads)); + } + + @Override + public WatchValue fromConst(byte[] value) { + return new WatchValue( + bytes.fromConst(value), + STATE.fromConst(value), + ADDRESS.fromConst(value), + READS.fromConst(value)); + } + + @Override + public byte[] toConcrete(WatchValue value, Purpose purpose) { + return bytes.toConcrete(value.bytes, purpose); + } + + @Override + public long sizeOf(WatchValue value) { + return bytes.sizeOf(value.bytes); + } + } + + public static class WatchValuePcodeExecutorStatePiece + implements PcodeExecutorStatePiece { + private final PcodeExecutorStatePiece bytesPiece; + private final PcodeExecutorStatePiece statePiece; + private final PcodeExecutorStatePiece addressPiece; + private final PcodeExecutorStatePiece readsPiece; + + private final PcodeArithmetic arithmetic; + + public WatchValuePcodeExecutorStatePiece( + PcodeExecutorStatePiece bytesPiece, + PcodeExecutorStatePiece statePiece, + PcodeExecutorStatePiece addressPiece, + PcodeExecutorStatePiece readsPiece) { + this.bytesPiece = bytesPiece; + this.statePiece = statePiece; + this.addressPiece = addressPiece; + this.readsPiece = readsPiece; + this.arithmetic = WatchValuePcodeArithmetic.forLanguage(bytesPiece.getLanguage()); + } + + @Override + public Language getLanguage() { + return bytesPiece.getLanguage(); + } + + @Override + public PcodeArithmetic getAddressArithmetic() { + return bytesPiece.getAddressArithmetic(); + } + + @Override + public PcodeArithmetic getArithmetic() { + return arithmetic; + } + + @Override + public void setVar(AddressSpace space, byte[] offset, int size, boolean quantize, + WatchValue val) { + bytesPiece.setVar(space, offset, size, quantize, val.bytes); + statePiece.setVar(space, offset, size, quantize, val.state); + addressPiece.setVar(space, offset, size, quantize, val.address); + readsPiece.setVar(space, offset, size, quantize, val.reads); + } + + @Override + public WatchValue getVar(AddressSpace space, byte[] offset, int size, boolean quantize, + Reason reason) { + return new WatchValue( + bytesPiece.getVar(space, offset, size, quantize, reason), + statePiece.getVar(space, offset, size, quantize, reason), + addressPiece.getVar(space, offset, size, quantize, reason), + readsPiece.getVar(space, offset, size, quantize, reason)); + } + + @Override + public MemBuffer getConcreteBuffer(Address address, Purpose purpose) { + return bytesPiece.getConcreteBuffer(address, purpose); + } + + @Override + public void clear() { + bytesPiece.clear(); + statePiece.clear(); + addressPiece.clear(); + readsPiece.clear(); + } + } + + public static class WatchValuePcodeExecutorState implements PcodeExecutorState { + private WatchValuePcodeExecutorStatePiece piece; + + public WatchValuePcodeExecutorState(WatchValuePcodeExecutorStatePiece piece) { + this.piece = piece; + } + + @Override + public Language getLanguage() { + return piece.getLanguage(); + } + + @Override + public PcodeArithmetic getArithmetic() { + return piece.arithmetic; + } + + @Override + public void setVar(AddressSpace space, WatchValue offset, int size, boolean quantize, + WatchValue val) { + piece.setVar(space, offset.bytes, size, quantize, val); + } + + @Override + public WatchValue getVar(AddressSpace space, WatchValue offset, int size, boolean quantize, + Reason reason) { + return piece.getVar(space, offset.bytes, size, quantize, reason); + } + + @Override + public MemBuffer getConcreteBuffer(Address address, Purpose purpose) { + return piece.getConcreteBuffer(address, purpose); + } + + @Override + public void clear() { + piece.clear(); + } + } + + public static WatchValuePcodeExecutorState buildWatchState(PluginTool tool, + DebuggerCoordinates coordinates) { + PcodeTraceDataAccess data = new DefaultPcodeTraceAccess(coordinates.getPlatform(), + coordinates.getViewSnap(), coordinates.getSnap()) + .getDataForThreadState(coordinates.getThread(), coordinates.getFrame()); + PcodeExecutorState bytesState = executorStateForCoordinates(tool, coordinates); + return new WatchValuePcodeExecutorState(new WatchValuePcodeExecutorStatePiece( + bytesState, + new TraceMemoryStatePcodeExecutorStatePiece(data), + new AddressOfPcodeExecutorStatePiece(data.getLanguage()), + new AddressesReadTracePcodeExecutorStatePiece(data))); + } + + /** + * Build an executor that can compute watch values + * + *

    + * This computes the concrete value, its state, its address, and the set of physical addresses + * involved in the computation. CAUTION: This executor's state will attempt to read live + * machine state, if applicable. Use the executor in a background thread to avoid locking the + * GUI. + * + * @param tool this plugin tool + * @param coordinates the coordinates providing context for the evaluation + * @return an executor for evaluating the watch + */ + public static PcodeExecutor buildWatchExecutor(PluginTool tool, + DebuggerCoordinates coordinates) { + TracePlatform platform = coordinates.getPlatform(); + Language language = platform.getLanguage(); + if (!(language instanceof SleighLanguage slang)) { + throw new IllegalArgumentException("Watch expressions require a Sleigh language"); + } + WatchValuePcodeExecutorState state = buildWatchState(tool, coordinates); + return new PcodeExecutor<>(slang, state.getArithmetic(), state, Reason.INSPECT); + } } 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 a3b1acea2c..9ea8017be6 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 @@ -486,20 +486,13 @@ public abstract class AbstractGhidraHeadedDebuggerGUITest } protected static LocationTrackingSpec getLocationTrackingSpec(String name) { - return LocationTrackingSpec.fromConfigName(name); + return LocationTrackingSpecFactory.fromConfigName(name); } protected static AutoReadMemorySpec getAutoReadMemorySpec(String name) { return AutoReadMemorySpec.fromConfigName(name); } - protected final LocationTrackingSpec trackNone = - getLocationTrackingSpec(NoneLocationTrackingSpec.CONFIG_NAME); - protected final LocationTrackingSpec trackPc = - getLocationTrackingSpec(PCLocationTrackingSpec.CONFIG_NAME); - protected final LocationTrackingSpec trackSp = - getLocationTrackingSpec(SPLocationTrackingSpec.CONFIG_NAME); - protected final AutoReadMemorySpec readNone = getAutoReadMemorySpec(NoneAutoReadMemorySpec.CONFIG_NAME); protected final AutoReadMemorySpec readVisible = diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProviderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProviderTest.java index f148cf05eb..f1c2fd00f8 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProviderTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProviderTest.java @@ -39,8 +39,7 @@ import ghidra.app.plugin.core.debug.DebuggerCoordinates; import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest; import ghidra.app.plugin.core.debug.gui.DebuggerResources; import ghidra.app.plugin.core.debug.gui.DebuggerResources.FollowsCurrentThreadAction; -import ghidra.app.plugin.core.debug.gui.action.AutoReadMemorySpec; -import ghidra.app.plugin.core.debug.gui.action.DebuggerGoToDialog; +import ghidra.app.plugin.core.debug.gui.action.*; import ghidra.app.plugin.core.debug.gui.console.DebuggerConsolePlugin; import ghidra.app.plugin.core.debug.gui.console.DebuggerConsoleProvider.BoundAction; import ghidra.app.plugin.core.debug.gui.console.DebuggerConsoleProvider.LogRow; @@ -271,7 +270,8 @@ public class DebuggerListingProviderTest extends AbstractGhidraHeadedDebuggerGUI TraceThread thread1; TraceThread thread2; DebuggerListingProvider extraProvider = SwingExecutorService.LATER - .submit(() -> listingPlugin.createListingIfMissing(trackPc, true)) + .submit(() -> listingPlugin.createListingIfMissing(PCLocationTrackingSpec.INSTANCE, + true)) .get(); try (UndoableTransaction tid = tb.startTransaction()) { DBTraceMemoryManager memory = tb.trace.getMemoryManager(); @@ -330,7 +330,7 @@ public class DebuggerListingProviderTest extends AbstractGhidraHeadedDebuggerGUI //Pre-check assertEquals(tb.addr(0x00400000), listingProvider.getLocation().getAddress()); - listingProvider.setTrackingSpec(trackSp); + listingProvider.setTrackingSpec(SPLocationTrackingSpec.INSTANCE); waitForSwing(); ProgramLocation loc = listingProvider.getLocation(); @@ -340,7 +340,7 @@ public class DebuggerListingProviderTest extends AbstractGhidraHeadedDebuggerGUI @Test public void testFollowsCurrentTraceOnTraceChangeWithoutRegisterTracking() throws Exception { - listingProvider.setTrackingSpec(trackNone); + listingProvider.setTrackingSpec(NoneLocationTrackingSpec.INSTANCE); try ( // ToyDBTraceBuilder b1 = new ToyDBTraceBuilder(name.getMethodName() + "_1", LANGID_TOYBE64); // @@ -396,7 +396,7 @@ public class DebuggerListingProviderTest extends AbstractGhidraHeadedDebuggerGUI @Test public void testFollowsCurrentThreadOnThreadChangeWithoutRegisterTracking() throws Exception { - listingProvider.setTrackingSpec(trackNone); + listingProvider.setTrackingSpec(NoneLocationTrackingSpec.INSTANCE); try ( // ToyDBTraceBuilder b1 = new ToyDBTraceBuilder(name.getMethodName() + "_1", LANGID_TOYBE64); // @@ -735,13 +735,17 @@ public class DebuggerListingProviderTest extends AbstractGhidraHeadedDebuggerGUI assertArrayEquals(zero, buf.array()); runSwing(() -> trace.getProgramView().getMemory().setForceFullView(true)); - goToDyn(addr(trace, 0x55550000)); - waitRecorder(recorder); - buf.clear(); - assertEquals(data.length, - trace.getMemoryManager().getBytes(recorder.getSnap(), addr(trace, 0x55550000), buf)); - assertArrayEquals(data, buf.array()); + waitForPass(noExc(() -> { + goToDyn(addr(trace, 0x55550000)); + waitRecorder(recorder); + + buf.clear(); + assertEquals(data.length, + trace.getMemoryManager() + .getBytes(recorder.getSnap(), addr(trace, 0x55550000), buf)); + assertArrayEquals(data, buf.array()); + })); } @Test @@ -787,7 +791,8 @@ public class DebuggerListingProviderTest extends AbstractGhidraHeadedDebuggerGUI assertEquals("(nowhere)", listingProvider.locationLabel.getText()); DebuggerListingProvider extraProvider = - runSwing(() -> listingPlugin.createListingIfMissing(trackNone, false)); + runSwing(() -> listingPlugin.createListingIfMissing(NoneLocationTrackingSpec.INSTANCE, + false)); waitForSwing(); assertEquals(traceManager.getCurrentView(), extraProvider.getProgram()); assertEquals("(nowhere)", extraProvider.locationLabel.getText()); @@ -877,7 +882,8 @@ public class DebuggerListingProviderTest extends AbstractGhidraHeadedDebuggerGUI waitForSwing(); // Check the default is track pc - assertEquals(trackPc, listingProvider.actionTrackLocation.getCurrentUserData()); + assertEquals(PCLocationTrackingSpec.INSTANCE, + listingProvider.actionTrackLocation.getCurrentUserData()); assertEquals(tb.addr(0x00401234), listingProvider.getLocation().getAddress()); goToDyn(tb.addr(0x00400000)); @@ -888,14 +894,16 @@ public class DebuggerListingProviderTest extends AbstractGhidraHeadedDebuggerGUI performAction(listingProvider.actionTrackLocation); assertEquals(tb.addr(0x00401234), listingProvider.getLocation().getAddress()); - setActionStateWithTrigger(listingProvider.actionTrackLocation, trackSp, + setActionStateWithTrigger(listingProvider.actionTrackLocation, + SPLocationTrackingSpec.INSTANCE, EventTrigger.GUI_ACTION); waitForSwing(); assertEquals(tb.addr(0x1fff8765), listingProvider.getLocation().getAddress()); - listingProvider.setTrackingSpec(trackNone); + listingProvider.setTrackingSpec(NoneLocationTrackingSpec.INSTANCE); waitForSwing(); - assertEquals(trackNone, listingProvider.actionTrackLocation.getCurrentUserData()); + assertEquals(NoneLocationTrackingSpec.INSTANCE, + listingProvider.actionTrackLocation.getCurrentUserData()); } @Test @@ -1022,7 +1030,8 @@ public class DebuggerListingProviderTest extends AbstractGhidraHeadedDebuggerGUI // NOTE: Action does not exist for main dynamic listing DebuggerListingProvider extraProvider = - runSwing(() -> listingPlugin.createListingIfMissing(trackNone, true)); + runSwing(() -> listingPlugin.createListingIfMissing(NoneLocationTrackingSpec.INSTANCE, + true)); waitForSwing(); assertTrue(extraProvider.actionFollowsCurrentThread.isEnabled()); assertTrue(extraProvider.actionFollowsCurrentThread.isSelected()); @@ -1258,7 +1267,8 @@ public class DebuggerListingProviderTest extends AbstractGhidraHeadedDebuggerGUI @Test public void testActivateTraceChangeLanguage() throws Exception { - assertEquals(trackPc, listingProvider.actionTrackLocation.getCurrentUserData()); + assertEquals(PCLocationTrackingSpec.INSTANCE, + listingProvider.actionTrackLocation.getCurrentUserData()); createSnaplessTrace("x86:LE:64:default"); @@ -1300,7 +1310,8 @@ public class DebuggerListingProviderTest extends AbstractGhidraHeadedDebuggerGUI @Test public void testActivateThreadTracks() throws Exception { - assertEquals(trackPc, listingProvider.actionTrackLocation.getCurrentUserData()); + assertEquals(PCLocationTrackingSpec.INSTANCE, + listingProvider.actionTrackLocation.getCurrentUserData()); createAndOpenTrace(); Register pc = tb.language.getProgramCounter(); @@ -1331,7 +1342,8 @@ public class DebuggerListingProviderTest extends AbstractGhidraHeadedDebuggerGUI @Test public void testActivateSnapTracks() throws Exception { - assertEquals(trackPc, listingProvider.actionTrackLocation.getCurrentUserData()); + assertEquals(PCLocationTrackingSpec.INSTANCE, + listingProvider.actionTrackLocation.getCurrentUserData()); createAndOpenTrace(); Register pc = tb.language.getProgramCounter(); @@ -1359,7 +1371,8 @@ public class DebuggerListingProviderTest extends AbstractGhidraHeadedDebuggerGUI @Test public void testActivateFrameTracks() throws Exception { - assertEquals(trackPc, listingProvider.actionTrackLocation.getCurrentUserData()); + assertEquals(PCLocationTrackingSpec.INSTANCE, + listingProvider.actionTrackLocation.getCurrentUserData()); createAndOpenTrace(); TraceThread thread; @@ -1387,7 +1400,8 @@ public class DebuggerListingProviderTest extends AbstractGhidraHeadedDebuggerGUI @Test public void testRegsPCChangedTracks() throws Exception { - assertEquals(trackPc, listingProvider.actionTrackLocation.getCurrentUserData()); + assertEquals(PCLocationTrackingSpec.INSTANCE, + listingProvider.actionTrackLocation.getCurrentUserData()); createAndOpenTrace(); DBTraceMemoryManager mm = tb.trace.getMemoryManager(); @@ -1417,7 +1431,8 @@ public class DebuggerListingProviderTest extends AbstractGhidraHeadedDebuggerGUI @Test public void testRegsPCChangedTracksDespiteStackWithNoPC() throws Exception { - assertEquals(trackPc, listingProvider.actionTrackLocation.getCurrentUserData()); + assertEquals(PCLocationTrackingSpec.INSTANCE, + listingProvider.actionTrackLocation.getCurrentUserData()); createAndOpenTrace(); DBTraceMemoryManager mm = tb.trace.getMemoryManager(); @@ -1451,7 +1466,8 @@ public class DebuggerListingProviderTest extends AbstractGhidraHeadedDebuggerGUI @Test public void testStackPCChangedTracks() throws Exception { - assertEquals(trackPc, listingProvider.actionTrackLocation.getCurrentUserData()); + assertEquals(PCLocationTrackingSpec.INSTANCE, + listingProvider.actionTrackLocation.getCurrentUserData()); createAndOpenTrace(); DBTraceStackManager sm = tb.trace.getStackManager(); 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 282db53510..03125b39d6 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 @@ -48,7 +48,7 @@ import ghidra.app.plugin.core.debug.DebuggerCoordinates; import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest; import ghidra.app.plugin.core.debug.gui.DebuggerResources; import ghidra.app.plugin.core.debug.gui.DebuggerResources.FollowsCurrentThreadAction; -import ghidra.app.plugin.core.debug.gui.action.DebuggerGoToDialog; +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; @@ -248,7 +248,8 @@ public class DebuggerMemoryBytesProviderTest extends AbstractGhidraHeadedDebugge TraceThread thread1; TraceThread thread2; DebuggerMemoryBytesProvider extraProvider = SwingExecutorService.LATER - .submit(() -> memBytesPlugin.createViewerIfMissing(trackPc, true)) + .submit(() -> memBytesPlugin.createViewerIfMissing(PCLocationTrackingSpec.INSTANCE, + true)) .get(); try (UndoableTransaction tid = tb.startTransaction()) { DBTraceMemoryManager memory = tb.trace.getMemoryManager(); @@ -307,7 +308,7 @@ public class DebuggerMemoryBytesProviderTest extends AbstractGhidraHeadedDebugge //Pre-check assertNull(memBytesProvider.getLocation()); - runSwing(() -> memBytesProvider.setTrackingSpec(trackSp)); + runSwing(() -> memBytesProvider.setTrackingSpec(SPLocationTrackingSpec.INSTANCE)); ProgramLocation loc = memBytesProvider.getLocation(); assertEquals(tb.trace.getProgramView(), loc.getProgram()); @@ -316,7 +317,7 @@ public class DebuggerMemoryBytesProviderTest extends AbstractGhidraHeadedDebugge @Test public void testFollowsCurrentTraceOnTraceChangeWithoutRegisterTracking() throws Exception { - runSwing(() -> memBytesProvider.setTrackingSpec(trackNone)); + runSwing(() -> memBytesProvider.setTrackingSpec(NoneLocationTrackingSpec.INSTANCE)); try ( // ToyDBTraceBuilder b1 = new ToyDBTraceBuilder(name.getMethodName() + "_1", LANGID_TOYBE64); // @@ -367,7 +368,7 @@ public class DebuggerMemoryBytesProviderTest extends AbstractGhidraHeadedDebugge @Test public void testFollowsCurrentThreadOnThreadChangeWithoutRegisterTracking() throws Exception { - runSwing(() -> memBytesProvider.setTrackingSpec(trackNone)); + runSwing(() -> memBytesProvider.setTrackingSpec(NoneLocationTrackingSpec.INSTANCE)); try ( // ToyDBTraceBuilder b1 = new ToyDBTraceBuilder(name.getMethodName() + "_1", LANGID_TOYBE64); // @@ -578,7 +579,8 @@ public class DebuggerMemoryBytesProviderTest extends AbstractGhidraHeadedDebugge assertEquals("(nowhere)", memBytesProvider.locationLabel.getText()); DebuggerMemoryBytesProvider extraProvider = - runSwing(() -> memBytesPlugin.createViewerIfMissing(trackNone, false)); + runSwing(() -> memBytesPlugin.createViewerIfMissing(NoneLocationTrackingSpec.INSTANCE, + false)); waitForSwing(); assertEquals(traceManager.getCurrentView(), extraProvider.getProgram()); assertEquals("(nowhere)", extraProvider.locationLabel.getText()); @@ -670,7 +672,8 @@ public class DebuggerMemoryBytesProviderTest extends AbstractGhidraHeadedDebugge waitForSwing(); // Check the default is track pc - assertEquals(trackPc, memBytesProvider.actionTrackLocation.getCurrentUserData()); + assertEquals(PCLocationTrackingSpec.INSTANCE, + memBytesProvider.actionTrackLocation.getCurrentUserData()); assertEquals(tb.addr(0x00401234), memBytesProvider.getLocation().getAddress()); goToDyn(tb.addr(0x00400000)); @@ -681,13 +684,15 @@ public class DebuggerMemoryBytesProviderTest extends AbstractGhidraHeadedDebugge performAction(memBytesProvider.actionTrackLocation); assertEquals(tb.addr(0x00401234), memBytesProvider.getLocation().getAddress()); - setActionStateWithTrigger(memBytesProvider.actionTrackLocation, trackSp, + setActionStateWithTrigger(memBytesProvider.actionTrackLocation, + SPLocationTrackingSpec.INSTANCE, EventTrigger.GUI_ACTION); waitForSwing(); assertEquals(tb.addr(0x1fff8765), memBytesProvider.getLocation().getAddress()); - runSwing(() -> memBytesProvider.setTrackingSpec(trackNone)); - assertEquals(trackNone, memBytesProvider.actionTrackLocation.getCurrentUserData()); + runSwing(() -> memBytesProvider.setTrackingSpec(NoneLocationTrackingSpec.INSTANCE)); + assertEquals(NoneLocationTrackingSpec.INSTANCE, + memBytesProvider.actionTrackLocation.getCurrentUserData()); } @Test @@ -721,7 +726,8 @@ public class DebuggerMemoryBytesProviderTest extends AbstractGhidraHeadedDebugge // NOTE: Action does not exist for main dynamic listing DebuggerMemoryBytesProvider extraProvider = - runSwing(() -> memBytesPlugin.createViewerIfMissing(trackNone, true)); + runSwing(() -> memBytesPlugin.createViewerIfMissing(NoneLocationTrackingSpec.INSTANCE, + true)); waitForSwing(); assertTrue(extraProvider.actionFollowsCurrentThread.isEnabled()); assertTrue(extraProvider.actionFollowsCurrentThread.isSelected()); @@ -902,7 +908,8 @@ public class DebuggerMemoryBytesProviderTest extends AbstractGhidraHeadedDebugge @Test public void testActivateThreadTracks() throws Exception { - assertEquals(trackPc, memBytesProvider.actionTrackLocation.getCurrentUserData()); + assertEquals(PCLocationTrackingSpec.INSTANCE, + memBytesProvider.actionTrackLocation.getCurrentUserData()); createAndOpenTrace(); Register pc = tb.language.getProgramCounter(); @@ -933,7 +940,8 @@ public class DebuggerMemoryBytesProviderTest extends AbstractGhidraHeadedDebugge @Test public void testActivateSnapTracks() throws Exception { - assertEquals(trackPc, memBytesProvider.actionTrackLocation.getCurrentUserData()); + assertEquals(PCLocationTrackingSpec.INSTANCE, + memBytesProvider.actionTrackLocation.getCurrentUserData()); createAndOpenTrace(); Register pc = tb.language.getProgramCounter(); @@ -961,7 +969,8 @@ public class DebuggerMemoryBytesProviderTest extends AbstractGhidraHeadedDebugge @Test public void testActivateFrameTracks() throws Exception { - assertEquals(trackPc, memBytesProvider.actionTrackLocation.getCurrentUserData()); + assertEquals(PCLocationTrackingSpec.INSTANCE, + memBytesProvider.actionTrackLocation.getCurrentUserData()); createAndOpenTrace(); TraceThread thread; @@ -989,7 +998,8 @@ public class DebuggerMemoryBytesProviderTest extends AbstractGhidraHeadedDebugge @Test public void testRegsPCChangedTracks() throws Exception { - assertEquals(trackPc, memBytesProvider.actionTrackLocation.getCurrentUserData()); + assertEquals(PCLocationTrackingSpec.INSTANCE, + memBytesProvider.actionTrackLocation.getCurrentUserData()); createAndOpenTrace(); DBTraceMemoryManager mm = tb.trace.getMemoryManager(); @@ -1019,7 +1029,8 @@ public class DebuggerMemoryBytesProviderTest extends AbstractGhidraHeadedDebugge @Test public void testRegsPCChangedTracksDespiteStackWithNoPC() throws Exception { - assertEquals(trackPc, memBytesProvider.actionTrackLocation.getCurrentUserData()); + assertEquals(PCLocationTrackingSpec.INSTANCE, + memBytesProvider.actionTrackLocation.getCurrentUserData()); createAndOpenTrace(); DBTraceMemoryManager mm = tb.trace.getMemoryManager(); @@ -1053,7 +1064,8 @@ public class DebuggerMemoryBytesProviderTest extends AbstractGhidraHeadedDebugge @Test public void testStackPCChangedTracks() throws Exception { - assertEquals(trackPc, memBytesProvider.actionTrackLocation.getCurrentUserData()); + assertEquals(PCLocationTrackingSpec.INSTANCE, + memBytesProvider.actionTrackLocation.getCurrentUserData()); createAndOpenTrace(); DBTraceStackManager sm = tb.trace.getStackManager(); 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 269a48e68d..b036ff6545 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 @@ -30,7 +30,6 @@ import com.google.common.collect.Range; import generic.test.category.NightlyCategory; import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest; -import ghidra.app.plugin.core.debug.gui.action.LocationTrackingSpec; import ghidra.app.plugin.core.debug.gui.action.NoneLocationTrackingSpec; import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingPlugin; import ghidra.app.plugin.core.debug.gui.register.DebuggerRegistersProvider.RegisterDataSettingsDialog; @@ -766,8 +765,7 @@ public class DebuggerRegistersProviderTest extends AbstractGhidraHeadedDebuggerG addRegisterValues(thread); addRegisterTypes(thread); // Ensure cause is goto PC, not register tracking - listingPlugin.setTrackingSpec( - LocationTrackingSpec.fromConfigName(NoneLocationTrackingSpec.CONFIG_NAME)); + listingPlugin.setTrackingSpec(NoneLocationTrackingSpec.INSTANCE); activateThread(thread); waitForSwing(); 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 cd7920221a..4257c6928b 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 @@ -22,7 +22,6 @@ import java.nio.ByteBuffer; import java.util.*; import java.util.stream.Collectors; -import org.apache.commons.lang3.exception.ExceptionUtils; import org.junit.*; import com.google.common.collect.Range; @@ -135,7 +134,7 @@ public class DebuggerWatchesProviderTest extends AbstractGhidraHeadedDebuggerGUI traceManager.openTrace(tb.trace); traceManager.activateThread(thread); - waitForSwing(); + waitForWatches(); assertEquals("0x400000", row.getRawValueString()); assertEquals("", row.getValueString()); // NB. No data type set @@ -153,8 +152,9 @@ public class DebuggerWatchesProviderTest extends AbstractGhidraHeadedDebuggerGUI row.setExpression("r0"); setRegisterValues(thread); + waitForWatches(); - waitForPass(() -> assertEquals("0x400000", row.getRawValueString())); + assertEquals("0x400000", row.getRawValueString()); assertNoErr(row); } @@ -169,7 +169,7 @@ public class DebuggerWatchesProviderTest extends AbstractGhidraHeadedDebuggerGUI traceManager.openTrace(tb.trace); traceManager.activateThread(thread); - waitForSwing(); + waitForWatches(); assertEquals("0x400000", row.getRawValueString()); assertEquals("400000h", row.getValueString()); @@ -190,7 +190,7 @@ public class DebuggerWatchesProviderTest extends AbstractGhidraHeadedDebuggerGUI traceManager.openTrace(tb.trace); traceManager.activateThread(thread); watchesProvider.watchFilterPanel.setSelectedItem(row); - waitForSwing(); + waitForWatches(); performEnabledAction(watchesProvider, watchesProvider.actionApplyDataType, true); @@ -199,6 +199,12 @@ public class DebuggerWatchesProviderTest extends AbstractGhidraHeadedDebuggerGUI assertEquals(FormatSettingsDefinition.DECIMAL, format.getChoice(u400000)); } + protected void waitForWatches() { + waitForSwing(); + watchesProvider.waitEvaluate(1000); + waitForSwing(); + } + @Test public void testWatchWithDataTypeSettings() { setRegisterValues(thread); @@ -210,7 +216,7 @@ public class DebuggerWatchesProviderTest extends AbstractGhidraHeadedDebuggerGUI traceManager.openTrace(tb.trace); traceManager.activateThread(thread); - waitForSwing(); + waitForWatches(); assertEquals("0x400000", row.getRawValueString()); assertEquals("400000h", row.getValueString()); @@ -236,7 +242,7 @@ public class DebuggerWatchesProviderTest extends AbstractGhidraHeadedDebuggerGUI traceManager.openTrace(tb.trace); traceManager.activateThread(thread); - waitForSwing(); + waitForWatches(); watchesProvider.watchFilterPanel.setSelectedItem(row); waitForSwing(); @@ -263,7 +269,7 @@ public class DebuggerWatchesProviderTest extends AbstractGhidraHeadedDebuggerGUI traceManager.openTrace(tb.trace); traceManager.activateThread(thread); - waitForSwing(); + waitForWatches(); assertEquals("0xdeadbeef", row.getRawValueString()); assertEquals("DEADBEEFh", row.getValueString()); @@ -285,7 +291,7 @@ public class DebuggerWatchesProviderTest extends AbstractGhidraHeadedDebuggerGUI traceManager.openTrace(tb.trace); traceManager.activateThread(thread); - waitForSwing(); + waitForWatches(); assertEquals("0x400008", row.getRawValueString()); assertEquals("400008h", row.getValueString()); @@ -315,7 +321,7 @@ public class DebuggerWatchesProviderTest extends AbstractGhidraHeadedDebuggerGUI traceManager.openTrace(trace); traceManager.activateThread(thread); - waitForSwing(); + waitForWatches(); // Verify no target read has occurred yet TraceMemorySpace regs = @@ -331,15 +337,11 @@ public class DebuggerWatchesProviderTest extends AbstractGhidraHeadedDebuggerGUI WatchRow row = Unique.assertOne(watchesProvider.watchTableModel.getModelData()); row.setExpression("*:4 r0"); row.setDataType(LongDataType.dataType); + waitForWatches(); - waitForPass(() -> { - if (row.getError() != null) { - ExceptionUtils.rethrow(row.getError()); - } - assertEquals("{ 01 02 03 04 }", row.getRawValueString()); - assertEquals("1020304h", row.getValueString()); - }); assertNoErr(row); + assertEquals("{ 01 02 03 04 }", row.getRawValueString()); + assertEquals("1020304h", row.getValueString()); } protected void runTestIsEditableEmu(String expression, boolean expectWritable) { @@ -353,7 +355,7 @@ public class DebuggerWatchesProviderTest extends AbstractGhidraHeadedDebuggerGUI traceManager.openTrace(tb.trace); traceManager.activateThread(thread); editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_EMULATOR); - waitForSwing(); + waitForWatches(); assertNoErr(row); assertFalse(row.isRawValueEditable()); @@ -387,6 +389,7 @@ public class DebuggerWatchesProviderTest extends AbstractGhidraHeadedDebuggerGUI traceManager.openTrace(tb.trace); traceManager.activateThread(thread); editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_EMULATOR); + waitForWatches(); performAction(watchesProvider.actionEnableEdits); @@ -554,6 +557,8 @@ public class DebuggerWatchesProviderTest extends AbstractGhidraHeadedDebuggerGUI performAction(watchesProvider.actionAdd); WatchRow row = Unique.assertOne(watchesProvider.watchTableModel.getModelData()); row.setExpression(expression); + waitForWatches(); + performAction(watchesProvider.actionEnableEdits); return row; @@ -731,7 +736,7 @@ public class DebuggerWatchesProviderTest extends AbstractGhidraHeadedDebuggerGUI rowR0.setDataType(PointerDataType.dataType); registersProvider.setSelectedRow(rowR0); }); - waitForSwing(); + waitForWatches(); performEnabledAction(registersProvider, watchesProvider.actionAddFromRegister, true); @@ -760,7 +765,7 @@ public class DebuggerWatchesProviderTest extends AbstractGhidraHeadedDebuggerGUI row.setExpression("*:8 r0"); traceManager.activateThread(thread); - waitForSwing(); + waitForWatches(); assertEquals(symbol, row.getSymbol()); } diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/pcode/exec/TraceRecorderPcodeExecTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/pcode/exec/TraceRecorderPcodeExecTest.java index e7bd20feba..6647c15bf8 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/pcode/exec/TraceRecorderPcodeExecTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/pcode/exec/TraceRecorderPcodeExecTest.java @@ -34,8 +34,13 @@ import ghidra.pcode.exec.PcodeExecutorStatePiece.Reason; import ghidra.pcode.exec.trace.DirectBytesTracePcodeExecutorState; import ghidra.pcode.utils.Utils; import ghidra.program.model.lang.Register; +import ghidra.program.model.lang.RegisterValue; import ghidra.trace.model.Trace; +import ghidra.trace.model.memory.TraceMemorySpace; import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.model.time.TraceSnapshot; +import ghidra.trace.model.time.schedule.TraceSchedule; +import ghidra.util.database.UndoableTransaction; /** * Test the {@link DirectBytesTracePcodeExecutorState} in combination with @@ -86,6 +91,59 @@ public class TraceRecorderPcodeExecTest extends AbstractGhidraHeadedDebuggerGUIT assertEquals(11, Utils.bytesToLong(result, result.length, language.isBigEndian())); } + @Test + public void testExecutorEvalInScratchReadsLive() throws Throwable { + createTestModel(); + mb.createTestProcessesAndThreads(); + + mb.testProcess1.regs.addRegistersFromLanguage(getToyBE64Language(), + Register::isBaseRegister); + TestTargetRegisterBankInThread regs = mb.testThread1.addRegisterBank(); + waitOn(regs.writeRegistersNamed(Map.of( + "r0", new byte[] { 5 }, + "r1", new byte[] { 6 }))); + + TraceRecorder recorder = modelService.recordTarget(mb.testProcess1, + createTargetTraceMapper(mb.testProcess1), ActionSource.AUTOMATIC); + + TraceThread thread = waitForValue(() -> recorder.getTraceThread(mb.testThread1)); + Trace trace = recorder.getTrace(); + SleighLanguage language = (SleighLanguage) trace.getBaseLanguage(); + + PcodeExpression expr = SleighProgramCompiler + .compileExpression(language, "r0 + r1"); + + Register r0 = language.getRegister("r0"); + Register r1 = language.getRegister("r1"); + waitForPass(() -> { + // TODO: A little brittle: Depends on a specific snap advancement strategy + assertEquals(3, trace.getTimeManager().getSnapshotCount()); + DebuggerRegisterMapper rm = recorder.getRegisterMapper(thread); + assertNotNull(rm); + assertNotNull(rm.getTargetRegister("r0")); + assertNotNull(rm.getTargetRegister("r1")); + assertTrue(rm.getRegistersOnTarget().contains(r0)); + assertTrue(rm.getRegistersOnTarget().contains(r1)); + }); + + TraceSchedule oneTick = TraceSchedule.snap(recorder.getSnap()).steppedForward(thread, 1); + try (UndoableTransaction tid = UndoableTransaction.start(trace, "Scratch")) { + TraceSnapshot scratch = trace.getTimeManager().getSnapshot(Long.MIN_VALUE, true); + scratch.setSchedule(oneTick); + scratch.setDescription("Faked"); + + TraceMemorySpace space = trace.getMemoryManager().getMemoryRegisterSpace(thread, true); + space.setValue(scratch.getKey(), new RegisterValue(r0, BigInteger.valueOf(10))); + } + + PcodeExecutor executor = DebuggerPcodeUtils.executorForCoordinates(tool, + DebuggerCoordinates.NOWHERE.recorder(recorder).thread(thread).time(oneTick)); + + // In practice, this should be backgrounded, but we're in a test thread + byte[] result = expr.evaluate(executor); + assertEquals(16, Utils.bytesToLong(result, result.length, language.isBigEndian())); + } + @Test public void testExecutorWrite() throws Throwable { createTestModel(); diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/AddressesReadTracePcodeExecutorStatePiece.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/AddressesReadTracePcodeExecutorStatePiece.java new file mode 100644 index 0000000000..415e580eb1 --- /dev/null +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/AddressesReadTracePcodeExecutorStatePiece.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.pcode.exec.trace; + +import java.util.HashMap; +import java.util.Map; + +import javax.help.UnsupportedOperationException; + +import ghidra.pcode.exec.*; +import ghidra.pcode.exec.PcodeArithmetic.Purpose; +import ghidra.pcode.exec.trace.data.PcodeTraceDataAccess; +import ghidra.program.model.address.*; +import ghidra.program.model.mem.MemBuffer; + +/** + * An auxilliary state piece that reports the (trace) address ranges + * + *

    + * Except for unique spaces, sets are ignored, and gets simply echo back the range of addresses of + * the requested read. In unique spaces, the "addresses read" is treated as the value, so that + * values transiting unique space can correct have their source address ranges reported. Use this + * with {@link AddressesReadPcodeArithmetic} to compute the union of these ranges during Sleigh + * expression evaluation. The ranges are translated from the guest platform, if applicable, to the + * trace address. In the case of registers, the addresses are also translated to the appropriate + * overlay space, if applicable. + */ +public class AddressesReadTracePcodeExecutorStatePiece + extends AbstractLongOffsetPcodeExecutorStatePiece + implements TracePcodeExecutorStatePiece { + + protected final PcodeTraceDataAccess data; + private final Map unique = new HashMap<>(); + + /** + * Construct the state piece + * + * @param data the trace data access shim + */ + public AddressesReadTracePcodeExecutorStatePiece(PcodeTraceDataAccess data) { + super(data.getLanguage(), BytesPcodeArithmetic.forLanguage(data.getLanguage()), + AddressesReadPcodeArithmetic.INSTANCE); + this.data = data; + } + + @Override + public MemBuffer getConcreteBuffer(Address address, Purpose purpose) { + throw new ConcretionError("Cannot make 'addresses read' concrete buffers", purpose); + } + + @Override + public PcodeTraceDataAccess getData() { + return data; + } + + @Override + public void writeDown(PcodeTraceDataAccess into) { + throw new UnsupportedOperationException(); + } + + @Override + protected AddressSpace getForSpace(AddressSpace space, boolean toWrite) { + return space; + } + + @Override + protected void setInSpace(AddressSpace space, long offset, int size, AddressSetView val) { + if (!space.isUniqueSpace()) { + return; + } + // TODO: size is not considered + unique.put(offset, val); + } + + @Override + protected AddressSetView getFromSpace(AddressSpace space, long offset, int size, + Reason reason) { + if (space.isUniqueSpace()) { + AddressSetView result = unique.get(offset); + if (result == null) { + return new AddressSet(); + } + return result; + } + Address start = data.translate(space.getAddress(offset)); + if (start == null) { + return new AddressSet(); + } + try { + return new AddressSet(new AddressRangeImpl(start, size)); + } + catch (AddressOverflowException e) { + throw new AssertionError(e); + } + } + + @Override + public void clear() { + unique.clear(); + } +} diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/DirectBytesTracePcodeExecutorStatePiece.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/DirectBytesTracePcodeExecutorStatePiece.java index 39059c08c0..195e634797 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/DirectBytesTracePcodeExecutorStatePiece.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/DirectBytesTracePcodeExecutorStatePiece.java @@ -67,7 +67,7 @@ public class DirectBytesTracePcodeExecutorStatePiece * * @param data the trace-data access shim */ - protected DirectBytesTracePcodeExecutorStatePiece(PcodeTraceDataAccess data) { + public DirectBytesTracePcodeExecutorStatePiece(PcodeTraceDataAccess data) { this(BytesPcodeArithmetic.forLanguage(data.getLanguage()), data); } @@ -138,4 +138,9 @@ public class DirectBytesTracePcodeExecutorStatePiece public void writeDown(PcodeTraceDataAccess into) { // Writes directly, so just ignore } + + @Override + public void clear() { + unique.clear(); + } } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/TraceMemoryStatePcodeExecutorStatePiece.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/TraceMemoryStatePcodeExecutorStatePiece.java index 02ae1be60f..e9ca1f7c45 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/TraceMemoryStatePcodeExecutorStatePiece.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/TraceMemoryStatePcodeExecutorStatePiece.java @@ -120,4 +120,9 @@ public class TraceMemoryStatePcodeExecutorStatePiece extends public MemBuffer getConcreteBuffer(Address address, Purpose purpose) { throw new ConcretionError("Cannot make TraceMemoryState into a concrete buffer", purpose); } + + @Override + public void clear() { + unique.clear(); + } } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/data/AbstractPcodeTraceDataAccess.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/data/AbstractPcodeTraceDataAccess.java index dfb24dc234..8176d97cd6 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/data/AbstractPcodeTraceDataAccess.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/data/AbstractPcodeTraceDataAccess.java @@ -51,6 +51,11 @@ public abstract class AbstractPcodeTraceDataAccess implements InternalPcodeTrace this.mm = platform.getTrace().getMemoryManager(); } + @Override + public TraceTimeViewport getViewport() { + return viewport; + } + @Override public Language getLanguage() { return platform.getLanguage(); @@ -186,6 +191,15 @@ public abstract class AbstractPcodeTraceDataAccess implements InternalPcodeTrace return ops.getViewBytes(snap, toOverlay(hostStart), buf); } + @Override + public Address translate(Address address) { + Address host = platform.mapGuestToHost(address); + if (host == null) { + return null; + } + return toOverlay(host); + } + @Override public PcodeTracePropertyAccess getPropertyAccess(String name, Class type) { return new DefaultPcodeTracePropertyAccess<>(this, name, type); diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/data/DefaultPcodeTraceAccess.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/data/DefaultPcodeTraceAccess.java index 15133d25c1..fc06a9659f 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/data/DefaultPcodeTraceAccess.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/data/DefaultPcodeTraceAccess.java @@ -46,12 +46,12 @@ public class DefaultPcodeTraceAccess extends AbstractPcodeTraceAccess // } @Override - public DefaultPcodeTraceMemoryAccess newDataForSharedState() { + protected DefaultPcodeTraceMemoryAccess newDataForSharedState() { return new DefaultPcodeTraceMemoryAccess(platform, snap, viewport); } @Override - public DefaultPcodeTraceRegistersAccess newDataForLocalState(TraceThread thread, int frame) { + protected DefaultPcodeTraceRegistersAccess newDataForLocalState(TraceThread thread, int frame) { return new DefaultPcodeTraceRegistersAccess(platform, snap, thread, frame, viewport); } } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/data/DefaultPcodeTraceThreadAccess.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/data/DefaultPcodeTraceThreadAccess.java index 31ab70302b..ab04af2030 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/data/DefaultPcodeTraceThreadAccess.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/data/DefaultPcodeTraceThreadAccess.java @@ -101,6 +101,14 @@ public class DefaultPcodeTraceThreadAccess return memory.getBytes(start, buf); } + @Override + public Address translate(Address address) { + if (address.isRegisterAddress()) { + return registers.translate(address); + } + return memory.translate(address); + } + @Override public PcodeTracePropertyAccess getPropertyAccess(String name, Class type) { throw new UnsupportedOperationException("This is meant for p-code executor use"); diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/data/InternalPcodeTraceDataAccess.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/data/InternalPcodeTraceDataAccess.java index ad349ff26a..708f365df6 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/data/InternalPcodeTraceDataAccess.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/data/InternalPcodeTraceDataAccess.java @@ -16,6 +16,7 @@ package ghidra.pcode.exec.trace.data; import ghidra.lifecycle.Internal; +import ghidra.trace.model.TraceTimeViewport; import ghidra.trace.model.guest.TracePlatform; import ghidra.trace.model.property.TracePropertyMapOperations; @@ -27,4 +28,6 @@ public interface InternalPcodeTraceDataAccess extends PcodeTraceDataAccess { TracePropertyMapOperations getPropertyOps(String name, Class type, boolean createIfAbsent); + + TraceTimeViewport getViewport(); } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/data/PcodeTraceAccess.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/data/PcodeTraceAccess.java index 3bd560bec7..d0c59b5917 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/data/PcodeTraceAccess.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/data/PcodeTraceAccess.java @@ -106,6 +106,18 @@ public interface PcodeTraceAccess { */ PcodeTraceRegistersAccess getDataForLocalState(TraceThread thread, int frame); + /** + * Construct a new trace thread data-access shim + * + * @param shared the shared (memory) state + * @param local the local (register) state + * @return the thread data-access shim + */ + default PcodeTraceDataAccess newPcodeTraceThreadAccess(PcodeTraceMemoryAccess shared, + PcodeTraceRegistersAccess local) { + return new DefaultPcodeTraceThreadAccess(shared, local); + } + /** * Get the data-access shim for use in an executor having thread context * @@ -123,7 +135,7 @@ public interface PcodeTraceAccess { if (thread == null) { return getDataForSharedState(); } - return new DefaultPcodeTraceThreadAccess(getDataForSharedState(), + return newPcodeTraceThreadAccess(getDataForSharedState(), getDataForLocalState(thread, frame)); } } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/data/PcodeTraceDataAccess.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/data/PcodeTraceDataAccess.java index 41171e4030..0c9820544e 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/data/PcodeTraceDataAccess.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/data/PcodeTraceDataAccess.java @@ -123,6 +123,14 @@ public interface PcodeTraceDataAccess { */ int getBytes(Address start, ByteBuffer buf); + /** + * Translate the given emulator address to a host/overlay address + * + * @param address the emulator address + * @return the host/overlay address + */ + Address translate(Address address); + /** * Get a property-access shim for the named property * diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/DBTraceTimeViewport.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/DBTraceTimeViewport.java index 9a06a946a7..5fd70fbce1 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/DBTraceTimeViewport.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/DBTraceTimeViewport.java @@ -291,6 +291,7 @@ public class DBTraceTimeViewport implements TraceTimeViewport { } } + @Override public List> getOrderedSpans() { try (LockHold hold = trace.lockRead()) { synchronized (ordered) { diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/TraceTimeViewport.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/TraceTimeViewport.java index 1f5cb1b385..0d059e0ff6 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/TraceTimeViewport.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/TraceTimeViewport.java @@ -173,6 +173,9 @@ public interface TraceTimeViewport { AddressSet computeVisibleParts(AddressSetView set, Range lifespan, T object, Occlusion occlusion); + + List> getOrderedSpans(); + /** * Get the snaps involved in the view in most-recent-first order * diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/ThreadPcodeExecutorState.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/ThreadPcodeExecutorState.java index 8d7429c291..fec087c579 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/ThreadPcodeExecutorState.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/ThreadPcodeExecutorState.java @@ -112,4 +112,17 @@ public class ThreadPcodeExecutorState implements PcodeExecutorState { public PcodeExecutorState getLocalState() { return localState; } + + /** + * {@inheritDoc} + * + *

    + * This will only clear the thread's local state, lest we invoke clear on the shared state for + * every thread. Instead, if necessary, the machine should clear its local state then clear each + * thread's local state. + */ + @Override + public void clear() { + localState.clear(); + } } diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/AbstractBytesPcodeExecutorStatePiece.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/AbstractBytesPcodeExecutorStatePiece.java index 54c3d384aa..f59267a974 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/AbstractBytesPcodeExecutorStatePiece.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/AbstractBytesPcodeExecutorStatePiece.java @@ -142,4 +142,11 @@ public abstract class AbstractBytesPcodeExecutorStatePiece { return null; } + @Override + public Address fromConst(BigInteger value, int size) { + return null; + } + @Override public Address fromConst(long value, int size) { return null; @@ -80,7 +85,7 @@ public enum AddressOfPcodeArithmetic implements PcodeArithmetic

    { @Override public byte[] toConcrete(Address value, Purpose purpose) { - throw new ConcretionError("Cannot decide branches using 'address of'", purpose); + throw new ConcretionError("Cannot make 'address of' concrete", purpose); } @Override diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/AddressOfPcodeExecutorStatePiece.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/AddressOfPcodeExecutorStatePiece.java index 77e7aaa350..fccad463bd 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/AddressOfPcodeExecutorStatePiece.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/AddressOfPcodeExecutorStatePiece.java @@ -70,6 +70,7 @@ public class AddressOfPcodeExecutorStatePiece if (!space.isUniqueSpace()) { return; } + // TODO: size is not considered long lOffset = addressArithmetic.toLong(offset, Purpose.STORE); unique.put(lOffset, val); } @@ -88,4 +89,9 @@ public class AddressOfPcodeExecutorStatePiece public MemBuffer getConcreteBuffer(Address address, Purpose purpose) { throw new ConcretionError("Cannot make 'address of' concrete buffers", purpose); } + + @Override + public void clear() { + unique.clear(); + } } diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/AddressesReadPcodeArithmetic.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/AddressesReadPcodeArithmetic.java new file mode 100644 index 0000000000..4d3ac04a73 --- /dev/null +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/AddressesReadPcodeArithmetic.java @@ -0,0 +1,91 @@ +/* ### + * 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.math.BigInteger; + +import javax.help.UnsupportedOperationException; + +import ghidra.program.model.address.AddressSet; +import ghidra.program.model.address.AddressSetView; +import ghidra.program.model.lang.Endian; + +/** + * An auxilliary arithmetic that reports the union of all addresses read, typically during the + * evaluation of an expression. + */ +public enum AddressesReadPcodeArithmetic implements PcodeArithmetic { + /** The singleton instance */ + INSTANCE; + + @Override + public Endian getEndian() { + return null; + } + + @Override + public AddressSetView unaryOp(int opcode, int sizeout, int sizein1, AddressSetView in1) { + return in1; + } + + @Override + public AddressSetView binaryOp(int opcode, int sizeout, int sizein1, AddressSetView in1, + int sizein2, AddressSetView in2) { + return in1.union(in2); + } + + @Override + public AddressSetView modBeforeStore(int sizeout, int sizeinAddress, AddressSetView inAddress, + int sizeinValue, AddressSetView inValue) { + return inValue; + } + + @Override + public AddressSetView modAfterLoad(int sizeout, int sizeinAddress, AddressSetView inAddress, + int sizeinValue, AddressSetView inValue) { + return inValue.union(inAddress); + } + + @Override + public AddressSetView fromConst(byte[] value) { + return new AddressSet(); + } + + @Override + public AddressSetView fromConst(BigInteger value, int size, boolean isContextreg) { + return new AddressSet(); + } + + @Override + public AddressSetView fromConst(BigInteger value, int size) { + return new AddressSet(); + } + + @Override + public AddressSetView fromConst(long value, int size) { + return new AddressSet(); + } + + @Override + public byte[] toConcrete(AddressSetView value, Purpose purpose) { + throw new ConcretionError("Cannot make 'addresses read' concrete", purpose); + } + + @Override + public long sizeOf(AddressSetView value) { + throw new UnsupportedOperationException(); + } +} diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/BytesPcodeExecutorStateSpace.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/BytesPcodeExecutorStateSpace.java index 539869423a..cd0355f8dc 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/BytesPcodeExecutorStateSpace.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/BytesPcodeExecutorStateSpace.java @@ -172,4 +172,8 @@ public class BytesPcodeExecutorStateSpace { } return readBytes(offset, size, reason); } + + public void clear() { + bytes.clear(); + } } diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/DefaultPcodeExecutorState.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/DefaultPcodeExecutorState.java index 161386a101..9aea5e3053 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/DefaultPcodeExecutorState.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/DefaultPcodeExecutorState.java @@ -70,4 +70,9 @@ public class DefaultPcodeExecutorState implements PcodeExecutorState { public MemBuffer getConcreteBuffer(Address address, Purpose purpose) { return piece.getConcreteBuffer(address, purpose); } + + @Override + public void clear() { + piece.clear(); + } } diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PairedPcodeExecutorState.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PairedPcodeExecutorState.java index a824ddaaaf..ea69019d56 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PairedPcodeExecutorState.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PairedPcodeExecutorState.java @@ -116,4 +116,9 @@ public class PairedPcodeExecutorState implements PcodeExecutorState * To compose three or more states, first ask if it is really necessary. Second, consider - * implementing the {@link PcodeExecutorStatePiece} interface directly. Third, use the Church-style - * triple. In that third case, the implementor must decide which side has the nested tuple. Putting - * it on the right keeps the concrete piece (conventionally on the left) in the most shallow - * position, so it can be accessed efficiently. However, putting it on the left (implying it's in - * the deepest position) keeps the concrete piece near the other pieces to which it's most closely - * bound. The latter goal is only important when the paired arithmetics mix information between - * their elements. + * implementing the {@link PcodeExecutorStatePiece} interface for a record type. Third, use the + * Church-style triple. In that third case, it is recommended to compose the nested pair on the + * right of the top pair: Compose the two right pieces into a single piece, then use + * {@link PairedPcodeExecutorState} to compose a concrete state with the composed piece, yielding a + * state of triples. This can be applied ad nauseam to compose arbitrarily large tuples; however, at + * a certain point clients should consider creating a record and implementing the state piece and/or + * state interface. It's helpful to use this implementation as a reference. Alternatively, the + * {@code Debugger} module has a {@code WatchValuePcodeExecutorState} which follows this + * recommendation. * * @see PairedPcodeExecutorState * @param the type of offset, usually the type of a controlling state @@ -122,4 +124,10 @@ public class PairedPcodeExecutorStatePiece public PcodeExecutorStatePiece getRight() { return right; } + + @Override + public void clear() { + left.clear(); + right.clear(); + } } diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeExecutorStatePiece.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeExecutorStatePiece.java index a968f3afb3..80ff70d1f1 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeExecutorStatePiece.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeExecutorStatePiece.java @@ -238,4 +238,15 @@ public interface PcodeExecutorStatePiece { default long quantizeOffset(AddressSpace space, long offset) { return space.truncateAddressableWordOffset(offset) * space.getAddressableUnitSize(); } + + /** + * Erase the entire state or piece + * + *

    + * This is generally only useful when the state is itself a cache to another object. This will + * ensure the state is reading from that object rather than a stale cache. If this is not a + * cache, this could in fact clear the whole state, and the machine using it will be left in the + * dark. + */ + void clear(); } diff --git a/Ghidra/Debug/TaintAnalysis/src/main/java/ghidra/pcode/emu/taint/AbstractTaintPcodeExecutorStatePiece.java b/Ghidra/Debug/TaintAnalysis/src/main/java/ghidra/pcode/emu/taint/AbstractTaintPcodeExecutorStatePiece.java index f770b3aa40..2fe8b9e30d 100644 --- a/Ghidra/Debug/TaintAnalysis/src/main/java/ghidra/pcode/emu/taint/AbstractTaintPcodeExecutorStatePiece.java +++ b/Ghidra/Debug/TaintAnalysis/src/main/java/ghidra/pcode/emu/taint/AbstractTaintPcodeExecutorStatePiece.java @@ -115,4 +115,11 @@ public abstract class AbstractTaintPcodeExecutorStatePiece protected TaintVec getFromSpace(S space, long offset, int size, Reason reason) { return space.get(offset, size); } + + @Override + public void clear() { + for (S space : spaceMap.values()) { + space.clear(); + } + } } diff --git a/Ghidra/Debug/TaintAnalysis/src/main/java/ghidra/pcode/emu/taint/plain/TaintSpace.java b/Ghidra/Debug/TaintAnalysis/src/main/java/ghidra/pcode/emu/taint/plain/TaintSpace.java index adaad69561..fe2dc0d7b5 100644 --- a/Ghidra/Debug/TaintAnalysis/src/main/java/ghidra/pcode/emu/taint/plain/TaintSpace.java +++ b/Ghidra/Debug/TaintAnalysis/src/main/java/ghidra/pcode/emu/taint/plain/TaintSpace.java @@ -62,9 +62,9 @@ public class TaintSpace { * Retrieve the taint sets for the variable at the given offset * *

    - * This retrieves as many taint sets as there are elements in the given buffer vector. This first - * element becomes the taint set at the given offset, then each subsequent element becomes the - * taint set at each subsequent offset until the vector is filled. This is analogous to the + * This retrieves as many taint sets as there are elements in the given buffer vector. This + * first element becomes the taint set at the given offset, then each subsequent element becomes + * the taint set at each subsequent offset until the vector is filled. This is analogous to the * manner in which bytes would be "read" from concrete state, starting at a given offset, into a * destination array. * @@ -108,4 +108,8 @@ public class TaintSpace { protected TaintSet whenNull(long offset) { return TaintSet.EMPTY; } + + public void clear() { + taints.clear(); + } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/action/builder/MultiStateActionBuilder.java b/Ghidra/Framework/Docking/src/main/java/docking/action/builder/MultiStateActionBuilder.java index 86be342b54..43711cdda2 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/action/builder/MultiStateActionBuilder.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/action/builder/MultiStateActionBuilder.java @@ -18,6 +18,7 @@ package docking.action.builder; import java.util.ArrayList; import java.util.List; import java.util.function.BiConsumer; +import java.util.function.Supplier; import javax.swing.Icon; @@ -26,7 +27,7 @@ import docking.menu.ActionState; import docking.menu.MultiStateDockingAction; import docking.widgets.EventTrigger; -/** +/** * Builder for {@link MultiStateDockingAction} * * @param The action state type @@ -38,9 +39,11 @@ public class MultiStateActionBuilder extends private boolean useCheckboxForIcons; private List> states = new ArrayList<>(); + private Supplier>> generator = null; /** * Builder constructor + * * @param name the name of the action to be built * @param owner the owner of the action to be build */ @@ -55,8 +58,10 @@ public class MultiStateActionBuilder extends /** * Sets the primary callback to be executed when this action changes its action state. - * This builder will throw an {@link IllegalStateException} if one of the build methods is - * called without providing this callback + * + *

    + * This builder will throw an {@link IllegalStateException} if one of the build methods is + * called without providing this callback. * * @param biConsumer the callback to execute when the selected action state is changed. * @return this builder (for chaining) @@ -69,10 +74,12 @@ public class MultiStateActionBuilder extends } /** - * Overrides the default icons for actions shown in popup menu of the multi-state action. By - * default, the popup menu items will use the icons as provided by the {@link ActionState}. - * By passing true to this method, icons will not be used in the popup menu. Instead, a - * checkbox icon will be used to show the active action state. + * Overrides the default icons for actions shown in popup menu of the multi-state action. + * + *

    + * By default, the popup menu items will use the icons as provided by the {@link ActionState}. + * By passing true to this method, icons will not be used in the popup menu. Instead, a checkbox + * icon will be used to show the active action state. * * @param b true to use a checkbox * @return this MultiActionDockingActionBuilder (for chaining) @@ -83,7 +90,7 @@ public class MultiStateActionBuilder extends } /** - * Add an action state + * Add an action state * * @param displayName the name to appear in the action menu * @param icon the icon to appear in the action menu @@ -96,7 +103,7 @@ public class MultiStateActionBuilder extends } /** - * Add an action state + * Add an action state * * @param actionState the action state to add * @return this MultiActionDockingActionBuilder (for chaining) @@ -107,7 +114,7 @@ public class MultiStateActionBuilder extends } /** - * Add a list of action states + * Add a list of action states * * @param list a list of ActionStates; * @return this MultiActionDockingActionBuilder (for chaining) @@ -117,6 +124,21 @@ public class MultiStateActionBuilder extends return self(); } + /** + * Generate the states dynamically upon the user clicking the button + * + *

    + * It is highly recommended that the current state is included in the list of available states. + * Otherwise, the user could become confused or frustrated. + * + * @param generator a function from action context to available states + * @return this MultiActionDockingActionBuilder (for chaining) + */ + public MultiStateActionBuilder stateGenerator(Supplier>> generator) { + this.generator = generator; + return self(); + } + @Override public MultiStateDockingAction build() { validate(); @@ -138,6 +160,16 @@ public class MultiStateActionBuilder extends super.actionPerformed(context); } } + + @Override + protected List> getStates() { + if (generator == null) { + return super.getStates(); + } + else { + return generator.get(); + } + } }; for (ActionState actionState : states) { diff --git a/Ghidra/Framework/Docking/src/main/java/docking/menu/ActionState.java b/Ghidra/Framework/Docking/src/main/java/docking/menu/ActionState.java index 4e4f498d15..e104fb503a 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/menu/ActionState.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/menu/ActionState.java @@ -15,15 +15,17 @@ */ package docking.menu; +import java.util.Objects; + import javax.swing.Icon; import ghidra.util.HelpLocation; -import ghidra.util.SystemUtilities; /** - * Note: this class overrides the equals(Object) and relies upon the equals - * method of the userData object. Thus, if it is important that equals work for you in - * the non-standard identity way, then you must override equals in your user data objects. + * Note: this class overrides the equals(Object) and relies upon the + * equals method of the userData object. Thus, if it is important that + * equals work for you in the non-standard identity way, then you must override equals + * in your user data objects. * * @param the type of the action state */ @@ -73,7 +75,7 @@ public class ActionState { ActionState otherState = (ActionState) other; - if (!SystemUtilities.isEqual(userData, otherState.userData)) { + if (!Objects.equals(userData, otherState.userData)) { return false; } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/menu/MultiStateDockingAction.java b/Ghidra/Framework/Docking/src/main/java/docking/menu/MultiStateDockingAction.java index 1fd1b5142c..7d35553615 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/menu/MultiStateDockingAction.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/menu/MultiStateDockingAction.java @@ -31,14 +31,18 @@ import help.Help; import resources.icons.EmptyIcon; /** - * An action that can be in one of multiple states. The button of this action has a - * drop-down icon that allows users to change the state of the button. As the user changes the - * state of this action, {@link #actionStateChanged(ActionState, EventTrigger)} will be called. - * Clients may also use the button of this action to respond to button presses by overriding + * An action that can be in one of multiple states. + * + *

    + * The button of this action has a drop-down icon that allows users to change the state of the + * button. As the user changes the state of this action, + * {@link #actionStateChanged(ActionState, EventTrigger)} will be called. Clients may also use the + * button of this action to respond to button presses by overriding * {@link #actionPerformed(ActionContext)}. * - *

    This action is intended primarily for use as toolbar actions. Alternatively, some clients - * use this action to add a button to custom widgets. In the custom usage case, clients should use + *

    + * This is the callback to be overridden when the child wishes to respond to user button presses + * that are on the button and not the drop-down. The default behavior is to show the popup menu + * when the button is clicked. */ @Override public void actionPerformed(ActionContext context) { @@ -106,10 +97,12 @@ public abstract class MultiStateDockingAction extends DockingAction { } /** - * Overrides the default icons for actions shown in popup menu of the multi-state action. By - * default, the popup menu items will use the icons as provided by the {@link ActionState}. - * By passing true to this method, icons will not be used in the popup menu. Instead, a - * checkbox icon will be used to show the active action state. + * Overrides the default icons for actions shown in popup menu of the multi-state action. + * + *

    + * By default, the popup menu items will use the icons as provided by the {@link ActionState}. + * By passing true to this method, icons will not be used in the popup menu. Instead, a checkbox + * icon will be used to show the active action state. * * @param useCheckboxForIcons true to use a checkbox */ @@ -118,9 +111,11 @@ public abstract class MultiStateDockingAction extends DockingAction { } /** - * Sets the icon to use if the active action state does not supply an icon. This is useful if - * you wish for your action states to not use icon, but desire the action itself to have an - * icon. + * Sets the icon to use if the active action state does not supply an icon. + * + *

    + * This is useful if you wish for your action states to not use icon, but desire the action + * itself to have an icon. * * @param icon the icon */ @@ -128,18 +123,38 @@ public abstract class MultiStateDockingAction extends DockingAction { this.defaultIcon = icon; } + /** + * Extension point: Get the states to display when the button is clicked + * + *

    + * This is called when the button is clicked, immediately before the menu is displayed. It is + * generally recommended to ensure the current state is included in this list. The states will + * be displayed in the order of the returned list. + * + * @return the list of possible states + */ + protected List> getStates() { + return actionStates; + } + + private void updateStates() { + List> newStates = getStates(); + if (newStates.equals(actionStates)) { + return; + } + actionStates.clear(); + actionStates.addAll(newStates); + } + protected List getStateActions() { - ActionState selectedState = actionStates.get(currentStateIndex); + updateStates(); List actions = new ArrayList<>(actionStates.size()); for (ActionState actionState : actionStates) { - - //@formatter:off - boolean isSelected = actionState == selectedState; - DockingActionIf a = useCheckboxForIcons ? - new ActionStateToggleAction(actionState, isSelected) : - new ActionStateAction(actionState, isSelected); + boolean isSelected = actionState.equals(currentState); + DockingActionIf a = useCheckboxForIcons + ? new ActionStateToggleAction(actionState, isSelected) + : new ActionStateAction(actionState, isSelected); actions.add(a); - //@formatter:on } return actions; } @@ -155,8 +170,8 @@ public abstract class MultiStateDockingAction extends DockingAction { } /** - * add the supplied {@code ActionState} - * if {@code fireFirstEvent} is {@code true} the first one will fire its event + * Add the supplied {@code ActionState}. + * * @param actionState the {@code ActionState} to add */ public void addActionState(ActionState actionState) { @@ -175,11 +190,11 @@ public abstract class MultiStateDockingAction extends DockingAction { } public T getCurrentUserData() { - return actionStates.get(currentStateIndex).getUserData(); + return currentState == null ? null : currentState.getUserData(); } public ActionState getCurrentState() { - return actionStates.get(currentStateIndex); + return currentState; } public List> getAllActionStates() { @@ -187,6 +202,7 @@ public abstract class MultiStateDockingAction extends DockingAction { } public void setCurrentActionStateByUserData(T t) { + updateStates(); for (ActionState actionState : actionStates) { // Note: most clients will pass a T that is already in our list. However, to be more @@ -194,7 +210,7 @@ public abstract class MultiStateDockingAction extends DockingAction { // problem using equals() here. // if (actionState.getUserData() == t) { if (actionState.getUserData().equals(t)) { - setCurrentActionState(actionState); + doSetCurrentActionState(actionState, EventTrigger.API_CALL); return; } } @@ -208,13 +224,16 @@ public abstract class MultiStateDockingAction extends DockingAction { } public void setCurrentActionStateWithTrigger(ActionState actionState, EventTrigger trigger) { - int indexOf = actionStates.indexOf(actionState); - if (indexOf < 0) { + updateStates(); + doSetCurrentActionState(actionState, trigger); + } + + protected void doSetCurrentActionState(ActionState actionState, EventTrigger trigger) { + if (!actionStates.contains(actionState)) { throw new IllegalArgumentException( "Attempted to set actionState to unknown ActionState."); } - currentStateIndex = indexOf; - + currentState = actionState; setButtonState(actionState); ToolBarData tbd = getToolBarData(); @@ -240,9 +259,8 @@ public abstract class MultiStateDockingAction extends DockingAction { @Override public JButton doCreateButton() { multipleButton = new MultipleActionDockingToolbarButton(multiActionGenerator); - if (currentStateIndex >= 0) { - ActionState actionState = actionStates.get(currentStateIndex); - setButtonState(actionState); + if (currentState != null) { + setButtonState(currentState); } return multipleButton;

    + * This action is intended primarily for use as toolbar actions. Alternatively, some clients use + * this action to add a button to custom widgets. In the custom use case, clients should use * {@link NonToolbarMultiStateAction}. * * @param the type of the user data @@ -49,7 +53,7 @@ public abstract class MultiStateDockingAction extends DockingAction { private static Icon EMPTY_ICON = new EmptyIcon(16, 16); private List> actionStates = new ArrayList<>(); - private int currentStateIndex = -1; + private ActionState currentState = null; private MultiActionDockingActionIf multiActionGenerator; private MultipleActionDockingToolbarButton multipleButton; @@ -57,11 +61,10 @@ public abstract class MultiStateDockingAction extends DockingAction { private boolean useCheckboxForIcons; /** - * Call this constructor with this action will not be added to a toolbar + * Constructor * * @param name the action name * @param owner the owner - * @see #MultiStateDockingAction(String, String, boolean) */ public MultiStateDockingAction(String name, String owner) { super(name, owner); @@ -71,22 +74,9 @@ public abstract class MultiStateDockingAction extends DockingAction { super.setToolBarData(new ToolBarData(null)); } - /** - * Use this constructor explicitly when this action is used in a toolbar, passing true - * for isToolbarAction (see the javadoc header note). - * - * @param name the action name - * @param owner the owner - * @param isToolbarAction true if this action is a toolbar action - * @deprecated use {@link #MultiStateDockingAction(String, String)} - */ - @Deprecated(forRemoval = true, since = "10.2") - protected MultiStateDockingAction(String name, String owner, boolean isToolbarAction) { - this(name, owner); - } - /** * This method will be called as the user changes the selected button state + * * @param newActionState the newly selected state * @param trigger the source of the event */ @@ -94,11 +84,12 @@ public abstract class MultiStateDockingAction extends DockingAction { /** * This method is called when the user clicks the button when this action is used as part of - * the default {@link DockingAction} framework. + * the default {@link DockingAction} framework. * - * This is the callback to be overridden when the child wishes to respond to user button - * presses that are on the button and not the drop-down. The default behavior is to show the - * popup menu when the button is clicked. + *