diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetThread.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetThread.java index 294c1adb6d..c268453f48 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetThread.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetThread.java @@ -35,10 +35,8 @@ import ghidra.util.Msg; @TargetObjectSchemaInfo( name = "Thread", - elements = { - @TargetElementType(type = Void.class) }, - attributes = { - @TargetAttributeType(type = Void.class) }) + elements = { @TargetElementType(type = Void.class) }, + attributes = { @TargetAttributeType(type = Void.class) }) public class GdbModelTargetThread extends DefaultTargetObject implements TargetThread, TargetExecutionStateful, TargetSteppable, GdbModelSelectableObject { diff --git a/Ghidra/Debug/Debugger/certification.manifest b/Ghidra/Debug/Debugger/certification.manifest index 074bc7de19..39216859c0 100644 --- a/Ghidra/Debug/Debugger/certification.manifest +++ b/Ghidra/Debug/Debugger/certification.manifest @@ -85,6 +85,8 @@ src/main/help/help/topics/DebuggerObjectsPlugin/images/stepinto.png||GHIDRA||||E src/main/help/help/topics/DebuggerObjectsPlugin/images/stepout.png||GHIDRA||||END| src/main/help/help/topics/DebuggerObjectsPlugin/images/stepover.png||GHIDRA||||END| src/main/help/help/topics/DebuggerObjectsPlugin/images/stop.png||GHIDRA||||END| +src/main/help/help/topics/DebuggerPcodeStepperPlugin/DebuggerPcodeStepperPlugin.html||GHIDRA||||END| +src/main/help/help/topics/DebuggerPcodeStepperPlugin/images/DebuggerPcodeStepperPlugin.png||GHIDRA||||END| src/main/help/help/topics/DebuggerRegionsPlugin/DebuggerRegionsPlugin.html||GHIDRA||||END| src/main/help/help/topics/DebuggerRegionsPlugin/images/DebuggerRegionsPlugin.png||GHIDRA||||END| src/main/help/help/topics/DebuggerRegistersPlugin/DebuggerRegistersPlugin.html||GHIDRA||||END| diff --git a/Ghidra/Debug/Debugger/src/main/help/help/TOC_Source.xml b/Ghidra/Debug/Debugger/src/main/help/help/TOC_Source.xml index 848df0c1aa..a9902c2ef8 100644 --- a/Ghidra/Debug/Debugger/src/main/help/help/TOC_Source.xml +++ b/Ghidra/Debug/Debugger/src/main/help/help/TOC_Source.xml @@ -136,8 +136,12 @@ sortgroup="o" target="help/topics/DebuggerMemviewPlugin/DebuggerMemviewPlugin.html" /> + + diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerObjectsPlugin/DebuggerObjectsPlugin.html b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerObjectsPlugin/DebuggerObjectsPlugin.html index 7812fd03e3..5ee5a69921 100644 --- a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerObjectsPlugin/DebuggerObjectsPlugin.html +++ b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerObjectsPlugin/DebuggerObjectsPlugin.html @@ -31,9 +31,9 @@ determines whether both options are in play. For example, threads and inferiors/processes are both resumable, so the Resume action works on both. For many of our targets, processes are interruptible while threads are - not. Nevertheless, if Enable By Selection Only is off, you can interrupt a thread because - it descends from an inferior or process. In almost every case, the selection directly or - indirectly determines the set of valid or enabled actions.

+ not. Nevertheless, if Enable By Selection Only is off, you can interrupt a thread + because it descends from an inferior or process. In almost every case, the selection directly + or indirectly determines the set of valid or enabled actions.

The application of these special properties to each object to determine its behavior and relevant actions allows all objects to be treated generically. This feature has several diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerPcodeStepperPlugin/DebuggerPcodeStepperPlugin.html b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerPcodeStepperPlugin/DebuggerPcodeStepperPlugin.html new file mode 100644 index 0000000000..e7ef4f2ac5 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerPcodeStepperPlugin/DebuggerPcodeStepperPlugin.html @@ -0,0 +1,81 @@ + + + + + + + Debugger: P-code Stepper + + + + + +

Debugger: P-code Stepper

+ + + + + + + +
+ +

P-code is the "microcode" of Ghidra's processor specifications, compiled from its SLEIGH + specification. Originally designed to facilitate static analysis, it is easily applied to + emulation as well. Stepping each p-code operation is an effective means of debugging the + SLEIGH. The plugin provides two panes: 1) The p-code listing, and 2) Temporary ("Unique") + variables. The listing works similarly to the dynamic listing. It displays each p-code + operation, highlighting the current "counter", which is the next operation to be executed. + There is also a cursor, allowing selection of an operation. The variables view operates + similarly to the registers view, displaying the current value of each unique variable.

+ +

P-code stepping is built into the emulation framework, and so the other UI elements + (listing, registers, etc.) will display machine state from emulated p-code operations, i.e., + partially executed machine instructions. The p-code stepper provides a means of navigating time + at p-code-level and displaying p-code-level details of the machine state.

+ +

Table Columns

+ +

The unique variables table displays information about temporary variables, including their + values and user-assigned types. It has the following columns:

+ + + +

Actions

+ +

The p-code stepper provides the following actions:

+ +

Step Trace p-code Backward

+ +

This action is available when the current coordinates have some positive number of p-code + ticks. It steps the trace backward to the previous p-code tick, possibly using emulation. Note + that stepping backward does not affect the target, and many windows that would ordinarily + interact with a live target, may no longer do so, until the user steps back to the present. + Note also that any component or script that does interact with the target and record things + "into the present" may not cause updates in windows that are not displaying the present.

+ +

Step Trace p-code Forward

+ +

This action is available when a thread is selected. It steps the current thread forward to + the next p-code tick, using emulation. Note that emulation does not affect the target, and many + windows that would ordinarily interact with a live target, may not longer do so, until the user + steps back to the present. Note also that any component or script that does interact with the + target and record things "into the present" may not cause updates in windows that are not + displaying the present.

+ + diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerPcodeStepperPlugin/images/DebuggerPcodeStepperPlugin.png b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerPcodeStepperPlugin/images/DebuggerPcodeStepperPlugin.png new file mode 100644 index 0000000000..112f3f6f3c Binary files /dev/null and b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerPcodeStepperPlugin/images/DebuggerPcodeStepperPlugin.png differ diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerRegistersPlugin/DebuggerRegistersPlugin.html b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerRegistersPlugin/DebuggerRegistersPlugin.html index c7ee982327..2ef13abe39 100644 --- a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerRegistersPlugin/DebuggerRegistersPlugin.html +++ b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerRegistersPlugin/DebuggerRegistersPlugin.html @@ -31,7 +31,7 @@

Table Columns

The table displays information about registers, including their values and types. It has the - following columns

+ following columns:

+ +

Actions

+ +

The time window provides the following action:

+ +

Hide Scratch

+ +

This toggle action is always available. It is enabled by default. The emulation service, + which enabled trace extrapolation and interpolation, writes emulated state into the trace's + "scratch space," which comprises all negative snaps. When this toggle is enabled, those + snapshots are hidden. They can be displayed by disabling this toggle. Note that navigating into + scratch space may cause temporary undefined behavior in some windows, and may prevent + interaction with the target.

diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerTimePlugin/images/DebuggerTimePlugin.png b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerTimePlugin/images/DebuggerTimePlugin.png index d3fc818500..71870aa773 100644 Binary files a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerTimePlugin/images/DebuggerTimePlugin.png and b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerTimePlugin/images/DebuggerTimePlugin.png differ diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/AbstractDebuggerPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/AbstractDebuggerPlugin.java index c41debbe7e..db08e63339 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/AbstractDebuggerPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/AbstractDebuggerPlugin.java @@ -17,6 +17,9 @@ package ghidra.app.plugin.core.debug; import ghidra.framework.plugintool.*; +/** + * All this really does anymore is handle the auto-service wiring thing + */ public abstract class AbstractDebuggerPlugin extends Plugin { @SuppressWarnings("unused") private AutoService.Wiring autoServiceWiring; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/DebuggerCoordinates.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/DebuggerCoordinates.java index c9f042cc4f..57f5d1355b 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/DebuggerCoordinates.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/DebuggerCoordinates.java @@ -16,6 +16,7 @@ package ghidra.app.plugin.core.debug; import java.io.IOException; +import java.util.Collection; import java.util.Objects; import org.jdom.Element; @@ -30,12 +31,16 @@ import ghidra.trace.database.DBTraceContentHandler; import ghidra.trace.model.Trace; import ghidra.trace.model.program.TraceProgramView; import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.model.time.TraceSchedule; +import ghidra.trace.model.time.TraceSnapshot; +import ghidra.trace.util.DefaultTraceTimeViewport; +import ghidra.trace.util.TraceTimeViewport; import ghidra.util.Msg; import ghidra.util.NotOwnerException; public class DebuggerCoordinates { public static final DebuggerCoordinates NOWHERE = - new DebuggerCoordinates(null, null, null, null, 0L, "", 0) { + new DebuggerCoordinates(null, null, null, null, TraceSchedule.ZERO, 0) { @Override public void writeDataState(PluginTool tool, SaveState saveState, String key) { // Write nothing @@ -47,52 +52,59 @@ public class DebuggerCoordinates { private static final String KEY_TRACE_PATH = "TracePath"; private static final String KEY_TRACE_VERSION = "TraceVersion"; private static final String KEY_THREAD_KEY = "ThreadKey"; - private static final String KEY_SNAP = "Snap"; - private static final String KEY_TICKS = "Ticks"; + private static final String KEY_TIME = "Time"; private static final String KEY_FRAME = "Frame"; public static DebuggerCoordinates all(Trace trace, TraceRecorder recorder, TraceThread thread, - TraceProgramView view, Long snap, String ticks, Integer frame) { + TraceProgramView view, TraceSchedule time, Integer frame) { if (trace == NOWHERE.trace && recorder == NOWHERE.recorder && thread == NOWHERE.thread && - view == NOWHERE.view && snap == NOWHERE.snap && ticks == NOWHERE.ticks && - frame == NOWHERE.frame) { + view == NOWHERE.view && time == NOWHERE.time && frame == NOWHERE.frame) { return NOWHERE; } - return new DebuggerCoordinates(trace, recorder, thread, view, snap, ticks, frame); + return new DebuggerCoordinates(trace, recorder, thread, view, time, frame); } public static DebuggerCoordinates trace(Trace trace) { if (trace == null) { return NOWHERE; } - return all(trace, null, null, null, null, null, null); + return all(trace, null, null, null, null, null); } public static DebuggerCoordinates recorder(TraceRecorder recorder) { return all(recorder == null ? null : recorder.getTrace(), recorder, - null, null, recorder == null ? null : recorder.getSnap(), null, null); + null, null, recorder == null ? null : TraceSchedule.snap(recorder.getSnap()), null); } public static DebuggerCoordinates thread(TraceThread thread) { return all(thread == null ? null : thread.getTrace(), null, thread, - null, null, null, null); + null, null, null); } public static DebuggerCoordinates view(TraceProgramView view) { return all(view == null ? null : view.getTrace(), null, null, view, - view == null ? null : view.getSnap(), null, null); + view == null ? null : TraceSchedule.snap(view.getSnap()), null); } public static DebuggerCoordinates snap(long snap) { - return all(null, null, null, null, snap, null, null); + return all(null, null, null, null, TraceSchedule.snap(snap), null); + } + + public static DebuggerCoordinates time(String time) { + return time(TraceSchedule.parse(time)); + } + + public static DebuggerCoordinates time(TraceSchedule time) { + return all(null, null, null, null, time, null); } public static DebuggerCoordinates frame(int frame) { - return all(null, null, null, null, null, null, frame); + return all(null, null, null, null, null, frame); } public static DebuggerCoordinates threadSnap(TraceThread thread, long snap) { - return all(thread == null ? null : thread.getTrace(), null, thread, null, snap, null, null); + return all(thread == null ? null : thread.getTrace(), null, thread, null, + TraceSchedule.snap(snap), null); } public static boolean equalsIgnoreRecorderAndView(DebuggerCoordinates a, @@ -103,10 +115,7 @@ public class DebuggerCoordinates { if (!Objects.equals(a.thread, b.thread)) { return false; } - if (!Objects.equals(a.snap, b.snap)) { - return false; - } - if (!Objects.equals(a.ticks, b.ticks)) { + if (!Objects.equals(a.time, b.time)) { return false; } if (!Objects.equals(a.frame, b.frame)) { @@ -119,30 +128,31 @@ public class DebuggerCoordinates { private final TraceRecorder recorder; private final TraceThread thread; private final TraceProgramView view; - private final Long snap; - private final String ticks; + private final TraceSchedule time; private final Integer frame; private final int hash; + private Long viewSnap; + private DefaultTraceTimeViewport viewport; + protected DebuggerCoordinates(Trace trace, TraceRecorder recorder, TraceThread thread, - TraceProgramView view, Long snap, String ticks, Integer frame) { + TraceProgramView view, TraceSchedule time, Integer frame) { this.trace = trace; this.recorder = recorder; this.thread = thread; this.view = view; - this.snap = snap; - this.ticks = ticks; + this.time = time; this.frame = frame; - this.hash = Objects.hash(trace, recorder, thread, view, snap, ticks, frame); + this.hash = Objects.hash(trace, recorder, thread, view, time, frame); } @Override public String toString() { return String.format( - "Coords(trace=%s,recorder=%s,thread=%s,view=%s,snap=%d,ticks=%s,frame=%d)", - trace, recorder, thread, view, snap, ticks, frame); + "Coords(trace=%s,recorder=%s,thread=%s,view=%s,time=%s,frame=%d)", + trace, recorder, thread, view, time, frame); } @Override @@ -163,10 +173,7 @@ public class DebuggerCoordinates { if (!Objects.equals(this.view, that.view)) { return false; } - if (!Objects.equals(this.snap, that.snap)) { - return false; - } - if (!Objects.equals(this.ticks, that.ticks)) { + if (!Objects.equals(this.time, that.time)) { return false; } if (!Objects.equals(this.frame, that.frame)) { @@ -189,7 +196,7 @@ public class DebuggerCoordinates { } public DebuggerCoordinates withRecorder(TraceRecorder newRecorder) { - return all(trace, newRecorder, thread, view, snap, ticks, frame); + return all(trace, newRecorder, thread, view, time, frame); } public TraceThread getThread() { @@ -208,7 +215,7 @@ public class DebuggerCoordinates { } public DebuggerCoordinates withThread(TraceThread newThread) { - return all(trace, recorder, newThread, view, snap, ticks, frame); + return all(trace, recorder, newThread, view, time, frame); } public TraceProgramView getView() { @@ -216,21 +223,60 @@ public class DebuggerCoordinates { } public Long getSnap() { - return snap; + return time.getSnap(); } + /** + * Get these same coordinates with time replaced by the given snap-only coordinate + * + * @param newSnap the new snap + * @return the new coordinates + */ public DebuggerCoordinates withSnap(Long newSnap) { - return all(trace, recorder, thread, view, newSnap, ticks, frame); + return all(trace, recorder, thread, view, + newSnap == null ? time : TraceSchedule.snap(newSnap), frame); } - public String getTicks() { - return ticks; + public DebuggerCoordinates withTime(TraceSchedule newTime) { + return all(trace, recorder, thread, view, newTime, frame); + } + + public TraceSchedule getTime() { + return time; } public Integer getFrame() { return frame; } + public synchronized long getViewSnap() { + if (viewSnap != null) { + return viewSnap; + } + if (time.isSnapOnly()) { + return viewSnap = time.getSnap(); + } + Collection snapshots = + trace.getTimeManager().getSnapshotsWithSchedule(time); + if (snapshots.isEmpty()) { + Msg.warn(this, "Seems the emulation service did not create the requested snapshot"); + return viewSnap = time.getSnap(); + } + return viewSnap = snapshots.iterator().next().getKey(); + } + + public synchronized TraceTimeViewport getViewport() { + if (viewport != null) { + return viewport; + } + if (trace == null) { + return null; + } + viewport = new DefaultTraceTimeViewport(trace); + viewport.setSnap(getViewSnap()); + return viewport; + } + public void writeDataState(PluginTool tool, SaveState saveState, String key) { SaveState coordState = new SaveState(); // for NOWHERE, key should be completely omitted @@ -252,11 +298,8 @@ public class DebuggerCoordinates { if (thread != null) { coordState.putLong(KEY_THREAD_KEY, thread.getKey()); } - if (snap != null) { - coordState.putLong(KEY_SNAP, snap); - } - if (ticks != null) { - coordState.putString(KEY_TICKS, ticks); + if (time != null) { + coordState.putString(KEY_TIME, time.toString()); } if (frame != null) { coordState.putInt(KEY_FRAME, frame); @@ -327,37 +370,50 @@ public class DebuggerCoordinates { long threadKey = coordState.getLong(KEY_THREAD_KEY, 0); thread = trace.getThreadManager().getThread(threadKey); } - Long snap = null; - if (coordState.hasValue(KEY_SNAP)) { - snap = coordState.getLong(KEY_SNAP, 0); + String timeSpec = coordState.getString(KEY_TIME, null); + TraceSchedule time; + try { + time = TraceSchedule.parse(timeSpec); + } + catch (Exception e) { + Msg.error(DebuggerCoordinates.class, + "Could not restore invalid time specification: " + timeSpec); + time = TraceSchedule.ZERO; } - String ticks = coordState.getString(KEY_TICKS, null); Integer frame = null; if (coordState.hasValue(KEY_FRAME)) { frame = coordState.getInt(KEY_FRAME, 0); } DebuggerCoordinates coords = - DebuggerCoordinates.all(trace, null, thread, null, snap, ticks, frame); + DebuggerCoordinates.all(trace, null, thread, null, time, frame); if (!resolve) { return coords; } return traceManager.resolveCoordinates(coords); } - public boolean isDeadOrPresent() { - return recorder == null || isPresent(); - } - public boolean isAlive() { return recorder != null; } public boolean isPresent() { - return recorder.getSnap() == snap; + return recorder.getSnap() == time.getSnap() && time.isSnapOnly(); + } + + public boolean isReadsPresent() { + return recorder.getSnap() == time.getSnap(); } public boolean isAliveAndPresent() { return isAlive() && isPresent(); } + + public boolean isDeadOrPresent() { + return !isAlive() || isPresent(); + } + + public boolean isAliveAndReadsPresent() { + return isAlive() && isReadsPresent(); + } } 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 7dba84da35..18eb37cc9a 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 @@ -35,6 +35,7 @@ import ghidra.app.plugin.core.debug.gui.memory.DebuggerRegionsPlugin; import ghidra.app.plugin.core.debug.gui.modules.DebuggerModulesPlugin; import ghidra.app.plugin.core.debug.gui.modules.DebuggerStaticMappingPlugin; import ghidra.app.plugin.core.debug.gui.objects.DebuggerObjectsPlugin; +import ghidra.app.plugin.core.debug.gui.pcode.DebuggerPcodeStepperPlugin; import ghidra.app.plugin.core.debug.gui.register.DebuggerRegistersPlugin; import ghidra.app.plugin.core.debug.gui.stack.DebuggerStackPlugin; import ghidra.app.plugin.core.debug.gui.target.DebuggerTargetsPlugin; @@ -48,7 +49,9 @@ import ghidra.framework.plugintool.util.PluginUtils; import ghidra.program.database.ProgramContentHandler; import ghidra.trace.model.Trace; import ghidra.util.*; +import resources.MultiIcon; import resources.ResourceManager; +import resources.icons.RotateIcon; public interface DebuggerResources { String OPTIONS_CATEGORY_WORKFLOW = "Debugger.Workflow"; @@ -66,16 +69,20 @@ public interface DebuggerResources { ImageIcon ICON_LAUNCH = ResourceManager.loadImage("images/launch.png"); ImageIcon ICON_ATTACH = ResourceManager.loadImage("images/attach.png"); ImageIcon ICON_RESUME = ResourceManager.loadImage("images/continue.png"); - ImageIcon ICON_STEP_INTO = ResourceManager.loadImage("images/stepinto.png"); - ImageIcon ICON_STEP_OVER = ResourceManager.loadImage("images/stepover.png"); - ImageIcon ICON_STEP_FINISH = ResourceManager.loadImage("images/stepout.png"); - ImageIcon ICON_REV_STEP_INTO = ResourceManager.loadImage("images/stepback.png"); ImageIcon ICON_TERMINATE = ResourceManager.loadImage("images/stop.png"); ImageIcon ICON_KILL = ResourceManager.loadImage("images/kill.png"); ImageIcon ICON_DETACH = ResourceManager.loadImage("images/detach.png"); - ImageIcon ICON_SEEK_PRESENT = ICON_RESUME; // TODO: Draw a new icon? ImageIcon ICON_RECORD = ResourceManager.loadImage("images/record.png"); + ImageIcon ICON_STEP_INTO = ResourceManager.loadImage("images/stepinto.png"); + ImageIcon ICON_STEP_OVER = ResourceManager.loadImage("images/stepover.png"); + ImageIcon ICON_STEP_FINISH = ResourceManager.loadImage("images/stepout.png"); + ImageIcon ICON_STEP_BACK = ResourceManager.loadImage("images/stepback.png"); + // TODO: Draw new icons? + ImageIcon ICON_SNAP_FORWARD = ResourceManager.loadImage("images/2rightarrow.png"); + ImageIcon ICON_SNAP_BACKWARD = ResourceManager.loadImage("images/2leftarrow.png"); + ImageIcon ICON_SEEK_PRESENT = ICON_RESUME; + ImageIcon ICON_SET_BREAKPOINT = ResourceManager.loadImage("images/breakpoint-set.png"); ImageIcon ICON_CLEAR_BREAKPOINT = ResourceManager.loadImage("images/breakpoint-clear.png"); ImageIcon ICON_ENABLE_BREAKPOINT = ResourceManager.loadImage("images/breakpoint-enable.png"); @@ -95,6 +102,7 @@ public interface DebuggerResources { ImageIcon ICON_BREAKPOINTS = ResourceManager.loadImage("images/breakpoints.png"); ImageIcon ICON_MODULES = ResourceManager.loadImage("images/modules.png"); ImageIcon ICON_MAPPINGS = ICON_PROGRAM; // TODO: A better icon + ImageIcon ICON_PCODE = ResourceManager.loadImage("images/stepinto.png"); // TODO //ResourceManager.loadImage("images/mappings.png"); ImageIcon ICON_REGIONS = ResourceManager.loadImage("images/memory16.gif"); ImageIcon ICON_TIME = ResourceManager.loadImage("images/time.png"); @@ -158,6 +166,11 @@ public interface DebuggerResources { HelpLocation HELP_PROVIDER_MODULES = new HelpLocation( PluginUtils.getPluginNameFromClass(DebuggerModulesPlugin.class), HELP_ANCHOR_PLUGIN); + String TITLE_PROVIDER_PCODE = "Pcode Stepper"; + ImageIcon ICON_PROVIDER_PCODE = ICON_PCODE; + HelpLocation HELP_PROVIDER_PCODE = new HelpLocation( + PluginUtils.getPluginNameFromClass(DebuggerPcodeStepperPlugin.class), HELP_ANCHOR_PLUGIN); + String TITLE_PROVIDER_REGIONS = "Regions"; ImageIcon ICON_PROVIDER_REGIONS = ICON_REGIONS; HelpLocation HELP_PROVIDER_REGIONS = new HelpLocation( @@ -233,6 +246,9 @@ public interface DebuggerResources { String OPTION_NAME_COLORS_WATCH_CHANGED_SEL = "Colors.Changed Watches (selected)"; Color DEFAULT_COLOR_WATCH_CHANGED_SEL = ColorUtils.blend(Color.RED, Color.WHITE, 0.5f); + String OPTION_NAME_COLORS_PCODE_COUNTER = "Colors.Pcode Counter"; + Color DEFAULT_COLOR_PCODE_COUNTER = new Color(0.75f, 0.875f, 0.75f); + String MARKER_NAME_BREAKPOINT_ENABLED = "Enabled Breakpoint"; String MARKER_NAME_BREAKPOINT_DISABLED = "Disabled Breakpoint"; String MARKER_NAME_BREAKPOINT_MIXED_ED = "Mixed Enabled-Disabled Breakpont"; @@ -248,6 +264,11 @@ public interface DebuggerResources { ImageIcon ICON_BREAKPOINT_MIXED_DE_MARKER = ResourceManager.loadImage("images/breakpoint-mixed-de.png"); + Icon ICON_UNIQUE_REF_READ = + new RotateIcon(ResourceManager.loadImage("images/cursor_arrow.gif"), 180); // TODO + ImageIcon ICON_UNIQUE_REF_WRITE = ResourceManager.loadImage("images/cursor_arrow.gif"); // TODO + Icon ICON_UNIQUE_REF_RW = new MultiIcon(ICON_UNIQUE_REF_READ, ICON_UNIQUE_REF_WRITE); // TODO + String OPTION_NAME_COLORS_ENABLED_BREAKPOINT_MARKERS = "Colors.Enabled Breakpoint Markers"; Color DEFAULT_COLOR_ENABLED_BREAKPOINT_MARKERS = new Color(0.875f, 0.75f, 0.75f); String OPTION_NAME_COLORS_DISABLED_BREAKPOINT_MARKERS = "Colors.Disabled Breakpoint Markers"; @@ -1088,6 +1109,23 @@ public interface DebuggerResources { } } + /*interface SelectAddressesAction { // TODO: Finish this conversion + String NAME = "Select Addresses"; + Icon ICON = ICON_SELECT_ADDRESSES; + String GROUP = GROUP_GENERAL; + String HELP_ANCHOR = "select_addresses"; + + static ActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new ActionBuilder(NAME, ownerName) + .toolBarIcon(ICON) + .toolBarGroup(GROUP) + .popupMenuPath(NAME) + .popupMenuIcon(ICON) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); + } + }*/ + abstract class AbstractSelectAddressesAction extends DockingAction { public static final String NAME = "Select Addresses"; public static final Icon ICON = ICON_SELECT_ADDRESSES; @@ -1146,30 +1184,88 @@ public interface DebuggerResources { } } - abstract class AbstractStepTraceForwardAction extends DockingAction { - public static final String NAME = "Step Trace Forward"; - public static final Icon ICON = ICON_STEP_INTO; - public static final String HELP_ANCHOR = "step_trace_forward"; + abstract class AbstractStepSnapForwardAction extends DockingAction { + public static final String NAME = "Step Trace Snap Forward"; + public static final Icon ICON = ICON_SNAP_FORWARD; + public static final String HELP_ANCHOR = "step_trace_snap_forward"; - public AbstractStepTraceForwardAction(Plugin owner) { + public AbstractStepSnapForwardAction(Plugin owner) { super(NAME, owner.getName()); - setDescription("Move the recording forward one tick"); + setDescription("Navigate the recording forward one snap"); setHelpLocation(new HelpLocation(owner.getName(), HELP_ANCHOR)); } } - abstract class AbstractStepTraceBackwardAction extends DockingAction { - public static final String NAME = "Step Trace Backward"; - public static final Icon ICON = ICON_REV_STEP_INTO; - public static final String HELP_ANCHOR = "step_trace_backward"; + abstract class AbstractStepTickForwardAction extends DockingAction { + public static final String NAME = "Step Trace Tick Forward"; + public static final Icon ICON = ICON_STEP_INTO; + public static final String HELP_ANCHOR = "step_trace_tick_forward"; - public AbstractStepTraceBackwardAction(Plugin owner) { + public AbstractStepTickForwardAction(Plugin owner) { super(NAME, owner.getName()); - setDescription("Move the recording backward one tick"); + setDescription("Navigate the recording forward one tick"); setHelpLocation(new HelpLocation(owner.getName(), HELP_ANCHOR)); } } + interface StepPcodeForwardAction { + String NAME = "Step Trace p-code Forward"; + String DESCRIPTION = "Navigate the recording forward one p-code tick"; + Icon ICON = ICON_STEP_INTO; + String GROUP = GROUP_CONTROL; + String HELP_ANCHOR = "step_trace_pcode_forward"; + + static ActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new ActionBuilder(NAME, ownerName) + .description(DESCRIPTION) + .toolBarIcon(ICON) + .toolBarGroup(GROUP) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); + } + } + + abstract class AbstractStepTickBackwardAction extends DockingAction { + public static final String NAME = "Step Trace Tick Backward"; + public static final Icon ICON = ICON_STEP_BACK; + public static final String HELP_ANCHOR = "step_trace_tick_backward"; + + public AbstractStepTickBackwardAction(Plugin owner) { + super(NAME, owner.getName()); + setDescription("Navigate the recording backward one tick"); + setHelpLocation(new HelpLocation(owner.getName(), HELP_ANCHOR)); + } + } + + abstract class AbstractStepSnapBackwardAction extends DockingAction { + public static final String NAME = "Step Trace Snap Backward"; + public static final Icon ICON = ICON_SNAP_BACKWARD; + public static final String HELP_ANCHOR = "step_trace_snap_backward"; + + public AbstractStepSnapBackwardAction(Plugin owner) { + super(NAME, owner.getName()); + setDescription("Navigate the recording backward one snap"); + setHelpLocation(new HelpLocation(owner.getName(), HELP_ANCHOR)); + } + } + + interface StepPcodeBackwardAction { + String NAME = "Step Trace p-code Backward"; + String DESCRIPTION = "Navigate the recording backward one p-code tick"; + Icon ICON = ICON_STEP_BACK; + String GROUP = GROUP_CONTROL; + String HELP_ANCHOR = "step_trace_pcode_backward"; + + static ActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new ActionBuilder(NAME, ownerName) + .description(DESCRIPTION) + .toolBarIcon(ICON) + .toolBarGroup(GROUP) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); + } + } + abstract class AbstractSeekTracePresentAction extends ToggleDockingAction { public static final String NAME = "Seek Trace Present"; public static final Icon ICON = ICON_SEEK_PRESENT; @@ -1372,6 +1468,22 @@ public interface DebuggerResources { } } + interface HideScratchSnapshotsAction { + String NAME = "Hide Scratch"; + String DESCRIPTION = "Hide negative snaps, typically used as emulation scratch space"; + String GROUP = GROUP_GENERAL; + String HELP_ANCHOR = "hide_scratch"; + + static ToggleActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new ToggleActionBuilder(NAME, ownerName) + .description(DESCRIPTION) + .menuGroup(GROUP_GENERAL) + .menuPath(NAME) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); + } + } + public abstract class AbstractDebuggerConnectionsNode extends GTreeNode { @Override public String getName() { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingAutoReadMemoryAction.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingAutoReadMemoryAction.java index 80c861576b..45ec2acb43 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingAutoReadMemoryAction.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingAutoReadMemoryAction.java @@ -104,7 +104,7 @@ public interface DebuggerListingAutoReadMemoryAction extends AutoReadMemoryActio @Override public CompletableFuture readMemory(DebuggerCoordinates coordinates, AddressSetView visible) { - if (!coordinates.isAliveAndPresent()) { + if (!coordinates.isAliveAndReadsPresent()) { return AsyncUtils.NIL; } TraceRecorder recorder = coordinates.getRecorder(); @@ -135,7 +135,7 @@ public interface DebuggerListingAutoReadMemoryAction extends AutoReadMemoryActio @Override public CompletableFuture readMemory(DebuggerCoordinates coordinates, AddressSetView visible) { - if (!coordinates.isAliveAndPresent()) { + if (!coordinates.isAliveAndReadsPresent()) { return AsyncUtils.NIL; } TraceRecorder recorder = coordinates.getRecorder(); 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 522fceb52b..b38f78266b 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 @@ -53,40 +53,40 @@ import ghidra.util.Swing; import utilities.util.SuppressableCallback; import utilities.util.SuppressableCallback.Suppression; -@PluginInfo(// - shortDescription = "View and annotate listings of trace (possibly live) memory", // - description = "Provides the memory listing display window. Functions similarly to " + - "the main program listing display window, but for traces. If the trace is the " + - "destination of a live recording, the view(s) retrieve live memory on demand.", // - category = PluginCategoryNames.DEBUGGER, // - packageName = DebuggerPluginPackage.NAME, // - status = PluginStatus.RELEASED, // - eventsConsumed = { // - // ProgramSelectionPluginEvent.class, // TODO: Later or remove - // ProgramHighlightPluginEvent.class, // TODO: Later or remove - ProgramClosedPluginEvent.class, // For marker set cleanup - ProgramLocationPluginEvent.class, // For static listing sync - TraceActivatedPluginEvent.class, // Trace/thread activation and register tracking - TraceClosedPluginEvent.class, // - }, // - eventsProduced = { // - ProgramLocationPluginEvent.class, // - // ProgramSelectionPluginEvent.class, // - TraceLocationPluginEvent.class, // - TraceSelectionPluginEvent.class // - }, // - servicesRequired = { // - DebuggerModelService.class, // For memory capture - DebuggerStaticMappingService.class, // For static listing sync. TODO: Optional? - ProgramManager.class, // TODO: Needed? - GoToService.class, // For static listing sync - ClipboardService.class, // - MarkerService.class // TODO: Make optional? - }, // - servicesProvided = { // - DebuggerListingService.class, // - } // -) +@PluginInfo( + shortDescription = "View and annotate listings of trace (possibly live) memory", + description = "Provides the memory listing display window. Functions similarly to " + + "the main program listing display window, but for traces. If the trace is the " + + "destination of a live recording, the view(s) retrieve live memory on demand.", + category = PluginCategoryNames.DEBUGGER, + packageName = DebuggerPluginPackage.NAME, + status = PluginStatus.RELEASED, + eventsConsumed = { + // ProgramSelectionPluginEvent.class, // TODO: Later or remove + // ProgramHighlightPluginEvent.class, // TODO: Later or remove + ProgramClosedPluginEvent.class, // For marker set cleanup + ProgramLocationPluginEvent.class, // For static listing sync + TraceActivatedPluginEvent.class, // Trace/thread activation and register tracking + TraceClosedPluginEvent.class, + }, + eventsProduced = { + ProgramLocationPluginEvent.class, + // ProgramSelectionPluginEvent.class, + TraceLocationPluginEvent.class, + TraceSelectionPluginEvent.class + }, + servicesRequired = { + DebuggerModelService.class, // For memory capture + DebuggerStaticMappingService.class, // For static listing sync. TODO: Optional? + DebuggerEmulationService.class, // TODO: Optional? + ProgramManager.class, // For static listing sync + //GoToService.class, // For static listing sync + ClipboardService.class, + MarkerService.class // TODO: Make optional? + }, + servicesProvided = { + DebuggerListingService.class, + }) public class DebuggerListingPlugin extends CodeBrowserPlugin implements DebuggerListingService { private static final String KEY_CONNECTED_PROVIDER = "connectedProvider"; private static final String KEY_DISCONNECTED_COUNT = "disconnectedCount"; @@ -118,8 +118,8 @@ public class DebuggerListingPlugin extends CodeBrowserPlugin implements Debugger protected NewListingAction actionNewListing; - @AutoServiceConsumed - private GoToService goToService; + //@AutoServiceConsumed + //private GoToService goToService; @AutoServiceConsumed private ProgramManager programManager; // NOTE: ListingPlugin doesn't extend AbstractDebuggerPlugin @@ -127,22 +127,22 @@ public class DebuggerListingPlugin extends CodeBrowserPlugin implements Debugger private AutoService.Wiring autoServiceWiring; @AutoOptionDefined( // - name = OPTION_NAME_COLORS_STALE_MEMORY, // - description = "Color of memory addresses whose content is not known in the view's " + - "snap", // - help = @HelpInfo(anchor = "colors")) + name = OPTION_NAME_COLORS_STALE_MEMORY, // + description = "Color of memory addresses whose content is not known in the view's " + + "snap", // + help = @HelpInfo(anchor = "colors")) private Color staleMemoryColor = DEFAULT_COLOR_BACKGROUND_STALE; @AutoOptionDefined( // - name = OPTION_NAME_COLORS_ERROR_MEMORY, // - description = "Color of memory addresses whose content could not be read in the " + - "view's snap", // - help = @HelpInfo(anchor = "colors")) + name = OPTION_NAME_COLORS_ERROR_MEMORY, // + description = "Color of memory addresses whose content could not be read in the " + + "view's snap", // + help = @HelpInfo(anchor = "colors")) private Color errorMemoryColor = DEFAULT_COLOR_BACKGROUND_ERROR; // NOTE: Static programs are marked via markerSet. Dynamic are marked via custom color model @AutoOptionDefined( // - name = OPTION_NAME_COLORS_REGISTER_MARKERS, // - description = "Background color for locations referred to by a tracked register", // - help = @HelpInfo(anchor = "colors")) + name = OPTION_NAME_COLORS_REGISTER_MARKERS, // + description = "Background color for locations referred to by a tracked register", // + help = @HelpInfo(anchor = "colors")) private Color trackingColor = DEFAULT_COLOR_REGISTER_MARKERS; @SuppressWarnings("unused") private AutoOptions.Wiring autoOptionsWiring; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProvider.java index 64a687cf80..c6d49a5353 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProvider.java @@ -81,6 +81,7 @@ import ghidra.trace.model.Trace.*; import ghidra.trace.model.memory.TraceMemoryRegion; import ghidra.trace.model.modules.*; import ghidra.trace.model.program.TraceProgramView; +import ghidra.trace.model.program.TraceVariableSnapProgramView; import ghidra.trace.model.stack.TraceStack; import ghidra.trace.model.thread.TraceThread; import ghidra.trace.model.time.TraceSnapshot; @@ -104,8 +105,8 @@ public class DebuggerListingProvider extends CodeViewerProvider implements Listi if (!Objects.equals(a.getRecorder(), b.getRecorder())) { return false; // For capture memory action } - if (!Objects.equals(a.getSnap(), b.getSnap())) { - return false; // Subsumed by view, but I care that it's snap has changed + if (!Objects.equals(a.getTime(), b.getTime())) { + return false; } if (!Objects.equals(a.getThread(), b.getThread())) { return false; // for reg/pc tracking @@ -113,7 +114,6 @@ public class DebuggerListingProvider extends CodeViewerProvider implements Listi if (!Objects.equals(a.getFrame(), b.getFrame())) { return false; // for reg/pc tracking } - // TODO: Ticks return true; } @@ -129,14 +129,11 @@ public class DebuggerListingProvider extends CodeViewerProvider implements Listi @Override public void actionPerformed(ActionContext context) { - if (current.getView() == null) { + if (!current.isAliveAndReadsPresent()) { return; } Trace trace = current.getTrace(); TraceRecorder recorder = current.getRecorder(); - if (recorder == null) { - return; - } BackgroundUtils.async(plugin.getTool(), trace, NAME, true, true, false, (__, monitor) -> recorder .captureProcessMemory(getListingPanel().getProgramSelection(), monitor)); @@ -144,13 +141,10 @@ public class DebuggerListingProvider extends CodeViewerProvider implements Listi @Override public boolean isEnabledForContext(ActionContext context) { + if (!current.isAliveAndReadsPresent()) { + return false; + } TraceRecorder recorder = current.getRecorder(); - if (recorder == null) { - return false; - } - if (recorder.getSnap() != current.getSnap()) { - return false; - } ProgramSelection selection = getSelection(); if (selection == null || selection.isEmpty()) { return false; @@ -334,7 +328,7 @@ public class DebuggerListingProvider extends CodeViewerProvider implements Listi @SuppressWarnings("unused") private final AutoService.Wiring autoServiceWiring; - @AutoOptionConsumed(name = OPTION_NAME_COLORS_REGISTER_MARKERS) + @AutoOptionConsumed(name = DebuggerResources.OPTION_NAME_COLORS_REGISTER_MARKERS) private Color trackingColor; @SuppressWarnings("unused") private final AutoOptions.Wiring autoOptionsWiring; @@ -757,7 +751,9 @@ public class DebuggerListingProvider extends CodeViewerProvider implements Listi return; } // Avoid odd race conditions by fixing the snap - TraceProgramView fixed = current.getTrace().getFixedProgramView(current.getSnap()); + TraceProgramView fixed = current.getView() instanceof TraceVariableSnapProgramView + ? current.getTrace().getFixedProgramView(current.getSnap()) + : current.getView(); ExporterDialog dialog = new ExporterDialog(tool, fixed.getDomainFile(), fixed, getSelection()); @@ -1056,13 +1052,14 @@ public class DebuggerListingProvider extends CodeViewerProvider implements Listi // Change of current snap (TODO) // Change of current frame (TODO) // Change of tracking settings - DebuggerCoordinates cur = this.current; + DebuggerCoordinates cur = current; TraceThread thread = cur.getThread(); if (thread == null || trackingSpec == null) { return null; } - Address address = trackingSpec.computeTraceAddress(cur); - return address == null ? null : new ProgramLocation(cur.getView(), address); + // NB: view's snap may be forked for emulation + Address address = trackingSpec.computeTraceAddress(cur, current.getView().getSnap()); + return address == null ? null : new ProgramLocation(current.getView(), address); } protected ProgramLocation doMarkTrackedLocation() { @@ -1082,16 +1079,16 @@ public class DebuggerListingProvider extends CodeViewerProvider implements Listi if (loc == null) { return; } - DebuggerCoordinates cur = current; + TraceProgramView curView = current.getView(); if (!syncToStaticListing || trackedStatic == null) { Swing.runIfSwingOrRunLater(() -> { - goTo(cur.getView(), loc); + goTo(curView, loc); doAutoImportCurrentModule(); }); } else { Swing.runIfSwingOrRunLater(() -> { - goTo(cur.getView(), loc); + goTo(curView, loc); doAutoImportCurrentModule(); plugin.fireStaticLocationEvent(trackedStatic); }); @@ -1118,7 +1115,7 @@ public class DebuggerListingProvider extends CodeViewerProvider implements Listi } public void staticProgramLocationChanged(ProgramLocation location) { - TraceProgramView view = current.getView(); + TraceProgramView view = current.getView(); // NB. Used for snap (don't want emuSnap) if (!isSyncToStaticListing() || view == null || location == null) { return; } @@ -1134,10 +1131,9 @@ public class DebuggerListingProvider extends CodeViewerProvider implements Listi return coordinates; } // Because the view's snap is changing with or without us.... So go with. - return current.withSnap(coordinates.getSnap()); + return current.withTime(coordinates.getTime()); } - // TODO: Figure out clone action public void goToCoordinates(DebuggerCoordinates coordinates) { if (sameCoordinates(current, coordinates)) { current = coordinates; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingTrackLocationAction.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingTrackLocationAction.java index 9f760a2473..c4c4f280b1 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingTrackLocationAction.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingTrackLocationAction.java @@ -27,6 +27,7 @@ import ghidra.program.model.lang.RegisterValue; import ghidra.trace.model.Trace; import ghidra.trace.model.TraceAddressSnapRange; import ghidra.trace.model.memory.TraceMemoryRegisterSpace; +import ghidra.trace.model.memory.TraceMemoryState; import ghidra.trace.model.stack.TraceStack; import ghidra.trace.model.stack.TraceStackFrame; import ghidra.trace.model.thread.TraceThread; @@ -90,7 +91,7 @@ public interface DebuggerListingTrackLocationAction extends TrackLocationAction */ String computeTitle(DebuggerCoordinates coordinates); - Address computeTraceAddress(DebuggerCoordinates coordinates); + Address computeTraceAddress(DebuggerCoordinates coordinates, long emuSnap); // TODO: Is there a way to generalize these so that other dependencies need not // have their own bespoke methods? @@ -115,7 +116,7 @@ public interface DebuggerListingTrackLocationAction extends TrackLocationAction } @Override - public Address computeTraceAddress(DebuggerCoordinates coordinates) { + public Address computeTraceAddress(DebuggerCoordinates coordinates, long emuSnap) { return null; } @@ -147,7 +148,7 @@ public interface DebuggerListingTrackLocationAction extends TrackLocationAction } @Override - default Address computeTraceAddress(DebuggerCoordinates coordinates) { + default Address computeTraceAddress(DebuggerCoordinates coordinates, long emuSnap) { Trace trace = coordinates.getTrace(); TraceThread thread = coordinates.getThread(); long snap = coordinates.getSnap(); @@ -164,7 +165,13 @@ public interface DebuggerListingTrackLocationAction extends TrackLocationAction if (regs == null) { return null; } - RegisterValue value = regs.getValue(snap, reg); + RegisterValue value; + if (regs.getState(emuSnap, reg) == TraceMemoryState.KNOWN) { + value = regs.getValue(emuSnap, reg); + } + else { + value = regs.getValue(snap, reg); + } if (value == null) { return null; } @@ -230,12 +237,14 @@ public interface DebuggerListingTrackLocationAction extends TrackLocationAction } @Override - public Address computeTraceAddress(DebuggerCoordinates coordinates) { - Address pc = computePCViaStack(coordinates); - if (pc != null) { - return pc; + public Address computeTraceAddress(DebuggerCoordinates coordinates, long emuSnap) { + if (coordinates.getTime().isSnapOnly()) { + Address pc = computePCViaStack(coordinates); + if (pc != null) { + return pc; + } } - return RegisterLocationTrackingSpec.super.computeTraceAddress(coordinates); + return RegisterLocationTrackingSpec.super.computeTraceAddress(coordinates, emuSnap); } // Note it does no good to override affectByRegChange. It must do what we'd avoid anyway. @@ -244,6 +253,9 @@ public interface DebuggerListingTrackLocationAction extends TrackLocationAction if (stack.getThread() != coordinates.getThread()) { return false; } + if (!coordinates.getTime().isSnapOnly()) { + return false; + } // TODO: Would be nice to have stack lifespan... TraceStack curStack = coordinates.getTrace() .getStackManager() diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/MemoryStateListingBackgroundColorModel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/MemoryStateListingBackgroundColorModel.java index 72a58c747d..01773dcf50 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/MemoryStateListingBackgroundColorModel.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/MemoryStateListingBackgroundColorModel.java @@ -63,11 +63,11 @@ public class MemoryStateListingBackgroundColorModel implements ListingBackground return defaultBackgroundColor; } - TraceMemoryState state = memory.getState(view.getSnap(), address); + Entry state = memory.getViewState(view.getSnap(), address); if (state == null) { return defaultBackgroundColor; } - switch (state) { + switch (state.getValue()) { case UNKNOWN: return getUnknownColor(address); case ERROR: @@ -84,11 +84,11 @@ public class MemoryStateListingBackgroundColorModel implements ListingBackground protected Color getUnknownColor(Address address) { Entry ent = - memory.getMostRecentStateEntry(view.getSnap(), address); + memory.getViewMostRecentStateEntry(view.getSnap(), address); if (ent == null || ent.getValue() != TraceMemoryState.KNOWN) { return unknownColor; } - TraceMemoryRegion region = memory.getRegionContaining(view.getSnap(), address); + TraceMemoryRegion region = memory.getRegionContaining(ent.getKey().getY1(), address); if (region != null && !region.isWrite()) { return unknownBlendedColor; } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProvider.java index e858282426..2c9ee1eb1d 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProvider.java @@ -490,9 +490,6 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter { } } - // TODO: Display modules in timeline? - // TODO: Select module/section containing current address? - private final DebuggerModulesPlugin plugin; // @AutoServiceConsumed via method diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/pcode/BranchPcodeRow.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/pcode/BranchPcodeRow.java new file mode 100644 index 0000000000..991657bd26 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/pcode/BranchPcodeRow.java @@ -0,0 +1,43 @@ +/* ### + * 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.pcode; + +import ghidra.program.model.pcode.PcodeOp; + +public class BranchPcodeRow implements PcodeRow { + private final int sequence; + private final int fromSeq; + + public BranchPcodeRow(int sequence, int fromSeq) { + this.sequence = sequence; + this.fromSeq = fromSeq; + } + + @Override + public int getSequence() { + return sequence; + } + + @Override + public String getCode() { + return "(branched from " + fromSeq + ")"; + } + + @Override + public PcodeOp getOp() { + return null; + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/pcode/DebuggerPcodeStepperPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/pcode/DebuggerPcodeStepperPlugin.java new file mode 100644 index 0000000000..a3ba6f59da --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/pcode/DebuggerPcodeStepperPlugin.java @@ -0,0 +1,61 @@ +/* ### + * 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.pcode; + +import ghidra.app.plugin.PluginCategoryNames; +import ghidra.app.plugin.core.debug.AbstractDebuggerPlugin; +import ghidra.app.plugin.core.debug.DebuggerPluginPackage; +import ghidra.app.plugin.core.debug.event.TraceActivatedPluginEvent; +import ghidra.app.services.DebuggerEmulationService; +import ghidra.app.services.DebuggerTraceManagerService; +import ghidra.framework.plugintool.*; +import ghidra.framework.plugintool.util.PluginStatus; + +@PluginInfo( + shortDescription = "Debugger p-code stepper", + description = "GUI to single-step emulation of p-code", + category = PluginCategoryNames.DEBUGGER, + packageName = DebuggerPluginPackage.NAME, + status = PluginStatus.UNSTABLE, + eventsConsumed = { + TraceActivatedPluginEvent.class, + }, + servicesRequired = { + DebuggerTraceManagerService.class, + DebuggerEmulationService.class, + }) +public class DebuggerPcodeStepperPlugin extends AbstractDebuggerPlugin { + protected DebuggerPcodeStepperProvider provider; + + public DebuggerPcodeStepperPlugin(PluginTool tool) { + super(tool); + } + + @Override + protected void init() { + provider = new DebuggerPcodeStepperProvider(this); + super.init(); + } + + @Override + public void processEvent(PluginEvent event) { + super.processEvent(event); + if (event instanceof TraceActivatedPluginEvent) { + TraceActivatedPluginEvent ev = (TraceActivatedPluginEvent) event; + provider.coordinatesActivated(ev.getActiveCoordinates()); + } + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/pcode/DebuggerPcodeStepperProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/pcode/DebuggerPcodeStepperProvider.java new file mode 100644 index 0000000000..66dc66933b --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/pcode/DebuggerPcodeStepperProvider.java @@ -0,0 +1,679 @@ +/* ### + * 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.pcode; + +import java.awt.*; +import java.math.BigInteger; +import java.util.*; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.stream.Collectors; + +import javax.swing.*; +import javax.swing.table.*; + +import org.apache.commons.lang3.StringUtils; + +import docking.action.DockingAction; +import docking.widgets.table.*; +import docking.widgets.table.DefaultEnumeratedColumnTableModel.EnumeratedTableColumn; +import ghidra.GhidraOptions; +import ghidra.app.plugin.core.debug.DebuggerCoordinates; +import ghidra.app.plugin.core.debug.DebuggerPluginPackage; +import ghidra.app.plugin.core.debug.gui.DebuggerResources; +import ghidra.app.plugin.core.debug.gui.pcode.UniqueRow.RefType; +import ghidra.app.plugin.core.debug.service.emulation.DebuggerTracePcodeEmulator; +import ghidra.app.services.DebuggerEmulationService; +import ghidra.app.services.DebuggerTraceManagerService; +import ghidra.async.SwingExecutorService; +import ghidra.base.widgets.table.DataTypeTableCellEditor; +import ghidra.docking.settings.Settings; +import ghidra.framework.options.AutoOptions; +import ghidra.framework.options.annotation.*; +import ghidra.framework.plugintool.AutoService; +import ghidra.framework.plugintool.ComponentProviderAdapter; +import ghidra.framework.plugintool.annotation.AutoServiceConsumed; +import ghidra.pcode.emu.PcodeThread; +import ghidra.pcode.exec.PcodeExecutorState; +import ghidra.pcode.exec.PcodeFrame; +import ghidra.program.model.data.DataType; +import ghidra.program.model.lang.Language; +import ghidra.program.model.pcode.PcodeOp; +import ghidra.program.model.pcode.Varnode; +import ghidra.trace.model.Trace; +import ghidra.trace.model.time.TraceSchedule; +import ghidra.util.ColorUtils; +import ghidra.util.HTMLUtilities; +import ghidra.util.database.UndoableTransaction; +import ghidra.util.table.GhidraTable; +import ghidra.util.table.GhidraTableFilterPanel; +import ghidra.util.table.column.AbstractGColumnRenderer; + +public class DebuggerPcodeStepperProvider extends ComponentProviderAdapter { + private static final String BACKGROUND_COLOR = "Background Color"; + private static final String ADDRESS_COLOR = "Address Color"; + private static final String CONSTANT_COLOR = "Constant Color"; + private static final String REGISTERS_COLOR = "Registers Color"; + private static final String LABELS_LOCAL_COLOR = "Labels, Local Color"; + private static final String MNEMONIC_COLOR = "Mnemonic Color"; + + protected static final Comparator UNIQUE_COMPARATOR = (u1, u2) -> { + assert u1.isUnique() && u2.isUnique(); + return u1.getAddress().compareTo(u2.getAddress()); + }; + + protected enum PcodeTableColumns implements EnumeratedTableColumn { + SEQUENCE("Sequence", Integer.class, PcodeRow::getSequence), + CODE("Code", String.class, PcodeRow::getCode); + + private final String header; + private final Function getter; + private final Class cls; + + PcodeTableColumns(String header, Class cls, Function getter) { + this.header = header; + this.cls = cls; + this.getter = getter; + } + + @Override + public String getHeader() { + return header; + } + + @Override + public Class getValueClass() { + return cls; + } + + @Override + public Object getValueOf(PcodeRow row) { + return getter.apply(row); + } + + @Override + public boolean isSortable() { + return this == SEQUENCE; // HACK + } + } + + protected static class PcodeTableModel + extends DefaultEnumeratedColumnTableModel { + public PcodeTableModel() { + super("p-code", PcodeTableColumns.class); + } + + @Override + public List defaultSortOrder() { + return List.of(PcodeTableColumns.SEQUENCE); + } + } + + protected enum UniqueTableColumns + implements EnumeratedTableColumn { + REF("Ref", RefType.class, UniqueRow::getRefType), + UNIQUE("Unique", String.class, UniqueRow::getName), + BYTES("Bytes", String.class, UniqueRow::getBytes), + VALUE("Value", BigInteger.class, UniqueRow::getValue), + TYPE("Type", DataType.class, UniqueRow::getDataType, UniqueRow::setDataType), + REPR("Repr", String.class, UniqueRow::getValueRepresentation); + + private final String header; + private final Function getter; + private final BiConsumer setter; + private final Class cls; + + @SuppressWarnings("unchecked") + UniqueTableColumns(String header, Class cls, Function getter, + BiConsumer setter) { + this.header = header; + this.cls = cls; + this.getter = getter; + this.setter = (BiConsumer) setter; + } + + UniqueTableColumns(String header, Class cls, Function getter) { + this(header, cls, getter, null); + } + + @Override + public Class getValueClass() { + return cls; + } + + @Override + public Object getValueOf(UniqueRow row) { + return getter.apply(row); + } + + @Override + public String getHeader() { + return header; + } + + @Override + public void setValueOf(UniqueRow row, Object value) { + setter.accept(row, value); + } + + @Override + public boolean isEditable(UniqueRow row) { + return setter != null; + } + } + + protected static class UniqueTableModel + extends DefaultEnumeratedColumnTableModel { + public UniqueTableModel() { + super("Unique", UniqueTableColumns.class); + } + + @Override + public List defaultSortOrder() { + return List.of(UniqueTableColumns.UNIQUE); + } + } + + class UniqueDataTypeEditor extends DataTypeTableCellEditor { + public UniqueDataTypeEditor() { + super(plugin.getTool()); + } + + @Override + protected DataType resolveSelection(DataType dataType) { + if (dataType == null) { + return null; + } + try (UndoableTransaction tid = + UndoableTransaction.start(current.getTrace(), "Resolve DataType", true)) { + return current.getTrace().getDataTypeManager().resolve(dataType, null); + } + } + } + + class CounterBackgroundCellRenderer extends AbstractGColumnRenderer { + Color foregroundColor = getForeground(); + + @Override + public Component getTableCellRendererComponent(GTableCellRenderingData data) { + super.getTableCellRendererComponent(data); + setForeground(pcodeTable.getForeground()); + boolean isCurrent = counter == data.getRowModelIndex(); + if (data.isSelected()) { + if (isCurrent) { + setBackground(ColorUtils.blend(counterColor, cursorColor, 0.5f)); + } + // else background is already set. Leave it alone + } + else if (isCurrent) { + setBackground(counterColor); + } + else { + setBackground(pcodeTable.getBackground()); + setOpaque(true); + } + setBorder(noFocusBorder); + return this; + } + + @Override + public String getFilterString(String t, Settings settings) { + return t; + } + } + + class PcodeCellRenderer extends CounterBackgroundCellRenderer { + { + setHTMLRenderingEnabled(true); + } + + @Override + protected void configureFont(JTable table, TableModel model, int column) { + setFont(fixedWidthFont); + } + + @Override + public Component getTableCellRendererComponent(GTableCellRenderingData data) { + super.getTableCellRendererComponent(data); + setText(injectStyle(getText())); + return this; + } + + String injectStyle(String html) { + if (StringUtils.startsWithIgnoreCase(html, "")) { + return style + html.substring("".length()); + } + return html; + } + } + + class UniqueRefCellRenderer extends AbstractGColumnRenderer { + @Override + public Component getTableCellRendererComponent(GTableCellRenderingData data) { + super.getTableCellRendererComponent(data); + setText(""); + switch ((RefType) data.getValue()) { + case NONE: + setIcon(null); + break; + case READ: + setIcon(DebuggerResources.ICON_UNIQUE_REF_READ); + break; + case WRITE: + setIcon(DebuggerResources.ICON_UNIQUE_REF_WRITE); + break; + case READ_WRITE: + setIcon(DebuggerResources.ICON_UNIQUE_REF_RW); + break; + default: + throw new AssertionError(); + } + return this; + } + + @Override + public String getFilterString(RefType t, Settings settings) { + return t.name(); + } + } + + protected static String createColoredStyle(String cls, Color color) { + if (color == null) { + return ""; + } + return " ." + cls + " { color:" + HTMLUtilities.toHexString(color) + "; }"; + } + + protected static boolean sameCoordinates(DebuggerCoordinates a, DebuggerCoordinates b) { + if (!Objects.equals(a.getTrace(), b.getTrace())) { + return false; + } + if (!Objects.equals(a.getTime(), b.getTime())) { + return false; + } + if (!Objects.equals(a.getThread(), b.getThread())) { + return false; + } + return true; + } + + private final DebuggerPcodeStepperPlugin plugin; + + DebuggerCoordinates current = DebuggerCoordinates.NOWHERE; + DebuggerCoordinates previous = DebuggerCoordinates.NOWHERE; + int counter; + + @AutoServiceConsumed + private DebuggerTraceManagerService traceManager; + @AutoServiceConsumed // NB. also by method + private DebuggerEmulationService emulationService; + @SuppressWarnings("unused") + private AutoService.Wiring autoServiceWiring; + + @AutoOptionDefined( + name = DebuggerResources.OPTION_NAME_COLORS_PCODE_COUNTER, + description = "Background color for the current p-code operation", + help = @HelpInfo(anchor = "colors")) + private Color counterColor = DebuggerResources.DEFAULT_COLOR_PCODE_COUNTER; + + private Color backgroundColor; + private Color cursorColor; + private Color addressColor; + private Color constantColor; + private Color registerColor; + private Color uniqueColor; + private Color opColor; + + @SuppressWarnings("unused") + private AutoOptions.Wiring autoOptionsWiring; + + String style = ""; + + JSplitPane mainPanel = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); + + GhidraTable uniqueTable; + UniqueTableModel uniqueTableModel = new UniqueTableModel(); + private GhidraTableFilterPanel uniqueFilterPanel; + + GhidraTable pcodeTable; + PcodeTableModel pcodeTableModel = new PcodeTableModel(); + // No filter panel on p-code + + DockingAction actionStepBackward; + DockingAction actionStepForward; + + public DebuggerPcodeStepperProvider(DebuggerPcodeStepperPlugin plugin) { + super(plugin.getTool(), DebuggerResources.TITLE_PROVIDER_PCODE, plugin.getName(), null); + this.plugin = plugin; + + this.autoServiceWiring = AutoService.wireServicesConsumed(plugin, this); + this.autoOptionsWiring = AutoOptions.wireOptions(plugin, this); + + setIcon(DebuggerResources.ICON_PROVIDER_PCODE); + setHelpLocation(DebuggerResources.HELP_PROVIDER_PCODE); + setWindowMenuGroup(DebuggerPluginPackage.NAME); + + buildMainPanel(); + + createActions(); + + setVisible(true); + contextChanged(); + } + + @AutoOptionConsumed( + name = DebuggerResources.OPTION_NAME_COLORS_PCODE_COUNTER) + private void setCounterColor() { + pcodeTableModel.fireTableDataChanged(); + } + + @AutoOptionConsumed( + category = GhidraOptions.CATEGORY_BROWSER_DISPLAY, + name = BACKGROUND_COLOR) + private void setBackgroundColor(Color backgroundColor) { + this.backgroundColor = backgroundColor; + if (pcodeTable != null) { + pcodeTable.setBackground(backgroundColor); + } + } + + @AutoOptionConsumed( + category = GhidraOptions.CATEGORY_BROWSER_FIELDS, + name = GhidraOptions.HIGHLIGHT_CURSOR_LINE_COLOR) + private void setCursorColor(Color cursorColor) { + this.cursorColor = cursorColor; + if (pcodeTable != null) { + pcodeTable.setSelectionBackground(cursorColor); + } + } + + @AutoOptionConsumed( + category = GhidraOptions.CATEGORY_BROWSER_DISPLAY, + name = ADDRESS_COLOR) + private void setAddressColor(Color addressColor) { + this.addressColor = addressColor; + recomputeStyle(); + } + + @AutoOptionConsumed( + category = GhidraOptions.CATEGORY_BROWSER_DISPLAY, + name = CONSTANT_COLOR) + private void setConstantColor(Color constantColor) { + this.constantColor = constantColor; + recomputeStyle(); + } + + @AutoOptionConsumed( + category = GhidraOptions.CATEGORY_BROWSER_DISPLAY, + name = REGISTERS_COLOR) + private void setRegisterColor(Color registerColor) { + this.registerColor = registerColor; + recomputeStyle(); + } + + @AutoOptionConsumed( + category = GhidraOptions.CATEGORY_BROWSER_DISPLAY, + name = LABELS_LOCAL_COLOR) + private void setUniqueColor(Color uniqueColor) { + this.uniqueColor = uniqueColor; + recomputeStyle(); + } + + @AutoOptionConsumed( + category = GhidraOptions.CATEGORY_BROWSER_DISPLAY, + name = MNEMONIC_COLOR) + private void setOpColor(Color opColor) { + this.opColor = opColor; + recomputeStyle(); + } + + protected void recomputeStyle() { + StringBuilder sb = new StringBuilder(""); // NB. should already be at end + style = sb.toString(); + pcodeTableModel.fireTableDataChanged(); + } + + protected void buildMainPanel() { + JPanel pcodePanel = new JPanel(new BorderLayout()); + pcodeTable = new GhidraTable(pcodeTableModel); + pcodePanel.add(new JScrollPane(pcodeTable)); + mainPanel.setLeftComponent(pcodePanel); + + JPanel uniquePanel = new JPanel(new BorderLayout()); + uniqueTable = new GhidraTable(uniqueTableModel); + uniquePanel.add(new JScrollPane(uniqueTable)); + uniqueFilterPanel = new GhidraTableFilterPanel<>(uniqueTable, uniqueTableModel); + uniquePanel.add(uniqueFilterPanel, BorderLayout.SOUTH); + mainPanel.setRightComponent(uniquePanel); + + pcodeTable.setTableHeader(null); + pcodeTable.setBackground(backgroundColor); + pcodeTable.setSelectionBackground(cursorColor); + pcodeTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + pcodeTable.getSelectionModel().addListSelectionListener(evt -> { + if (evt.getValueIsAdjusting()) { + return; + } + uniqueTableModel.fireTableDataChanged(); + }); + + TableColumnModel pcodeColModel = pcodeTable.getColumnModel(); + TableColumn seqCol = pcodeColModel.getColumn(PcodeTableColumns.SEQUENCE.ordinal()); + seqCol.setCellRenderer(new CounterBackgroundCellRenderer()); + seqCol.setMinWidth(24); + seqCol.setMaxWidth(24); + TableColumn codeCol = pcodeColModel.getColumn(PcodeTableColumns.CODE.ordinal()); + codeCol.setCellRenderer(new PcodeCellRenderer()); + //codeCol.setPreferredWidth(75); + + TableColumnModel uniqueColModel = uniqueTable.getColumnModel(); + TableColumn refCol = uniqueColModel.getColumn(UniqueTableColumns.REF.ordinal()); + refCol.setCellRenderer(new UniqueRefCellRenderer()); + refCol.setMinWidth(24); + refCol.setMaxWidth(24); + TableColumn uniqCol = uniqueColModel.getColumn(UniqueTableColumns.UNIQUE.ordinal()); + uniqCol.setPreferredWidth(45); + TableColumn bytesCol = uniqueColModel.getColumn(UniqueTableColumns.BYTES.ordinal()); + bytesCol.setCellRenderer(CustomToStringCellRenderer.MONO_OBJECT); + bytesCol.setPreferredWidth(65); + TableColumn valCol = uniqueColModel.getColumn(UniqueTableColumns.VALUE.ordinal()); + valCol.setCellRenderer(CustomToStringCellRenderer.MONO_BIG_HEX); // TODO: Changed coloring + valCol.setPreferredWidth(45); + TableColumn typeCol = uniqueColModel.getColumn(UniqueTableColumns.TYPE.ordinal()); + typeCol.setCellEditor(new UniqueDataTypeEditor()); + typeCol.setPreferredWidth(45); + TableColumn reprCol = uniqueColModel.getColumn(UniqueTableColumns.REPR.ordinal()); + reprCol.setPreferredWidth(45); + } + + protected void createActions() { + actionStepBackward = DebuggerResources.StepPcodeBackwardAction.builder(plugin) + .enabledWhen(c -> current.getTrace() != null && current.getTime().pTickCount() != 0) + .onAction(c -> stepBackwardActivated()) + .buildAndInstallLocal(this); + actionStepForward = DebuggerResources.StepPcodeForwardAction.builder(plugin) + .enabledWhen( + c -> current.getThread() != null) + .onAction(c -> stepForwardActivated()) + .buildAndInstallLocal(this); + } + + private void stepBackwardActivated() { + if (current.getTrace() == null) { + return; + } + TraceSchedule time = current.getTime().steppedPcodeBackward(1); + if (time == null) { + return; + } + traceManager.activateTime(time); + } + + private void stepForwardActivated() { + if (current.getThread() == null) { + return; + } + TraceSchedule time = current.getTime().steppedPcodeForward(current.getThread(), 1); + traceManager.activateTime(time); + } + + @Override + public JComponent getComponent() { + return mainPanel; + } + + public void coordinatesActivated(DebuggerCoordinates coordinates) { + if (sameCoordinates(current, coordinates)) { + current = coordinates; + return; + } + previous = current; + current = coordinates; + + doLoadPcodeFrame(); + + setSubTitle(current.getTime().toString()); + + contextChanged(); + } + + protected void populateSingleton(PcodeRow row) { + counter = 0; + pcodeTableModel.clear(); + pcodeTableModel.add(row); + uniqueTableModel.clear(); + } + + protected void populateFromFrame(PcodeFrame frame, PcodeExecutorState state) { + populatePcode(frame); + populateUnique(frame, state); + } + + protected void populatePcode(PcodeFrame frame) { + Language language = current.getTrace().getBaseLanguage(); + int index = frame.index(); + List toAdd = frame.getCode() + .stream() + .map(op -> new OpPcodeRow(language, op, index == op.getSeqnum().getTime())) + .collect(Collectors.toCollection(ArrayList::new)); + if (frame.isBranch()) { + counter = toAdd.size(); + toAdd.add(new BranchPcodeRow(counter, frame.getBranched())); + } + else if (frame.isFallThrough()) { + counter = toAdd.size(); + toAdd.add(new FallthroughPcodeRow(counter)); + } + else { + counter = index; + } + pcodeTableModel.clear(); + pcodeTableModel.addAll(toAdd); + pcodeTable.getSelectionModel().setSelectionInterval(counter, counter); + pcodeTable.scrollToSelectedRow(); + } + + protected void populateUnique(PcodeFrame frame, PcodeExecutorState state) { + Language language = current.getTrace().getBaseLanguage(); + // NOTE: They may overlap. I don't think I care. + Set uniques = new TreeSet<>(UNIQUE_COMPARATOR); + for (PcodeOp op : frame.getCode()) { + Varnode out = op.getOutput(); + if (out != null && out.isUnique()) { + uniques.add(out); + } + for (Varnode in : op.getInputs()) { + if (in.isUnique()) { + uniques.add(in); + } + } + } + // TODO: Highlight uniques that the selected op(s) reference + // (including overlaps) + // TODO: Permit modification of unique variables + List toAdd = + uniques.stream() + .map(u -> new UniqueRow(this, language, state, u)) + .collect(Collectors.toList()); + uniqueTableModel.clear(); + uniqueTableModel.addAll(toAdd); + } + + protected void doLoadPcodeFrame() { + if (emulationService == null) { + return; + } + DebuggerCoordinates current = this.current; // Volatile, also after background + Trace trace = current.getTrace(); + if (trace == null) { + return; + } + if (current.getThread() == null) { + populateSingleton(EnumPcodeRow.NO_THREAD); + return; + } + TraceSchedule time = current.getTime(); + if (time.pTickCount() == 0) { + populateSingleton(EnumPcodeRow.DECODE); + return; + } + DebuggerTracePcodeEmulator emu = emulationService.getCachedEmulator(trace, time); + if (emu != null) { + doLoadPcodeFrameFromEmulator(emu); + return; + } + emulationService.backgroundEmulate(trace, time).thenAcceptAsync(__ -> { + if (current != this.current) { + return; + } + doLoadPcodeFrameFromEmulator(emulationService.getCachedEmulator(trace, time)); + }, SwingExecutorService.INSTANCE); + } + + protected void doLoadPcodeFrameFromEmulator(DebuggerTracePcodeEmulator emu) { + PcodeThread thread = emu.getThread(current.getThread().getPath(), false); + if (thread == null) { + /** + * Happens when focus is on a thread not stepped in the schedule. Stepping it would + * create it and decode its first instruction. + */ + populateSingleton(EnumPcodeRow.DECODE); + return; + } + PcodeFrame frame = thread.getFrame(); + if (frame == null) { + /** + * Happens when an instruction is completed via p-code stepping, but the next + * instruction has not been decoded, yet. + */ + populateSingleton(EnumPcodeRow.DECODE); + return; + } + populateFromFrame(frame, thread.getState()); + } + + @AutoServiceConsumed + private void setEmulationService(DebuggerEmulationService emulationService) { + doLoadPcodeFrame(); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/pcode/EnumPcodeRow.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/pcode/EnumPcodeRow.java new file mode 100644 index 0000000000..854b31122f --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/pcode/EnumPcodeRow.java @@ -0,0 +1,43 @@ +/* ### + * 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.pcode; + +import ghidra.program.model.pcode.PcodeOp; + +public enum EnumPcodeRow implements PcodeRow { + NO_THREAD("no thread selected"), DECODE("decode instruction"); + + private final String message; + + private EnumPcodeRow(String message) { + this.message = "(" + message + ")"; + } + + @Override + public int getSequence() { + return 0; + } + + @Override + public String getCode() { + return message; + } + + @Override + public PcodeOp getOp() { + return null; + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/pcode/FallthroughPcodeRow.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/pcode/FallthroughPcodeRow.java new file mode 100644 index 0000000000..8965b76855 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/pcode/FallthroughPcodeRow.java @@ -0,0 +1,41 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.debug.gui.pcode; + +import ghidra.program.model.pcode.PcodeOp; + +public class FallthroughPcodeRow implements PcodeRow { + private final int sequence; + + public FallthroughPcodeRow(int sequence) { + this.sequence = sequence; + } + + @Override + public int getSequence() { + return sequence; + } + + @Override + public String getCode() { + return "(fall-through)"; + } + + @Override + public PcodeOp getOp() { + return null; + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/pcode/OpPcodeRow.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/pcode/OpPcodeRow.java new file mode 100644 index 0000000000..81a3165165 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/pcode/OpPcodeRow.java @@ -0,0 +1,51 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.debug.gui.pcode; + +import ghidra.pcode.exec.PcodeProgram; +import ghidra.program.model.lang.Language; +import ghidra.program.model.pcode.PcodeOp; + +public class OpPcodeRow implements PcodeRow { + protected final Language language; + protected final PcodeOp op; + protected final boolean isNext; + + public OpPcodeRow(Language language, PcodeOp op, boolean isNext) { + this.language = language; + this.op = op; + this.isNext = isNext; + } + + @Override + public int getSequence() { + return op.getSeqnum().getTime(); + } + + @Override + public String getCode() { + return "" + PcodeProgram.opToString(language, op, true) + ""; + } + + public boolean isNext() { + return isNext; + } + + @Override + public PcodeOp getOp() { + return op; + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/pcode/PcodeRow.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/pcode/PcodeRow.java new file mode 100644 index 0000000000..a9c4790cd2 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/pcode/PcodeRow.java @@ -0,0 +1,26 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.debug.gui.pcode; + +import ghidra.program.model.pcode.PcodeOp; + +public interface PcodeRow { + int getSequence(); + + String getCode(); + + PcodeOp getOp(); +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/pcode/UniqueRow.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/pcode/UniqueRow.java new file mode 100644 index 0000000000..37775dd2b5 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/pcode/UniqueRow.java @@ -0,0 +1,146 @@ +/* ### + * 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.pcode; + +import java.math.BigInteger; +import java.util.stream.Stream; + +import ghidra.docking.settings.SettingsImpl; +import ghidra.pcode.exec.PcodeExecutorState; +import ghidra.pcode.exec.PcodeProgram; +import ghidra.pcode.utils.Utils; +import ghidra.program.model.address.*; +import ghidra.program.model.data.DataType; +import ghidra.program.model.lang.Language; +import ghidra.program.model.mem.ByteMemBufferImpl; +import ghidra.program.model.mem.MemBuffer; +import ghidra.program.model.pcode.PcodeOp; +import ghidra.program.model.pcode.Varnode; +import ghidra.util.NumericUtilities; + +public class UniqueRow { + public enum RefType { + NONE, READ, WRITE, READ_WRITE; + + static RefType fromRW(boolean isRead, boolean isWrite) { + if (isRead) { + if (isWrite) { + return READ_WRITE; + } + else { + return READ; + } + } + else { + if (isWrite) { + return WRITE; + } + else { + return NONE; + } + } + } + } + + protected final DebuggerPcodeStepperProvider provider; + protected final Language language; + protected final PcodeExecutorState state; + protected final Varnode vn; + + protected DataType dataType; + + public UniqueRow(DebuggerPcodeStepperProvider provider, Language language, + PcodeExecutorState state, Varnode vn) { + if (!vn.isUnique()) { + throw new AssertionError("Only uniques allowed in unique table"); + } + this.provider = provider; + this.language = language; + this.state = state; + this.vn = vn; + } + + protected static AddressRange rangeOf(Varnode vn) { + try { + return new AddressRangeImpl(vn.getAddress(), vn.getSize()); + } + catch (AddressOverflowException e) { + throw new AssertionError(e); + } + } + + protected static boolean overlap(Varnode vn1, Varnode vn2) { + return rangeOf(vn1).intersects(rangeOf(vn2)); + } + + public RefType getRefType() { + int index = provider.pcodeTable.getSelectedRow(); + if (index == -1) { + return RefType.NONE; + } + PcodeRow row = provider.pcodeTableModel.getRowObject(index); + PcodeOp op = row.getOp(); + if (op == null) { + return RefType.NONE; + } + boolean isRead = Stream.of(op.getInputs()).anyMatch(in -> overlap(in, vn)); + Varnode out = op.getOutput(); + boolean isWrite = out != null && overlap(out, vn); + return RefType.fromRW(isRead, isWrite); + } + + public String getName() { + return PcodeProgram.uniqueToString(vn, false); + } + + public String getBytes() { + // TODO: Could keep value cached? + byte[] bytes = state.getVar(vn); + if (bytes == null) { + return "??"; + } + if (bytes.length > 20) { + return NumericUtilities.convertBytesToString(bytes, 0, 20, " ") + " ..."; + } + return NumericUtilities.convertBytesToString(bytes, " "); + } + + public BigInteger getValue() { + byte[] bytes = state.getVar(vn); + return Utils.bytesToBigInteger(bytes, bytes.length, language.isBigEndian(), false); + } + + public DataType getDataType() { + return dataType; + } + + public void setDataType(DataType dataType) { + this.dataType = dataType; + } + + public String getValueRepresentation() { + // TODO: Could compute this upon setting data type? + if (dataType == null) { + return ""; + } + byte[] bytes = state.getVar(vn); + if (bytes == null) { + return "??"; + } + MemBuffer buffer = new ByteMemBufferImpl(vn.getAddress(), bytes, language.isBigEndian()); + return dataType.getRepresentation(buffer, SettingsImpl.NO_SETTINGS, bytes.length); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/register/DebuggerRegistersPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/register/DebuggerRegistersPlugin.java index 58d1a70dfb..cf52e441f7 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/register/DebuggerRegistersPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/register/DebuggerRegistersPlugin.java @@ -35,22 +35,22 @@ import ghidra.program.util.DefaultLanguageService; import ghidra.trace.model.Trace; import ghidra.util.Msg; -@PluginInfo( // - shortDescription = "Debugger registers manager", // - description = "GUI to view and modify register values", // - category = PluginCategoryNames.DEBUGGER, // - packageName = DebuggerPluginPackage.NAME, // - status = PluginStatus.RELEASED, // - eventsConsumed = { TraceActivatedPluginEvent.class, // - TraceClosedPluginEvent.class, // - }, // - servicesRequired = { // - DebuggerModelService.class, // - DebuggerTraceManagerService.class, // - MarkerService.class, // TODO - DataTypeManagerService.class, // For DataType selection field - } // -) +@PluginInfo( + shortDescription = "Debugger registers manager", + description = "GUI to view and modify register values", + category = PluginCategoryNames.DEBUGGER, + packageName = DebuggerPluginPackage.NAME, + status = PluginStatus.RELEASED, + eventsConsumed = { + TraceActivatedPluginEvent.class, + TraceClosedPluginEvent.class, + }, + servicesRequired = { + DebuggerModelService.class, + DebuggerTraceManagerService.class, + MarkerService.class, // TODO + DataTypeManagerService.class, // For DataType selection field + }) public class DebuggerRegistersPlugin extends AbstractDebuggerPlugin { private static final String KEY_SELECTION_BY_CSPEC = "selectionByCSpec"; private static final String KEY_FAVORITES_BY_CSPEC = "favoritesByCSpec"; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/register/DebuggerRegistersProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/register/DebuggerRegistersProvider.java index f522f60684..8972868699 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/register/DebuggerRegistersProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/register/DebuggerRegistersProvider.java @@ -70,9 +70,9 @@ import ghidra.trace.model.Trace.*; import ghidra.trace.model.listing.*; import ghidra.trace.model.memory.TraceMemoryRegisterSpace; import ghidra.trace.model.memory.TraceMemoryState; +import ghidra.trace.model.program.TraceProgramView; import ghidra.trace.model.thread.TraceThread; -import ghidra.trace.util.TraceAddressSpace; -import ghidra.trace.util.TraceRegisterUtils; +import ghidra.trace.util.*; import ghidra.util.Msg; import ghidra.util.Swing; import ghidra.util.data.DataTypeParser.AllowedDataTypes; @@ -171,13 +171,12 @@ public class DebuggerRegistersProvider extends ComponentProviderAdapter if (!Objects.equals(a.getThread(), b.getThread())) { return false; } - if (!Objects.equals(a.getSnap(), b.getSnap())) { + if (!Objects.equals(a.getTime(), b.getTime())) { return false; } if (!Objects.equals(a.getFrame(), b.getFrame())) { return false; } - // TODO: Ticks return true; } @@ -185,6 +184,7 @@ public class DebuggerRegistersProvider extends ComponentProviderAdapter public TraceChangeListener() { listenForUntyped(DomainObject.DO_OBJECT_RESTORED, e -> objectRestored(e)); listenFor(TraceMemoryBytesChangeType.CHANGED, this::registerValueChanged); + listenFor(TraceMemoryStateChangeType.CHANGED, this::registerStateChanged); listenFor(TraceCodeChangeType.ADDED, this::registerTypeAdded); listenFor(TraceCodeChangeType.DATA_TYPE_REPLACED, this::registerTypeReplaced); listenFor(TraceCodeChangeType.LIFESPAN_CHANGED, this::registerTypeLifespanChanged); @@ -211,9 +211,11 @@ public class DebuggerRegistersProvider extends ComponentProviderAdapter if (!isVisible(space)) { return false; } - if (!range.getLifespan().contains(current.getSnap())) { + TraceProgramView view = current.getView(); + if (view == null || !view.getViewport().containsAnyUpper(range.getLifespan())) { return false; } + // Probably not worth checking for occlusion here. Just a little refresh waste. return true; } @@ -238,13 +240,21 @@ public class DebuggerRegistersProvider extends ComponentProviderAdapter refreshRange(range.getRange()); } + private void registerStateChanged(TraceAddressSpace space, TraceAddressSnapRange range, + TraceMemoryState oldState, TraceMemoryState newState) { + if (!isVisible(space, range)) { + return; + } + recomputeViewKnown(); + refreshRange(range.getRange()); + } + private void registerTypeAdded(TraceAddressSpace space, TraceAddressSnapRange range, TraceCodeUnit oldIsNull, TraceCodeUnit newUnit) { if (!isVisible(space, range)) { return; } refreshRange(range.getRange()); - } private void registerTypeReplaced(TraceAddressSpace space, TraceAddressSnapRange range, @@ -260,10 +270,15 @@ public class DebuggerRegistersProvider extends ComponentProviderAdapter if (!isVisible(space)) { return; } - long snap = current.getSnap(); - if (oldSpan.contains(snap) == newSpan.contains(snap)) { + TraceProgramView view = current.getView(); + if (view == null) { return; } + TraceTimeViewport viewport = view.getViewport(); + if (viewport.containsAnyUpper(oldSpan) == viewport.containsAnyUpper(newSpan)) { + return; + } + // A little waste if occluded, but probably cheaper than checking. AddressRange range = new AddressRangeImpl(unit.getMinAddress(), unit.getMaxAddress()); refreshRange(range); // Slightly wasteful, as we already have the data unit } @@ -418,6 +433,7 @@ public class DebuggerRegistersProvider extends ComponentProviderAdapter DockingAction actionClearDataType; DebuggerRegisterActionContext myActionContext; + AddressSetView viewKnown; protected DebuggerRegistersProvider(final DebuggerRegistersPlugin plugin, Map> selectionByCSpec, @@ -710,6 +726,7 @@ public class DebuggerRegistersProvider extends ComponentProviderAdapter doSetRecorder(current.getRecorder()); updateSubTitle(); + recomputeViewKnown(); loadRegistersAndValues(); contextChanged(); //checkEditsEnabled(); @@ -725,16 +742,10 @@ public class DebuggerRegistersProvider extends ComponentProviderAdapter } boolean canWriteTarget() { + if (!current.isAliveAndPresent()) { + return false; + } TraceRecorder recorder = current.getRecorder(); - if (recorder == null) { - return false; - } - if (!recorder.isRecording()) { - return false; - } - if (recorder.getSnap() != current.getSnap()) { - return false; - } TargetRegisterBank targetRegs = recorder.getTargetRegisterBank(current.getThread(), current.getFrame()); if (targetRegs == null) { @@ -760,8 +771,7 @@ public class DebuggerRegistersProvider extends ComponentProviderAdapter if (regs == null) { return BigInteger.ZERO; } - long snap = current.getSnap(); - return regs.getValue(snap, register).getUnsignedValue(); + return regs.getViewValue(current.getViewSnap(), register).getUnsignedValue(); } void writeRegisterValue(Register register, BigInteger value) { @@ -840,15 +850,24 @@ public class DebuggerRegistersProvider extends ComponentProviderAdapter return TraceRegisterUtils.getValueRepresentationHackPointer(data); } - boolean isRegisterKnown(Register register) { + void recomputeViewKnown() { TraceMemoryRegisterSpace regs = getRegisterMemorySpace(false); - if (regs == null) { + TraceProgramView view = current.getView(); + if (regs == null || view == null) { + viewKnown = null; + return; + } + viewKnown = new AddressSet(view.getViewport() + .unionedAddresses(snap -> regs.getAddressesWithState(snap, + state -> state == TraceMemoryState.KNOWN))); + } + + boolean isRegisterKnown(Register register) { + if (viewKnown == null) { return false; } AddressRange range = TraceRegisterUtils.rangeForRegister(register); - return regs - .getAddressesWithState(current.getSnap(), state -> state == TraceMemoryState.KNOWN) - .contains(range.getMinAddress(), range.getMaxAddress()); + return viewKnown.contains(range.getMinAddress(), range.getMaxAddress()); } boolean isRegisterChanged(Register register) { @@ -861,20 +880,14 @@ public class DebuggerRegistersProvider extends ComponentProviderAdapter if (!isRegisterKnown(register)) { return false; } - TraceMemoryRegisterSpace curVals = getRegisterMemorySpace(current, false); - TraceMemoryRegisterSpace prevVals = getRegisterMemorySpace(previous, false); - if (prevVals == null) { + TraceMemoryRegisterSpace curSpace = getRegisterMemorySpace(current, false); + TraceMemoryRegisterSpace prevSpace = getRegisterMemorySpace(previous, false); + if (prevSpace == null) { return false; } - // is register known at previous? // Do I care? - /*AddressRange range = TraceRegisterUtils.rangeForRegister(register); - if (!prevVals - .getAddressesWithState(previous.getSnap(), state -> state == TraceMemoryState.KNOWN) - .contains(range.getMinAddress(), range.getMaxAddress())) { - return false; - }*/ - return !Objects.equals(curVals.getValue(current.getSnap(), register), - prevVals.getValue(previous.getSnap(), register)); + RegisterValue curRegVal = curSpace.getViewValue(current.getViewSnap(), register); + RegisterValue prevRegVal = prevSpace.getViewValue(previous.getViewSnap(), register); + return !Objects.equals(curRegVal, prevRegVal); } private boolean computeEditsEnabled() { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackProvider.java index e17a73151e..c8e6671eca 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackProvider.java @@ -41,13 +41,15 @@ import ghidra.program.model.address.Address; import ghidra.program.model.lang.Register; import ghidra.program.model.lang.RegisterValue; import ghidra.program.model.listing.Program; -import ghidra.trace.model.Trace; +import ghidra.trace.model.*; +import ghidra.trace.model.Trace.TraceMemoryBytesChangeType; import ghidra.trace.model.Trace.TraceStackChangeType; -import ghidra.trace.model.TraceDomainObjectListener; import ghidra.trace.model.memory.TraceMemoryRegisterSpace; import ghidra.trace.model.stack.TraceStack; import ghidra.trace.model.stack.TraceStackFrame; import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.util.TraceAddressSpace; +import ghidra.trace.util.TraceRegisterUtils; import ghidra.util.Swing; import ghidra.util.table.GhidraTable; import ghidra.util.table.GhidraTableFilterPanel; @@ -129,13 +131,12 @@ public class DebuggerStackProvider extends ComponentProviderAdapter { if (!Objects.equals(a.getThread(), b.getThread())) { return false; } - if (!Objects.equals(a.getSnap(), b.getSnap())) { + if (!Objects.equals(a.getTime(), b.getTime())) { return false; } if (!Objects.equals(a.getFrame(), b.getFrame())) { return false; } - // TODO: Ticks return true; } @@ -144,9 +145,14 @@ public class DebuggerStackProvider extends ComponentProviderAdapter { listenFor(TraceStackChangeType.ADDED, this::stackAdded); listenFor(TraceStackChangeType.CHANGED, this::stackChanged); listenFor(TraceStackChangeType.DELETED, this::stackDeleted); + + listenFor(TraceMemoryBytesChangeType.CHANGED, this::bytesChanged); } private void stackAdded(TraceStack stack) { + if (stack.getSnap() != current.getViewSnap()) { + return; + } TraceThread curThread = current.getThread(); if (curThread != stack.getThread()) { return; @@ -167,6 +173,35 @@ public class DebuggerStackProvider extends ComponentProviderAdapter { } loadStack(); } + + // For updating a synthetic frame + private void bytesChanged(TraceAddressSpace space, TraceAddressSnapRange range) { + TraceThread curThread = current.getThread(); + if (space.getThread() != curThread || space.getFrameLevel() != 0) { + return; + } + if (!current.getView().getViewport().containsAnyUpper(range.getLifespan())) { + return; + } + List stackData = stackTableModel.getModelData(); + if (stackData.isEmpty() || !(stackData.get(0) instanceof StackFrameRow.Synthetic)) { + return; + } + StackFrameRow.Synthetic frameRow = (StackFrameRow.Synthetic) stackData.get(0); + Trace trace = current.getTrace(); + Register pc = trace.getBaseLanguage().getProgramCounter(); + if (!TraceRegisterUtils.rangeForRegister(pc).intersects(range.getRange())) { + return; + } + TraceMemoryRegisterSpace regs = + trace.getMemoryManager().getMemoryRegisterSpace(curThread, false); + RegisterValue value = regs.getViewValue(current.getViewSnap(), pc); + Address address = trace.getBaseLanguage() + .getDefaultSpace() + .getAddress(value.getUnsignedValue().longValue()); + frameRow.updateProgramCounter(address); + stackTableModel.fireTableDataChanged(); + } } class ForFunctionsListener implements DebuggerStaticMappingChangeListener { @@ -367,7 +402,7 @@ public class DebuggerStackProvider extends ComponentProviderAdapter { return; } Register pc = curTrace.getBaseLanguage().getProgramCounter(); - RegisterValue value = regs.getValue(current.getSnap(), pc); + RegisterValue value = regs.getViewValue(current.getViewSnap(), pc); if (value == null) { contextChanged(); return; @@ -375,7 +410,7 @@ public class DebuggerStackProvider extends ComponentProviderAdapter { Address address = curTrace.getBaseLanguage() .getDefaultSpace() .getAddress(value.getUnsignedValue().longValue()); - stackTableModel.add(new StackFrameRow(this, address)); + stackTableModel.add(new StackFrameRow.Synthetic(this, address)); } protected void loadStack() { @@ -384,8 +419,9 @@ public class DebuggerStackProvider extends ComponentProviderAdapter { doSetCurrentStack(null); return; } + // TODO: getLatestViewStack? Conventionally, I don't expect any scratch stacks, yet. TraceStack stack = - current.getTrace().getStackManager().getLatestStack(curThread, current.getSnap()); + current.getTrace().getStackManager().getLatestStack(curThread, current.getViewSnap()); if (stack == null) { doSetSyntheticStack(); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/StackFrameRow.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/StackFrameRow.java index de26f0df9e..891deedd27 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/StackFrameRow.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/StackFrameRow.java @@ -27,11 +27,21 @@ import ghidra.trace.model.thread.TraceThread; import ghidra.util.database.UndoableTransaction; public class StackFrameRow { + public static class Synthetic extends StackFrameRow { + public Synthetic(DebuggerStackProvider provider, Address pc) { + super(provider, pc); + } + + public void updateProgramCounter(Address pc) { + this.pc = pc; + } + } + private final DebuggerStackProvider provider; final TraceStackFrame frame; private int level; - private Address pc; + Address pc; public StackFrameRow(DebuggerStackProvider provider, TraceStackFrame frame) { this.provider = provider; @@ -40,7 +50,7 @@ public class StackFrameRow { this.pc = frame.getProgramCounter(); } - public StackFrameRow(DebuggerStackProvider provider, Address pc) { + private StackFrameRow(DebuggerStackProvider provider, Address pc) { this.provider = provider; this.frame = null; this.level = 0; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProvider.java index 8ac4195077..2645645496 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProvider.java @@ -49,6 +49,7 @@ import ghidra.trace.model.Trace.TraceThreadChangeType; import ghidra.trace.model.TraceDomainObjectListener; import ghidra.trace.model.thread.TraceThread; import ghidra.trace.model.thread.TraceThreadManager; +import ghidra.trace.model.time.TraceSchedule; import ghidra.trace.model.time.TraceSnapshot; import ghidra.util.Swing; import ghidra.util.datastruct.CollectionChangeListener; @@ -73,26 +74,30 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter { if (!Objects.equals(a.getThread(), b.getThread())) { return false; } - if (!Objects.equals(a.getSnap(), b.getSnap())) { + if (!Objects.equals(a.getTime(), b.getTime())) { return false; } - // TODO: Ticks return true; } - protected class StepTraceBackwardAction extends AbstractStepTraceBackwardAction { - public static final String GROUP = DebuggerResources.GROUP_GENERAL; + protected class StepSnapBackwardAction extends AbstractStepSnapBackwardAction { + public static final String GROUP = DebuggerResources.GROUP_CONTROL; - public StepTraceBackwardAction() { + public StepSnapBackwardAction() { super(plugin); - setToolBarData(new ToolBarData(ICON, GROUP)); + setToolBarData(new ToolBarData(ICON, GROUP, "1")); addLocalAction(this); setEnabled(false); } @Override public void actionPerformed(ActionContext context) { - traceManager.activateSnap(current.getSnap() - 1); + if (current.getTime().isSnapOnly()) { + traceManager.activateSnap(current.getSnap() - 1); + } + else { + traceManager.activateSnap(current.getSnap()); + } } @Override @@ -100,7 +105,9 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter { if (current.getTrace() == null) { return false; } - // TODO: Can I track minSnap? + if (!current.getTime().isSnapOnly()) { + return true; + } if (current.getSnap() <= 0) { return false; } @@ -108,12 +115,80 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter { } } - protected class StepTraceForwardAction extends AbstractStepTraceForwardAction { - public static final String GROUP = DebuggerResources.GROUP_GENERAL; + protected class StepTickBackwardAction extends AbstractStepTickBackwardAction { + public static final String GROUP = DebuggerResources.GROUP_CONTROL; - public StepTraceForwardAction() { + public StepTickBackwardAction() { super(plugin); - setToolBarData(new ToolBarData(ICON, GROUP)); + setToolBarData(new ToolBarData(ICON, GROUP, "2")); + addLocalAction(this); + setEnabled(false); + } + + @Override + public void actionPerformed(ActionContext context) { + if (current.getTrace() == null) { + return; + } + TraceSchedule time = current.getTime().steppedBackward(current.getTrace(), 1); + if (time == null) { + return; + } + traceManager.activateTime(time); + } + + @Override + public boolean isEnabledForContext(ActionContext context) { + if (emulationService == null) { + return false; + } + if (current.getTrace() == null) { + return false; + } + if (current.getTime().steppedBackward(current.getTrace(), 1) == null) { + return false; + } + return true; + } + } + + protected class StepTickForwardAction extends AbstractStepTickForwardAction { + public static final String GROUP = DebuggerResources.GROUP_CONTROL; + + public StepTickForwardAction() { + super(plugin); + setToolBarData(new ToolBarData(ICON, GROUP, "3")); + addLocalAction(this); + setEnabled(false); + } + + @Override + public void actionPerformed(ActionContext context) { + if (current.getThread() == null) { + return; + } + TraceSchedule time = current.getTime().steppedForward(current.getThread(), 1); + traceManager.activateTime(time); + } + + @Override + public boolean isEnabledForContext(ActionContext context) { + if (emulationService == null) { + return false; + } + if (current.getThread() == null) { + return false; + } + return true; + } + } + + protected class StepSnapForwardAction extends AbstractStepSnapForwardAction { + public static final String GROUP = DebuggerResources.GROUP_CONTROL; + + public StepSnapForwardAction() { + super(plugin); + setToolBarData(new ToolBarData(ICON, GROUP, "4")); addLocalAction(this); setEnabled(false); } @@ -139,7 +214,7 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter { protected class SeekTracePresentAction extends AbstractSeekTracePresentAction implements BooleanChangeAdapter { - public static final String GROUP = DebuggerResources.GROUP_GENERAL; + public static final String GROUP = "zz"; public SeekTracePresentAction() { super(plugin); @@ -241,6 +316,8 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter { private DebuggerModelService modelService; @AutoServiceConsumed // NB, also by method private DebuggerTraceManagerService traceManager; + @AutoServiceConsumed // NB, also by method + private DebuggerEmulationService emulationService; @SuppressWarnings("unused") private final AutoService.Wiring autoWiring; @@ -269,8 +346,10 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter { private ActionContext myActionContext; DockingAction actionSaveTrace; - StepTraceBackwardAction actionStepTraceBackward; - StepTraceForwardAction actionStepTraceForward; + StepSnapBackwardAction actionStepSnapBackward; + StepTickBackwardAction actionStepTickBackward; + StepTickForwardAction actionStepTickForward; + StepSnapForwardAction actionStepSnapForward; SeekTracePresentAction actionSeekTracePresent; ToggleDockingAction actionSyncFocus; Set strongRefs = new HashSet<>(); // Eww @@ -323,6 +402,11 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter { contextChanged(); } + @AutoServiceConsumed + public void setEmulationService(DebuggerEmulationService emulationService) { + contextChanged(); + } + private void removeOldListeners() { if (currentTrace == null) { return; @@ -394,6 +478,9 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter { doSetTrace(current.getTrace()); doSetThread(current.getThread()); doSetSnap(current.getSnap()); + + setSubTitle(current.getTime().toString()); + contextChanged(); } @@ -568,8 +655,10 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter { protected void createActions() { // TODO: Make other actions use builder? - actionStepTraceBackward = new StepTraceBackwardAction(); - actionStepTraceForward = new StepTraceForwardAction(); + actionStepSnapBackward = new StepSnapBackwardAction(); + actionStepTickBackward = new StepTickBackwardAction(); + actionStepTickForward = new StepTickForwardAction(); + actionStepSnapForward = new StepSnapForwardAction(); actionSeekTracePresent = new SeekTracePresentAction(); actionSyncFocus = SynchronizeFocusAction.builder(plugin) .selected(traceManager != null && traceManager.isSynchronizeFocus()) diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimePlugin.java index 4c9ecd6b62..9cfa103ea9 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimePlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimePlugin.java @@ -20,21 +20,22 @@ import ghidra.app.plugin.core.debug.AbstractDebuggerPlugin; import ghidra.app.plugin.core.debug.DebuggerPluginPackage; import ghidra.app.plugin.core.debug.event.TraceActivatedPluginEvent; import ghidra.app.services.DebuggerTraceManagerService; +import ghidra.framework.options.SaveState; import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.util.PluginStatus; @PluginInfo( // - shortDescription = "Lists recorded snapshots in a trace", // - description = "Provides the component which lists snapshots and allows navigation", // - category = PluginCategoryNames.DEBUGGER, // - packageName = DebuggerPluginPackage.NAME, // - status = PluginStatus.RELEASED, // - eventsConsumed = { // - TraceActivatedPluginEvent.class // - }, // - servicesRequired = { // - DebuggerTraceManagerService.class // - } // + shortDescription = "Lists recorded snapshots in a trace", // + description = "Provides the component which lists snapshots and allows navigation", // + category = PluginCategoryNames.DEBUGGER, // + packageName = DebuggerPluginPackage.NAME, // + status = PluginStatus.RELEASED, // + eventsConsumed = { // + TraceActivatedPluginEvent.class // + }, // + servicesRequired = { // + DebuggerTraceManagerService.class // + } // ) public class DebuggerTimePlugin extends AbstractDebuggerPlugin { protected DebuggerTimeProvider provider; @@ -63,4 +64,14 @@ public class DebuggerTimePlugin extends AbstractDebuggerPlugin { provider.coordinatesActivated(ev.getActiveCoordinates()); } } + + @Override + public void writeConfigState(SaveState saveState) { + provider.writeConfigState(saveState); + } + + @Override + public void readConfigState(SaveState saveState) { + provider.readConfigState(saveState); + } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimeProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimeProvider.java index 4ac3f51214..a7a10ff5cb 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimeProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimeProvider.java @@ -19,6 +19,8 @@ import static ghidra.app.plugin.core.debug.gui.DebuggerResources.*; import java.awt.BorderLayout; import java.awt.event.MouseEvent; +import java.lang.invoke.MethodHandles; +import java.util.Collection; import java.util.Objects; import java.util.function.BiConsumer; import java.util.function.Function; @@ -31,16 +33,19 @@ import com.google.common.collect.Collections2; import docking.ActionContext; import docking.action.DockingActionIf; +import docking.action.ToggleDockingAction; import docking.widgets.table.*; import docking.widgets.table.DefaultEnumeratedColumnTableModel.EnumeratedTableColumn; import ghidra.app.plugin.core.debug.DebuggerCoordinates; import ghidra.app.plugin.core.debug.DebuggerPluginPackage; +import ghidra.app.plugin.core.debug.gui.DebuggerResources; import ghidra.app.plugin.core.debug.gui.DebuggerSnapActionContext; import ghidra.app.services.DebuggerTraceManagerService; import ghidra.framework.model.DomainObject; -import ghidra.framework.plugintool.AutoService; +import ghidra.framework.options.SaveState; +import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.AutoService.Wiring; -import ghidra.framework.plugintool.ComponentProviderAdapter; +import ghidra.framework.plugintool.annotation.AutoConfigStateField; import ghidra.framework.plugintool.annotation.AutoServiceConsumed; import ghidra.trace.model.Trace; import ghidra.trace.model.Trace.TraceSnapshotChangeType; @@ -50,13 +55,15 @@ import ghidra.trace.model.time.TraceTimeManager; import ghidra.util.table.GhidraTableFilterPanel; public class DebuggerTimeProvider extends ComponentProviderAdapter { + private static final AutoConfigState.ClassHandler CONFIG_STATE_HANDLER = + AutoConfigState.wireHandler(DebuggerTimeProvider.class, MethodHandles.lookup()); protected enum SnapshotTableColumns implements EnumeratedTableColumn { SNAP("Snap", Long.class, SnapshotRow::getSnap), TIMESTAMP("Timestamp", String.class, SnapshotRow::getTimeStamp), // TODO: Use Date type here EVENT_THREAD("Event Thread", String.class, SnapshotRow::getEventThreadName), - TICKS("Ticks", Long.class, SnapshotRow::getTicks), + SCHEDULE("Schedule", String.class, SnapshotRow::getSchedule), DESCRIPTION("Description", String.class, SnapshotRow::getDescription, SnapshotRow::setDescription); private final String header; @@ -103,17 +110,6 @@ public class DebuggerTimeProvider extends ComponentProviderAdapter { } } - protected static boolean sameCoordinates(DebuggerCoordinates a, DebuggerCoordinates b) { - if (!Objects.equals(a.getTrace(), b.getTrace())) { - return false; - } - if (!Objects.equals(a.getSnap(), b.getSnap())) { - return false; - } - // TODO: Ticks? - return true; - } - private class SnapshotListener extends TraceDomainObjectListener { public SnapshotListener() { listenForUntyped(DomainObject.DO_OBJECT_RESTORED, e -> objectRestored()); @@ -128,6 +124,9 @@ public class DebuggerTimeProvider extends ComponentProviderAdapter { } private void snapAdded(TraceSnapshot snapshot) { + if (snapshot.getKey() < 0 && hideScratch) { + return; + } SnapshotRow row = new SnapshotRow(current.getTrace(), snapshot); snapshotTableModel.add(row); if (current.getSnap() == snapshot.getKey()) { @@ -136,14 +135,30 @@ public class DebuggerTimeProvider extends ComponentProviderAdapter { } private void snapChanged(TraceSnapshot snapshot) { + if (snapshot.getKey() < 0 && hideScratch) { + return; + } snapshotTableModel.notifyUpdatedWith(row -> row.getSnapshot() == snapshot); } private void snapDeleted(TraceSnapshot snapshot) { + if (snapshot.getKey() < 0 && hideScratch) { + return; + } snapshotTableModel.deleteWith(row -> row.getSnapshot() == snapshot); } } + protected static boolean sameCoordinates(DebuggerCoordinates a, DebuggerCoordinates b) { + if (!Objects.equals(a.getTrace(), b.getTrace())) { + return false; + } + if (!Objects.equals(a.getTime(), b.getTime())) { + return false; + } + return true; + } + protected final DebuggerTimePlugin plugin; DebuggerCoordinates current = DebuggerCoordinates.NOWHERE; @@ -163,7 +178,12 @@ public class DebuggerTimeProvider extends ComponentProviderAdapter { /* testing */ GTable snapshotTable; /* testing */ GhidraTableFilterPanel snapshotFilterPanel; - private DebuggerSnapActionContext currentCtx; + private DebuggerSnapActionContext myActionContext; + + ToggleDockingAction actionHideScratch; + + @AutoConfigStateField + /* testing */ boolean hideScratch = true; public DebuggerTimeProvider(DebuggerTimePlugin plugin) { super(plugin.getTool(), TITLE_PROVIDER_TIME, plugin.getName()); @@ -177,6 +197,11 @@ public class DebuggerTimeProvider extends ComponentProviderAdapter { setWindowMenuGroup(DebuggerPluginPackage.NAME); buildMainPanel(); + + myActionContext = new DebuggerSnapActionContext(0); + createActions(); + contextChanged(); + setVisible(true); } @@ -192,7 +217,10 @@ public class DebuggerTimeProvider extends ComponentProviderAdapter { @Override public ActionContext getActionContext(MouseEvent event) { - return currentCtx; + if (myActionContext == null) { + return super.getActionContext(event); + } + return myActionContext; } protected void buildMainPanel() { @@ -209,15 +237,16 @@ public class DebuggerTimeProvider extends ComponentProviderAdapter { } SnapshotRow row = snapshotFilterPanel.getSelectedItem(); if (row == null) { - currentCtx = null; + myActionContext = null; return; } long snap = row.getSnap(); if (snap == current.getSnap().longValue()) { return; } - currentCtx = new DebuggerSnapActionContext(snap); + myActionContext = new DebuggerSnapActionContext(snap); viewManager.activateSnap(snap); + contextChanged(); }); TableColumnModel columnModel = snapshotTable.getColumnModel(); @@ -227,12 +256,29 @@ public class DebuggerTimeProvider extends ComponentProviderAdapter { timeCol.setPreferredWidth(200); TableColumn etCol = columnModel.getColumn(SnapshotTableColumns.EVENT_THREAD.ordinal()); etCol.setPreferredWidth(40); - TableColumn ticksCol = columnModel.getColumn(SnapshotTableColumns.TICKS.ordinal()); - ticksCol.setPreferredWidth(60); + TableColumn schdCol = columnModel.getColumn(SnapshotTableColumns.SCHEDULE.ordinal()); + schdCol.setPreferredWidth(60); TableColumn descCol = columnModel.getColumn(SnapshotTableColumns.DESCRIPTION.ordinal()); descCol.setPreferredWidth(200); } + protected void createActions() { + actionHideScratch = DebuggerResources.HideScratchSnapshotsAction.builder(plugin) + .selected(hideScratch) + .onAction(this::activatedHideScratch) + .buildAndInstallLocal(this); + } + + private void activatedHideScratch(ActionContext ctx) { + hideScratch = !hideScratch; + if (hideScratch) { + deleteScratchSnapshots(); + } + else { + loadScratchSnapshots(); + } + } + private void addNewListeners() { if (currentTrace == null) { return; @@ -290,7 +336,35 @@ public class DebuggerTimeProvider extends ComponentProviderAdapter { return; } TraceTimeManager manager = curTrace.getTimeManager(); - snapshotTableModel.addAll( - Collections2.transform(manager.getAllSnapshots(), s -> new SnapshotRow(curTrace, s))); + Collection snapshots = hideScratch + ? manager.getSnapshots(0, true, Long.MAX_VALUE, true) + : manager.getAllSnapshots(); + snapshotTableModel.addAll(Collections2.transform(snapshots, + s -> new SnapshotRow(curTrace, s))); + } + + protected void deleteScratchSnapshots() { + snapshotTableModel.deleteWith(s -> s.getSnap() < 0); + } + + protected void loadScratchSnapshots() { + Trace curTrace = current.getTrace(); + if (curTrace == null) { + return; + } + TraceTimeManager manager = curTrace.getTimeManager(); + snapshotTableModel.addAll(Collections2.transform( + manager.getSnapshots(Long.MIN_VALUE, true, 0, false), + s -> new SnapshotRow(curTrace, s))); + } + + public void writeConfigState(SaveState saveState) { + CONFIG_STATE_HANDLER.writeConfigState(this, saveState); + } + + public void readConfigState(SaveState saveState) { + CONFIG_STATE_HANDLER.readConfigState(this, saveState); + + actionHideScratch.setSelected(hideScratch); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/time/SnapshotRow.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/time/SnapshotRow.java index 1caaf3be7b..3c6d8d3d66 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/time/SnapshotRow.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/time/SnapshotRow.java @@ -51,8 +51,8 @@ public class SnapshotRow { return thread == null ? "" : thread.getName(); } - public long getTicks() { - return snapshot.getTicks(); + public String getSchedule() { + return snapshot.getScheduleString(); } public String getDescription() { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/watch/DebuggerWatchesProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/watch/DebuggerWatchesProvider.java index eab673f94c..7f6d6d4527 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/watch/DebuggerWatchesProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/watch/DebuggerWatchesProvider.java @@ -138,7 +138,7 @@ public class DebuggerWatchesProvider extends ComponentProviderAdapter { if (!Objects.equals(a.getRecorder(), b.getRecorder())) { return false; // May need to read target } - if (!Objects.equals(a.getSnap(), b.getSnap())) { + if (!Objects.equals(a.getTime(), b.getTime())) { return false; } if (!Objects.equals(a.getThread(), b.getThread())) { @@ -147,7 +147,6 @@ public class DebuggerWatchesProvider extends ComponentProviderAdapter { if (!Objects.equals(a.getFrame(), b.getFrame())) { return false; } - // TODO: Ticks return true; } @@ -236,21 +235,25 @@ public class DebuggerWatchesProvider extends ComponentProviderAdapter { @SuppressWarnings("unused") private final AutoService.Wiring autoServiceWiring; - @AutoOptionDefined(name = DebuggerResources.OPTION_NAME_COLORS_WATCH_STALE, // - description = "Text color for watches whose value is not known", // - help = @HelpInfo(anchor = "colors")) + @AutoOptionDefined( + name = DebuggerResources.OPTION_NAME_COLORS_WATCH_STALE, // + description = "Text color for watches whose value is not known", // + help = @HelpInfo(anchor = "colors")) protected Color watchStaleColor = DebuggerResources.DEFAULT_COLOR_WATCH_STALE; - @AutoOptionDefined(name = DebuggerResources.OPTION_NAME_COLORS_WATCH_STALE_SEL, // - description = "Selected text color for watches whose value is not known", // - help = @HelpInfo(anchor = "colors")) + @AutoOptionDefined( + name = DebuggerResources.OPTION_NAME_COLORS_WATCH_STALE_SEL, // + description = "Selected text color for watches whose value is not known", // + help = @HelpInfo(anchor = "colors")) protected Color watchStaleSelColor = DebuggerResources.DEFAULT_COLOR_WATCH_STALE_SEL; - @AutoOptionDefined(name = DebuggerResources.OPTION_NAME_COLORS_WATCH_CHANGED, // - description = "Text color for watches whose value just changed", // - help = @HelpInfo(anchor = "colors")) + @AutoOptionDefined( + name = DebuggerResources.OPTION_NAME_COLORS_WATCH_CHANGED, // + description = "Text color for watches whose value just changed", // + help = @HelpInfo(anchor = "colors")) protected Color watchChangesColor = DebuggerResources.DEFAULT_COLOR_WATCH_CHANGED; - @AutoOptionDefined(name = DebuggerResources.OPTION_NAME_COLORS_WATCH_CHANGED_SEL, // - description = "Selected text color for watches whose value just changed", // - help = @HelpInfo(anchor = "colors")) + @AutoOptionDefined( + name = DebuggerResources.OPTION_NAME_COLORS_WATCH_CHANGED_SEL, // + description = "Selected text color for watches whose value just changed", // + help = @HelpInfo(anchor = "colors")) protected Color watchChangesSelColor = DebuggerResources.DEFAULT_COLOR_WATCH_CHANGED_SEL; private final AddressSet changed = new AddressSet(); @@ -554,7 +557,7 @@ public class DebuggerWatchesProvider extends ComponentProviderAdapter { setRowsContext(coordinates); - if (current.isAliveAndPresent()) { + if (current.isAliveAndReadsPresent()) { readTarget(); } reevaluate(); 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 d1a14e0647..16f79849ed 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 @@ -189,10 +189,10 @@ public class WatchRow { } @Override - public void execute(SleighProgram program, + public PcodeFrame execute(PcodeProgram program, SleighUseropLibrary> library) { depsState.reset(); - super.execute(program, library); + return super.execute(program, library); } public AddressSet getReads() { @@ -204,7 +204,7 @@ public class WatchRow { DebuggerCoordinates coordinates) { Trace trace = coordinates.getTrace(); ReadDepsTraceBytesPcodeExecutorState state = - new ReadDepsTraceBytesPcodeExecutorState(trace, coordinates.getSnap(), + new ReadDepsTraceBytesPcodeExecutorState(trace, coordinates.getViewSnap(), coordinates.getThread(), coordinates.getFrame()); Language language = trace.getBaseLanguage(); PcodeExecutorState> paired = @@ -232,11 +232,11 @@ public class WatchRow { language = (SleighLanguage) newLanguage; recompile(); } - if (coordinates.isAliveAndPresent()) { + if (coordinates.isAliveAndReadsPresent()) { asyncExecutor = TracePcodeUtils.executorForCoordinates(coordinates); } executorWithState = TraceSleighUtils.buildByteWithStateExecutor(trace, - coordinates.getSnap(), coordinates.getThread(), coordinates.getFrame()); + coordinates.getViewSnap(), coordinates.getThread(), coordinates.getFrame()); executorWithAddress = buildAddressDepsExecutor(coordinates); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/DbgengX64DisassemblyInject.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/DbgengX64DisassemblyInject.java index 021cc896ca..5d732aafe0 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/DbgengX64DisassemblyInject.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/DbgengX64DisassemblyInject.java @@ -43,6 +43,7 @@ import ghidra.util.Msg; import ghidra.util.task.TaskMonitor; @DisassemblyInjectInfo(langIDs = { "x86:LE:64:default" }) +// TODO: Filter / selector on debugger. This is running for GDB, too.... public class DbgengX64DisassemblyInject implements DisassemblyInject { enum Mode { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/AbstractReadsTargetPcodeExecutorState.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/AbstractReadsTargetPcodeExecutorState.java new file mode 100644 index 0000000000..9c972466f9 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/AbstractReadsTargetPcodeExecutorState.java @@ -0,0 +1,115 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.debug.service.emulation; + +import java.util.concurrent.*; + +import com.google.common.collect.Range; +import com.google.common.primitives.UnsignedLong; + +import ghidra.app.services.TraceRecorder; +import ghidra.framework.plugintool.PluginTool; +import ghidra.pcode.exec.AccessPcodeExecutionException; +import ghidra.pcode.exec.trace.TraceCachedWriteBytesPcodeExecutorState; +import ghidra.pcode.exec.trace.TraceSleighUtils; +import ghidra.program.model.address.AddressSet; +import ghidra.program.model.address.AddressSpace; +import ghidra.trace.model.Trace; +import ghidra.trace.model.memory.TraceMemorySpace; +import ghidra.trace.model.memory.TraceMemoryState; +import ghidra.trace.model.thread.TraceThread; +import ghidra.util.database.UndoableTransaction; + +public abstract class AbstractReadsTargetPcodeExecutorState + extends TraceCachedWriteBytesPcodeExecutorState { + + abstract class AbstractReadsTargetCachedSpace extends CachedSpace { + public AbstractReadsTargetCachedSpace(AddressSpace space, + TraceMemorySpace source, long snap) { + super(space, source, snap); + } + + protected abstract void fillUninitialized(AddressSet uninitialized); + + protected boolean isLive() { + return recorder != null && recorder.isRecording() && recorder.getSnap() == snap; + } + + protected AddressSet computeUnknown(AddressSet uninitialized) { + return uninitialized.subtract(source.getAddressesWithState(snap, uninitialized, + s -> s != null && s != TraceMemoryState.UNKNOWN)); + } + + @Override + public byte[] read(long offset, int size) { + if (source != null) { + AddressSet uninitialized = new AddressSet(); + for (Range rng : cache.getUninitialized(offset, offset + size) + .asRanges()) { + uninitialized.add(space.getAddress(lower(rng)), + space.getAddress(upper(rng))); + } + if (uninitialized.isEmpty()) { + return super.read(offset, size); + } + + fillUninitialized(uninitialized); + } + + // TODO: What to flush when bytes in the trace change? + return super.read(offset, size); + } + + protected T waitTimeout(CompletableFuture future) { + try { + return future.get(1, TimeUnit.SECONDS); + } + catch (InterruptedException | ExecutionException | TimeoutException e) { + throw new AccessPcodeExecutionException("Timed out reading target", e); + } + } + } + + protected final TraceRecorder recorder; + protected final PluginTool tool; + + public AbstractReadsTargetPcodeExecutorState(PluginTool tool, Trace trace, long snap, + TraceThread thread, int frame, TraceRecorder recorder) { + super(trace, snap, thread, frame); + this.tool = tool; + this.recorder = recorder; + } + + protected abstract AbstractReadsTargetCachedSpace createCachedSpace(AddressSpace s, + TraceMemorySpace tms); + + @Override + protected CachedSpace getForSpace(AddressSpace space, boolean toWrite) { + return spaces.computeIfAbsent(space, s -> { + TraceMemorySpace tms; + if (s.isUniqueSpace()) { + tms = null; + } + else { + try (UndoableTransaction tid = + UndoableTransaction.start(trace, "Create space", true)) { + tms = TraceSleighUtils.getSpaceForExecution(s, trace, thread, frame, true); + } + } + return createCachedSpace(s, tms); + }); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/DebuggerEmulationServicePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/DebuggerEmulationServicePlugin.java new file mode 100644 index 0000000000..99474cc8ff --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/DebuggerEmulationServicePlugin.java @@ -0,0 +1,287 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.debug.service.emulation; + +import java.util.*; +import java.util.concurrent.CompletableFuture; + +import org.apache.commons.lang3.exception.ExceptionUtils; + +import ghidra.app.plugin.PluginCategoryNames; +import ghidra.app.plugin.core.debug.DebuggerPluginPackage; +import ghidra.app.plugin.core.debug.event.TraceClosedPluginEvent; +import ghidra.app.services.*; +import ghidra.async.AsyncLazyMap; +import ghidra.framework.plugintool.*; +import ghidra.framework.plugintool.annotation.AutoServiceConsumed; +import ghidra.framework.plugintool.util.PluginStatus; +import ghidra.trace.model.Trace; +import ghidra.trace.model.time.TraceSchedule; +import ghidra.trace.model.time.TraceSchedule.CompareResult; +import ghidra.trace.model.time.TraceSnapshot; +import ghidra.util.database.UndoableTransaction; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.Task; +import ghidra.util.task.TaskMonitor; + +@PluginInfo( + shortDescription = "Debugger Emulation Service Plugin", + description = "Manages and cache trace emulation states", + category = PluginCategoryNames.DEBUGGER, + packageName = DebuggerPluginPackage.NAME, + status = PluginStatus.UNSTABLE, + eventsConsumed = { + TraceClosedPluginEvent.class + }, + servicesRequired = { + DebuggerTraceManagerService.class + }, + servicesProvided = { + DebuggerEmulationService.class + }) +public class DebuggerEmulationServicePlugin extends Plugin implements DebuggerEmulationService { + protected static final int MAX_CACHE_SIZE = 5; + protected static long nextSnap = Long.MIN_VALUE; // HACK + + protected static class CacheKey implements Comparable { + protected final Trace trace; + protected final TraceSchedule time; + + public CacheKey(Trace trace, TraceSchedule time) { + this.trace = trace; + this.time = time; + } + + @Override + public int hashCode() { + return Objects.hash(trace, time); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof CacheKey)) { + return false; + } + CacheKey that = (CacheKey) obj; + if (this.trace != that.trace) { + return false; + } + if (!Objects.equals(this.time, that.time)) { + return false; + } + return true; + } + + @Override + public int compareTo(CacheKey that) { + return compareKey(that).compareTo; + } + + public CompareResult compareKey(CacheKey that) { + CompareResult result; + + // I don't care the order, I just care that traces matter first + result = CompareResult.unrelated(Integer.compare(System.identityHashCode(this.trace), + System.identityHashCode(that.trace))); + if (result != CompareResult.EQUALS) { + return result; + } + + result = this.time.compareSchedule(that.time); + if (result != CompareResult.EQUALS) { + return result; + } + + return CompareResult.EQUALS; + } + } + + protected static class CachedEmulator { + final DebuggerTracePcodeEmulator emulator; + + public CachedEmulator(DebuggerTracePcodeEmulator emulator) { + this.emulator = emulator; + } + } + + protected class EmulateTask extends Task { + protected final CacheKey key; + protected final CompletableFuture future = new CompletableFuture<>(); + + public EmulateTask(CacheKey key) { + super("Emulate " + key.time + " in " + key.trace, true, true, false, false); + this.key = key; + } + + @Override + public void run(TaskMonitor monitor) throws CancelledException { + try { + future.complete(doEmulate(key, monitor)); + } + catch (CancelledException e) { + future.completeExceptionally(e); + throw e; + } + catch (Throwable e) { + future.completeExceptionally(e); + ExceptionUtils.rethrow(e); + } + } + } + + protected final Set eldest = new LinkedHashSet<>(); + protected final NavigableMap cache = new TreeMap<>(); + protected final AsyncLazyMap requests = + new AsyncLazyMap<>(new HashMap<>(), this::doBackgroundEmulate) + .forgetErrors((key, t) -> true) + .forgetValues((key, l) -> true); + + @AutoServiceConsumed + private DebuggerTraceManagerService traceManager; + @AutoServiceConsumed + private DebuggerModelService modelService; + @SuppressWarnings("unused") + private AutoService.Wiring autoServiceWiring; + + public DebuggerEmulationServicePlugin(PluginTool tool) { + super(tool); + autoServiceWiring = AutoService.wireServicesProvidedAndConsumed(this); + } + + protected Map.Entry findNearestPrefix(CacheKey key) { + Map.Entry candidate = cache.floorEntry(key); + if (candidate == null) { + return null; + } + if (!candidate.getKey().compareKey(key).related) { + return null; + } + return candidate; + } + + protected CompletableFuture doBackgroundEmulate(CacheKey key) { + EmulateTask task = new EmulateTask(key); + tool.execute(task, 500); + return task.future; + } + + @Override + public CompletableFuture backgroundEmulate(Trace trace, TraceSchedule time) { + if (!traceManager.getOpenTraces().contains(trace)) { + throw new IllegalArgumentException( + "Cannot emulate a trace unless it's opened in the tool."); + } + if (time.isSnapOnly()) { + return CompletableFuture.completedFuture(time.getSnap()); + } + return requests.get(new CacheKey(trace, time)); + } + + protected TraceSnapshot findScratch(Trace trace, TraceSchedule time) { + Collection exist = + trace.getTimeManager().getSnapshotsWithSchedule(time); + if (!exist.isEmpty()) { + return exist.iterator().next(); + } + /** + * TODO: This could be more sophisticated.... Does it need to be, though? Ideally, we'd only + * keep state around that has annotations, e.g., bookmarks and code units. That needs a new + * query (latestStartSince) on those managers, though. It must find the latest start tick + * since a given snap. We consider only start snaps because placed code units go "from now + * on out". + */ + TraceSnapshot last = trace.getTimeManager().getMostRecentSnapshot(-1); + long snap = last == null ? Long.MIN_VALUE : last.getKey() + 1; + TraceSnapshot snapshot = trace.getTimeManager().getSnapshot(snap, true); + snapshot.setDescription("Emulated"); + snapshot.setSchedule(time); + return snapshot; + } + + protected long doEmulate(CacheKey key, TaskMonitor monitor) throws CancelledException { + Trace trace = key.trace; + TraceSchedule time = key.time; + CachedEmulator ce; + DebuggerTracePcodeEmulator emu; + Map.Entry ancestor = findNearestPrefix(key); + if (ancestor != null) { + CacheKey prevKey = ancestor.getKey(); + + cache.remove(prevKey); + eldest.remove(prevKey); + + // TODO: Handle errors, and add to proper place in cache? + // TODO: Finish partially-executed instructions? + ce = ancestor.getValue(); + emu = ce.emulator; + monitor.initialize(time.totalTickCount() - prevKey.time.totalTickCount()); + time.finish(trace, prevKey.time, emu, monitor); + } + else { + emu = new DebuggerTracePcodeEmulator(tool, trace, time.getSnap(), + modelService == null ? null : modelService.getRecorder(trace)); + ce = new CachedEmulator(emu); + monitor.initialize(time.totalTickCount()); + time.execute(trace, emu, monitor); + } + TraceSnapshot destSnap; + try (UndoableTransaction tid = UndoableTransaction.start(trace, "Emulate", true)) { + destSnap = findScratch(trace, time); + emu.writeDown(trace, destSnap.getKey(), time.getSnap(), false); + } + + cache.put(key, ce); + eldest.add(key); + + assert cache.size() == eldest.size(); + while (cache.size() > MAX_CACHE_SIZE) { + CacheKey expired = eldest.iterator().next(); + eldest.remove(expired); + cache.remove(expired); + } + + return destSnap.getKey(); + } + + @Override + public long emulate(Trace trace, TraceSchedule time, TaskMonitor monitor) + throws CancelledException { + if (!traceManager.getOpenTraces().contains(trace)) { + throw new IllegalArgumentException( + "Cannot emulate a trace unless it's opened in the tool."); + } + if (time.isSnapOnly()) { + return time.getSnap(); + } + return doEmulate(new CacheKey(trace, time), monitor); + } + + @Override + public DebuggerTracePcodeEmulator getCachedEmulator(Trace trace, TraceSchedule time) { + CachedEmulator ce = cache.get(new CacheKey(trace, time)); + return ce == null ? null : ce.emulator; + } + + @AutoServiceConsumed + private void setTraceManager(DebuggerTraceManagerService traceManager) { + cache.clear(); + } + + @AutoServiceConsumed + private void setModelService(DebuggerModelService modelService) { + cache.clear(); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/DebuggerTracePcodeEmulator.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/DebuggerTracePcodeEmulator.java new file mode 100644 index 0000000000..2a99ee316e --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/DebuggerTracePcodeEmulator.java @@ -0,0 +1,85 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.debug.service.emulation; + +import ghidra.app.services.TraceRecorder; +import ghidra.framework.plugintool.PluginTool; +import ghidra.pcode.emu.BytesPcodeThread; +import ghidra.pcode.emu.PcodeThread; +import ghidra.pcode.exec.PcodeExecutorState; +import ghidra.pcode.exec.trace.TracePcodeEmulator; +import ghidra.program.model.lang.Register; +import ghidra.program.model.lang.RegisterValue; +import ghidra.trace.model.Trace; +import ghidra.trace.model.memory.TraceMemoryRegisterSpace; +import ghidra.trace.model.memory.TraceMemoryState; +import ghidra.trace.model.thread.TraceThread; + +/** + * A trace emulator that knows how to read target memory when necessary + * + *

+ * This emulator must always be run in its own thread, or at least a thread that can never lock the + * UI. It blocks on target reads so that execution can proceed synchronously. Probably the most + * suitable option is to use a background task. + */ +public class DebuggerTracePcodeEmulator extends TracePcodeEmulator { + protected final PluginTool tool; + protected final TraceRecorder recorder; + + public DebuggerTracePcodeEmulator(PluginTool tool, Trace trace, long snap, + TraceRecorder recorder) { + super(trace, snap); + this.tool = tool; + this.recorder = recorder; + } + + protected boolean isRegisterKnown(String threadName, Register register) { + TraceThread thread = trace.getThreadManager().getLiveThreadByPath(snap, threadName); + TraceMemoryRegisterSpace space = + trace.getMemoryManager().getMemoryRegisterSpace(thread, false); + if (space == null) { + return false; + } + return space.getState(snap, register) == TraceMemoryState.KNOWN; + } + + @Override + protected BytesPcodeThread createThread(String name) { + BytesPcodeThread thread = super.createThread(name); + Register contextreg = language.getContextBaseRegister(); + if (contextreg != null && !isRegisterKnown(name, contextreg)) { + RegisterValue context = trace.getRegisterContextManager() + .getValueWithDefault(language, contextreg, snap, thread.getCounter()); + thread.overrideContext(context); + } + return thread; + } + + @Override + protected PcodeExecutorState createMemoryState() { + return new ReadsTargetMemoryPcodeExecutorState(tool, trace, snap, null, 0, + recorder); + } + + @Override + protected PcodeExecutorState createRegisterState(PcodeThread emuThread) { + TraceThread traceThread = + trace.getThreadManager().getLiveThreadByPath(snap, emuThread.getName()); + return new ReadsTargetRegistersPcodeExecutorState(tool, trace, snap, traceThread, 0, + recorder); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/ReadsTargetMemoryPcodeExecutorState.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/ReadsTargetMemoryPcodeExecutorState.java new file mode 100644 index 0000000000..2fdb45e6d9 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/ReadsTargetMemoryPcodeExecutorState.java @@ -0,0 +1,120 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.debug.service.emulation; + +import java.util.Map.Entry; + +import org.apache.commons.lang3.tuple.Pair; + +import ghidra.app.services.DebuggerStaticMappingService; +import ghidra.app.services.TraceRecorder; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.address.*; +import ghidra.program.model.listing.Program; +import ghidra.program.model.mem.Memory; +import ghidra.program.model.mem.MemoryAccessException; +import ghidra.trace.model.Trace; +import ghidra.trace.model.memory.TraceMemorySpace; +import ghidra.trace.model.thread.TraceThread; +import ghidra.util.MathUtilities; +import ghidra.util.Msg; +import ghidra.util.task.TaskMonitor; + +public class ReadsTargetMemoryPcodeExecutorState + extends AbstractReadsTargetPcodeExecutorState { + + protected class ReadsTargetMemoryCachedSpace extends AbstractReadsTargetCachedSpace { + + public ReadsTargetMemoryCachedSpace(AddressSpace space, TraceMemorySpace source, + long snap) { + super(space, source, snap); + } + + @Override + protected void fillUninitialized(AddressSet uninitialized) { + // TODO: fillUnknownWithStaticImages? + if (!isLive()) { + return; + } + AddressSet unknown = computeUnknown(uninitialized); + if (unknown.isEmpty()) { + return; + } + fillUnknownWithRecorder(unknown); + unknown = computeUnknown(uninitialized); + if (unknown.isEmpty()) { + return; + } + Msg.warn(this, "Emulator read from UNKNOWN state: " + unknown); + } + + protected void fillUnknownWithRecorder(AddressSet unknown) { + waitTimeout(recorder.captureProcessMemory(unknown, TaskMonitor.DUMMY)); + } + + private void fillUnknownWithStaticImages(AddressSet unknown) { + if (!space.isMemorySpace()) { + return; + } + DebuggerStaticMappingService mappingService = + tool.getService(DebuggerStaticMappingService.class); + byte[] data = new byte[4096]; + for (Entry> ent : mappingService + .getOpenMappedViews(trace, unknown, snap) + .entrySet()) { + Program program = ent.getKey(); + Pair pair = ent.getValue(); + Msg.warn(this, + "Filling in unknown trace memory in emulator using mapped image: " + + program + ": " + pair.getRight()); + long shift = pair.getLeft(); + Memory memory = program.getMemory(); + for (AddressRange rng : pair.getRight()) { + long lower = rng.getMinAddress().getOffset(); + long fullLen = rng.getLength(); + while (fullLen > 0) { + int len = MathUtilities.unsignedMin(data.length, fullLen); + try { + int read = + memory.getBytes(space.getAddress(lower), data, 0, len); + if (read < len) { + Msg.warn(this, + " Partial read of " + rng + ". Got " + read + " bytes"); + } + cache.putData(lower - shift, data, 0, read); + } + catch (MemoryAccessException | AddressOutOfBoundsException e) { + throw new AssertionError(e); + } + lower += len; + fullLen -= len; + } + } + } + } + } + + public ReadsTargetMemoryPcodeExecutorState(PluginTool tool, Trace trace, long snap, + TraceThread thread, int frame, TraceRecorder recorder) { + super(tool, trace, snap, thread, frame, recorder); + } + + @Override + protected AbstractReadsTargetCachedSpace createCachedSpace(AddressSpace s, + TraceMemorySpace tms) { + return new ReadsTargetMemoryCachedSpace(s, tms, snap); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/ReadsTargetRegistersPcodeExecutorState.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/ReadsTargetRegistersPcodeExecutorState.java new file mode 100644 index 0000000000..9f47369783 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/ReadsTargetRegistersPcodeExecutorState.java @@ -0,0 +1,76 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.debug.service.emulation; + +import java.util.HashSet; +import java.util.Set; + +import ghidra.app.services.TraceRecorder; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.address.*; +import ghidra.program.model.lang.Register; +import ghidra.trace.model.Trace; +import ghidra.trace.model.memory.TraceMemorySpace; +import ghidra.trace.model.thread.TraceThread; +import ghidra.util.Msg; + +public class ReadsTargetRegistersPcodeExecutorState + extends AbstractReadsTargetPcodeExecutorState { + + protected class ReadsTargetRegistersCachedSpace extends AbstractReadsTargetCachedSpace { + + public ReadsTargetRegistersCachedSpace(AddressSpace space, TraceMemorySpace source, + long snap) { + super(space, source, snap); + } + + @Override + protected void fillUninitialized(AddressSet uninitialized) { + if (!isLive()) { + return; + } + AddressSet unknown = computeUnknown(uninitialized); + Set toRead = new HashSet<>(); + for (AddressRange rng : unknown) { + Register register = + language.getRegister(rng.getMinAddress(), (int) rng.getLength()); + if (register == null) { + Msg.error(this, "Could not figure register for " + rng); + } + else if (!recorder.getRegisterMapper(thread) + .getRegistersOnTarget() + .contains(register)) { + Msg.warn(this, "Register not recognized by target: " + register); + } + else { + toRead.add(register); + } + } + waitTimeout(recorder.captureThreadRegisters(thread, 0, toRead)); + } + } + + public ReadsTargetRegistersPcodeExecutorState(PluginTool tool, Trace trace, long snap, + TraceThread thread, int frame, TraceRecorder recorder) { + super(tool, trace, snap, thread, frame, recorder); + } + + @Override + protected AbstractReadsTargetCachedSpace createCachedSpace(AddressSpace s, + TraceMemorySpace tms) { + return new ReadsTargetRegistersCachedSpace(s, tms, snap); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingServicePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingServicePlugin.java index 16f2f92a80..5484ee9625 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingServicePlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingServicePlugin.java @@ -23,6 +23,8 @@ import java.util.Map.Entry; import java.util.stream.Collectors; import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.lang3.tuple.Pair; import com.google.common.collect.Range; @@ -64,24 +66,24 @@ import ghidra.util.exception.VersionException; import ghidra.util.task.TaskMonitor; @PluginInfo( // - shortDescription = "Debugger static mapping manager", // - description = "Track and manage static mappings (program-trace relocations)", // - category = PluginCategoryNames.DEBUGGER, // - packageName = DebuggerPluginPackage.NAME, // - status = PluginStatus.RELEASED, // - eventsConsumed = { - ProgramOpenedPluginEvent.class, // - ProgramClosedPluginEvent.class, // - TraceOpenedPluginEvent.class, // - TraceClosedPluginEvent.class, // - }, // - servicesRequired = { // - ProgramManager.class, // - DebuggerTraceManagerService.class, // - }, // - servicesProvided = { // - DebuggerStaticMappingService.class, // - } // + shortDescription = "Debugger static mapping manager", // + description = "Track and manage static mappings (program-trace relocations)", // + category = PluginCategoryNames.DEBUGGER, // + packageName = DebuggerPluginPackage.NAME, // + status = PluginStatus.RELEASED, // + eventsConsumed = { + ProgramOpenedPluginEvent.class, // + ProgramClosedPluginEvent.class, // + TraceOpenedPluginEvent.class, // + TraceClosedPluginEvent.class, // + }, // + servicesRequired = { // + ProgramManager.class, // + DebuggerTraceManagerService.class, // + }, // + servicesProvided = { // + DebuggerStaticMappingService.class, // + } // ) public class DebuggerStaticMappingServicePlugin extends Plugin implements DebuggerStaticMappingService, DomainFolderChangeAdapter { @@ -284,6 +286,7 @@ public class DebuggerStaticMappingServicePlugin extends Plugin private Program program; private AddressRange staticRange; + private Long shift; public MappingEntry(TraceStaticMapping mapping) { this.mapping = mapping; @@ -309,6 +312,7 @@ public class DebuggerStaticMappingServicePlugin extends Plugin Address minAddr = opened.getAddressFactory().getAddress(mapping.getStaticAddress()); Address maxAddr = addOrMax(minAddr, mapping.getLength() - 1); this.staticRange = new AddressRangeImpl(minAddr, maxAddr); + this.shift = mapping.getMinTraceAddress().subtract(staticRange.getMinAddress()); return true; } return false; @@ -318,6 +322,7 @@ public class DebuggerStaticMappingServicePlugin extends Plugin if (this.program == closed) { this.program = null; this.staticRange = null; + this.shift = null; return true; } return false; @@ -562,7 +567,7 @@ public class DebuggerStaticMappingServicePlugin extends Plugin } protected void collectOpenMappedPrograms(AddressRange rng, Range span, - Map result) { + Map> result) { TraceAddressSnapRange tatr = new ImmutableTraceAddressSnapRange(rng, span); for (Entry out : outbound.entrySet()) { MappingEntry me = out.getValue(); @@ -572,14 +577,16 @@ public class DebuggerStaticMappingServicePlugin extends Plugin if (!out.getKey().intersects(tatr)) { continue; } - AddressSet set = result.computeIfAbsent(me.program, p -> new AddressSet()); - set.add(me.mapTraceRangeToProgram(rng)); + + Pair set = result.computeIfAbsent(me.program, + p -> new ImmutablePair<>(me.shift, new AddressSet())); + ((AddressSet) set.getRight()).add(me.mapTraceRangeToProgram(rng)); } } - public Map getOpenMappedViews(AddressSetView set, + public Map> getOpenMappedViews(AddressSetView set, Range span) { - Map result = new HashMap<>(); + Map> result = new HashMap<>(); for (AddressRange rng : set) { collectOpenMappedPrograms(rng, span, result); } @@ -709,7 +716,8 @@ public class DebuggerStaticMappingServicePlugin extends Plugin return null; } - protected void collectOpenMappedViews(AddressRange rng, Map result) { + protected void collectOpenMappedViews(AddressRange rng, + Map> result) { for (Entry inPreceeding : inbound.headMapByValue( rng.getMaxAddress(), true).entrySet()) { Address start = inPreceeding.getValue(); @@ -720,13 +728,14 @@ public class DebuggerStaticMappingServicePlugin extends Plugin if (!me.isInProgramRange(rng)) { continue; } - AddressSet set = result.computeIfAbsent(me.getTraceSnap(), p -> new AddressSet()); - set.add(me.mapProgramRangeToTrace(rng)); + Pair set = result.computeIfAbsent(me.getTraceSnap(), + p -> new ImmutablePair<>(me.shift, new AddressSet())); + ((AddressSet) set.getRight()).add(me.mapProgramRangeToTrace(rng)); } } - public Map getOpenMappedViews(AddressSetView set) { - Map result = new HashMap<>(); + public Map> getOpenMappedViews(AddressSetView set) { + Map> result = new HashMap<>(); for (AddressRange rng : set) { collectOpenMappedViews(rng, result); } @@ -1099,13 +1108,17 @@ public class DebuggerStaticMappingServicePlugin extends Plugin return info.getOpenMappedLocations(loc.getAddress(), loc.getLifespan()); } + protected long getNonScratchSnap(TraceProgramView view) { + return view.getViewport().getTop(s -> s >= 0 ? s : null); + } + @Override public ProgramLocation getStaticLocationFromDynamic(ProgramLocation loc) { loc = traceManager.fixLocation(loc, true); TraceProgramView view = (TraceProgramView) loc.getProgram(); Trace trace = view.getTrace(); - TraceLocation tloc = new DefaultTraceLocation(trace, null, Range.singleton(view.getSnap()), - loc.getAddress()); + TraceLocation tloc = new DefaultTraceLocation(trace, null, + Range.singleton(getNonScratchSnap(view)), loc.getAddress()); ProgramLocation mapped = getOpenMappedLocation(tloc); if (mapped == null) { return null; @@ -1134,7 +1147,7 @@ public class DebuggerStaticMappingServicePlugin extends Plugin @Override public ProgramLocation getDynamicLocationFromStatic(TraceProgramView view, ProgramLocation loc) { - TraceLocation tloc = getOpenMappedLocation(view.getTrace(), loc, view.getSnap()); + TraceLocation tloc = getOpenMappedLocation(view.getTrace(), loc, getNonScratchSnap(view)); if (tloc == null) { return null; } @@ -1142,7 +1155,8 @@ public class DebuggerStaticMappingServicePlugin extends Plugin } @Override - public Map getOpenMappedViews(Trace trace, AddressSetView set, + public Map> getOpenMappedViews(Trace trace, + AddressSetView set, long snap) { InfoPerTrace info = requireTrackedInfo(trace); if (info == null) { @@ -1152,7 +1166,8 @@ public class DebuggerStaticMappingServicePlugin extends Plugin } @Override - public Map getOpenMappedViews(Program program, AddressSetView set) { + public Map> getOpenMappedViews(Program program, + AddressSetView set) { InfoPerProgram info = requireTrackedInfo(program); if (info == null) { return null; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java index 444317cd06..140ad0bd7e 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java @@ -56,6 +56,8 @@ import ghidra.trace.model.program.TraceProgramView; import ghidra.trace.model.program.TraceVariableSnapProgramView; import ghidra.trace.model.stack.TraceStackFrame; import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.model.time.TraceSchedule; +import ghidra.trace.model.time.TraceSnapshot; import ghidra.util.*; import ghidra.util.datastruct.CollectionChangeListener; import ghidra.util.exception.*; @@ -119,8 +121,9 @@ public class DebuggerTraceManagerServicePlugin extends Plugin } private void threadDeleted(TraceThread thread) { - if (threadFocusByTrace.get(trace) == thread) { - threadFocusByTrace.remove(trace); + DebuggerCoordinates last = lastCoordsByTrace.get(trace); + if (last != null && last.getThread() == thread) { + lastCoordsByTrace.remove(trace); } if (current.getThread() == thread) { activate(DebuggerCoordinates.trace(trace)); @@ -158,7 +161,7 @@ public class DebuggerTraceManagerServicePlugin extends Plugin } } - protected final Map threadFocusByTrace = new WeakHashMap<>(); + protected final Map lastCoordsByTrace = new WeakHashMap<>(); protected final Map listenersByTrace = new WeakHashMap<>(); protected final Set tracesView = Collections.unmodifiableSet(listenersByTrace.keySet()); @@ -177,6 +180,8 @@ public class DebuggerTraceManagerServicePlugin extends Plugin // @AutoServiceConsumed via method private DebuggerModelService modelService; + @AutoServiceConsumed + private DebuggerEmulationService emulationService; @SuppressWarnings("unused") private final AutoService.Wiring autoServiceWiring; @@ -422,6 +427,7 @@ public class DebuggerTraceManagerServicePlugin extends Plugin if (trace == null) { return DebuggerCoordinates.NOWHERE; } + DebuggerCoordinates lastForTrace = lastCoordsByTrace.get(trace); // Note: override recorder with that known to service TraceRecorder recorder = computeRecorder(trace); TargetObject focus = recorder == null ? null : recorder.getFocus(); @@ -431,7 +437,7 @@ public class DebuggerTraceManagerServicePlugin extends Plugin thread = threadFromTargetFocus(recorder, focus); } if (thread /*still*/ == null) { // either no focus support, or focus is not a thread - thread = threadFocusByTrace.get(trace); + thread = lastForTrace == null ? null : lastForTrace.getThread(); } // NOTE, if still null without focus support, // we will take the eldest live thread at the resolved snap @@ -444,20 +450,20 @@ public class DebuggerTraceManagerServicePlugin extends Plugin */ // Note: override view. May not agree on snap now, but will upon activation TraceProgramView view = trace.getProgramView(); - Long snap = coordinates.getSnap(); - if (snap == null) { + TraceSchedule time = coordinates.getTime(); + if (time == null) { if (recorder != null && autoActivatePresent.get() && trace != current.getTrace()) { - snap = recorder.getSnap(); + time = TraceSchedule.snap(recorder.getSnap()); } else { - snap = view.getSnap(); + time = lastForTrace == null ? TraceSchedule.snap(0) : lastForTrace.getTime(); } } if (!supportsFocus(recorder)) { if (thread /*still*/ == null) { Iterator it = - trace.getThreadManager().getLiveThreads(snap).iterator(); + trace.getThreadManager().getLiveThreads(time.getSnap()).iterator(); // docs say eldest come first if (it.hasNext()) { thread = it.next(); @@ -472,15 +478,6 @@ public class DebuggerTraceManagerServicePlugin extends Plugin } } - String ticks = coordinates.getTicks(); - // TODO: Memorize ticks by trace and frame by thread - if (ticks == null && trace == current.getTrace()) { - ticks = current.getTicks(); - } - if (ticks == null) { - ticks = ""; - } - // Note, not likely we can view non-zero frame with emulated ticks Integer frame = coordinates.getFrame(); if (frame == null) { if (supportsFocus(recorder)) { @@ -493,17 +490,20 @@ public class DebuggerTraceManagerServicePlugin extends Plugin frame = traceFrame.getLevel(); } } - if (frame /*still*/ == null && thread == current.getThread()) { - frame = current.getFrame(); + if (frame /*still*/ == null && lastForTrace != null && + thread == lastForTrace.getThread()) { + // TODO: Memorize frame by thread, instead of by trace? + frame = lastForTrace.getFrame(); } } // TODO: Is it reasonable to change back to frame 0 on snap change? // Only 0 (possibly synthetic) is guaranteed to exist in any snap - if (frame == null || !"".equals(ticks) || !Objects.equals(snap, current.getSnap())) { + if (frame == null || !time.isSnapOnly() || + !Objects.equals(time.getSnap(), current.getSnap())) { frame = 0; } - return DebuggerCoordinates.all(trace, recorder, thread, view, Objects.requireNonNull(snap), - Objects.requireNonNull(ticks), Objects.requireNonNull(frame)); + return DebuggerCoordinates.all(trace, recorder, thread, view, Objects.requireNonNull(time), + Objects.requireNonNull(frame)); } protected DebuggerCoordinates doSetCurrent(DebuggerCoordinates newCurrent) { @@ -515,8 +515,8 @@ public class DebuggerTraceManagerServicePlugin extends Plugin } current = resolved; contextChanged(); - if (current.getTrace() != null && current.getThread() != null) { - threadFocusByTrace.put(current.getTrace(), current.getThread()); + if (resolved.getTrace() != null) { + lastCoordsByTrace.put(resolved.getTrace(), resolved); } return resolved; } @@ -561,10 +561,10 @@ public class DebuggerTraceManagerServicePlugin extends Plugin } TraceThread thread = threadFromTargetFocus(recorder, obj); long snap = recorder.getSnap(); - String ticks = ""; TraceStackFrame traceFrame = frameFromTargetFocus(recorder, obj); Integer frame = traceFrame == null ? null : traceFrame.getLevel(); - activateNoFocus(DebuggerCoordinates.all(trace, recorder, thread, null, snap, ticks, frame)); + activateNoFocus(DebuggerCoordinates.all(trace, recorder, thread, null, + TraceSchedule.snap(snap), frame)); return true; } @@ -651,7 +651,8 @@ public class DebuggerTraceManagerServicePlugin extends Plugin @Override public TraceThread getCurrentThreadFor(Trace trace) { - return threadFocusByTrace.get(trace); + DebuggerCoordinates coords = lastCoordsByTrace.get(trace); + return coords == null ? null : coords.getThread(); } @Override @@ -664,14 +665,44 @@ public class DebuggerTraceManagerServicePlugin extends Plugin return current.getFrame(); } - protected void fireLocationEvent(DebuggerCoordinates coordinates) { + protected void prepareViewAndFireEvent(DebuggerCoordinates coordinates) { TraceVariableSnapProgramView varView = (TraceVariableSnapProgramView) coordinates.getView(); - if (varView != null) { - varView.setSnap(coordinates.getSnap()); + if (varView == null) { // Should only happen with NOWHERE + fireLocationEvent(coordinates); } - Swing.runIfSwingOrRunLater(() -> { - firePluginEvent(new TraceActivatedPluginEvent(getName(), coordinates)); - }); + else if (coordinates.getTime().isSnapOnly()) { + varView.setSnap(coordinates.getSnap()); + fireLocationEvent(coordinates); + } + else { + Collection suitable = coordinates.getTrace() + .getTimeManager() + .getSnapshotsWithSchedule(coordinates.getTime()); + if (!suitable.isEmpty()) { + TraceSnapshot found = suitable.iterator().next(); + varView.setSnap(found.getKey()); + fireLocationEvent(coordinates); + return; + } + if (emulationService == null) { + throw new IllegalStateException( + "Cannot navigate to coordinates with execution schedules, " + + "because the emulation service is not available."); + } + CompletableFuture bg = + emulationService.backgroundEmulate(coordinates.getTrace(), coordinates.getTime()); + bg.thenAccept(emuSnap -> Swing.runLater(() -> { + if (!coordinates.equals(current)) { + return; // We navigated elsewhere before emulation completed + } + varView.setSnap(emuSnap); + fireLocationEvent(coordinates); + })); + } + } + + protected void fireLocationEvent(DebuggerCoordinates coordinates) { + firePluginEvent(new TraceActivatedPluginEvent(getName(), coordinates)); } @Override @@ -874,7 +905,7 @@ public class DebuggerTraceManagerServicePlugin extends Plugin protected void doTraceClosed(Trace trace) { synchronized (listenersByTrace) { trace.release(this); - threadFocusByTrace.remove(trace); + lastCoordsByTrace.remove(trace); trace.removeListener(listenersByTrace.remove(trace)); //Msg.debug(this, "Remaining Consumers of " + trace + ": " + trace.getConsumerList()); } @@ -909,12 +940,12 @@ public class DebuggerTraceManagerServicePlugin extends Plugin while (it.hasNext()) { Trace trace = it.next(); trace.release(this); - threadFocusByTrace.remove(trace); + lastCoordsByTrace.remove(trace); trace.removeListener(listenersByTrace.get(trace)); it.remove(); } // Be certain - threadFocusByTrace.clear(); + lastCoordsByTrace.clear(); } autoServiceWiring.dispose(); } @@ -931,7 +962,7 @@ public class DebuggerTraceManagerServicePlugin extends Plugin if (resolved == null) { return; } - fireLocationEvent(resolved); + prepareViewAndFireEvent(resolved); } protected static TargetObject translateToFocus(DebuggerCoordinates prev, @@ -970,7 +1001,7 @@ public class DebuggerTraceManagerServicePlugin extends Plugin if (resolved == null) { return; } - fireLocationEvent(resolved); + prepareViewAndFireEvent(resolved); if (!synchronizeFocus.get()) { return; } @@ -997,6 +1028,11 @@ public class DebuggerTraceManagerServicePlugin extends Plugin activate(DebuggerCoordinates.snap(snap)); } + @Override + public void activateTime(TraceSchedule time) { + activate(DebuggerCoordinates.time(time)); + } + @Override public void activateFrame(int frameLevel) { activate(DebuggerCoordinates.frame(frameLevel)); @@ -1142,24 +1178,20 @@ public class DebuggerTraceManagerServicePlugin extends Plugin } List traces; DebuggerCoordinates currentCoords; - Map threadByTrace; - Map snapByTrace; + Map coordsByTrace; synchronized (listenersByTrace) { currentCoords = current; traces = tracesView.stream().filter(t -> { ProjectLocator loc = t.getDomainFile().getProjectLocator(); return loc != null && !loc.isTransient(); }).collect(Collectors.toList()); - threadByTrace = Map.copyOf(threadFocusByTrace); - snapByTrace = tracesView.stream() - .collect(Collectors.toMap(t -> t, t -> t.getProgramView().getSnap())); + coordsByTrace = Map.copyOf(lastCoordsByTrace); } saveState.putInt(KEY_TRACE_COUNT, traces.size()); for (int index = 0; index < traces.size(); index++) { Trace t = traces.get(index); - DebuggerCoordinates coords = DebuggerCoordinates.all(t, null, threadByTrace.get(t), - null, snapByTrace.get(t), null, null); + DebuggerCoordinates coords = coordsByTrace.get(t); String stateName = PREFIX_OPEN_TRACE + index; coords.writeDataState(tool, saveState, stateName); } @@ -1173,7 +1205,11 @@ public class DebuggerTraceManagerServicePlugin extends Plugin for (int index = 0; index < traceCount; index++) { String stateName = PREFIX_OPEN_TRACE + index; // Trace will be opened by readDataState, resolve causes update to focus and view - DebuggerCoordinates.readDataState(tool, saveState, stateName, true); + DebuggerCoordinates coords = + DebuggerCoordinates.readDataState(tool, saveState, stateName, true); + if (coords.getTrace() != null) { + lastCoordsByTrace.put(coords.getTrace(), coords); + } } activate(DebuggerCoordinates.readDataState(tool, saveState, KEY_CURRENT_COORDS, false)); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/workflow/DisassembleAtPcDebuggerBot.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/workflow/DisassembleAtPcDebuggerBot.java index 11ad9547d7..f3597da30a 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/workflow/DisassembleAtPcDebuggerBot.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/workflow/DisassembleAtPcDebuggerBot.java @@ -44,19 +44,17 @@ import ghidra.trace.model.memory.*; import ghidra.trace.model.program.TraceProgramView; import ghidra.trace.model.stack.*; import ghidra.trace.model.thread.TraceThread; -import ghidra.trace.util.TraceAddressSpace; -import ghidra.trace.util.TraceRegisterUtils; -import ghidra.util.IntersectionAddressSetView; -import ghidra.util.UnionAddressSetView; +import ghidra.trace.util.*; +import ghidra.util.*; import ghidra.util.classfinder.ClassSearcher; import ghidra.util.database.UndoableTransaction; import ghidra.util.task.TaskMonitor; @DebuggerBotInfo( // - description = "Disassemble memory at the program counter", // - details = "Listens for changes in memory or pc (stack or registers) and disassembles", // - help = @HelpInfo(anchor = "disassemble_at_pc"), // - enabledByDefault = true // + description = "Disassemble memory at the program counter", // + details = "Listens for changes in memory or pc (stack or registers) and disassembles", // + help = @HelpInfo(anchor = "disassemble_at_pc"), // + enabledByDefault = true // ) public class DisassembleAtPcDebuggerBot implements DebuggerBot { @@ -64,12 +62,11 @@ public class DisassembleAtPcDebuggerBot implements DebuggerBot { private final TraceStackManager stackManager; private final TraceMemoryManager memoryManager; private final TraceCodeManager codeManager; + private final TraceTimeViewport viewport; private final Register pc; private final AddressRange pcRange; - private boolean usesStacks = false; - private final Set injects = new LinkedHashSet<>(); private final ChangeListener injectsChangeListener = e -> updateInjects(); @@ -83,6 +80,7 @@ public class DisassembleAtPcDebuggerBot implements DebuggerBot { this.stackManager = trace.getStackManager(); this.memoryManager = trace.getMemoryManager(); this.codeManager = trace.getCodeManager(); + this.viewport = trace.getProgramView().getViewport(); this.pc = trace.getBaseLanguage().getProgramCounter(); this.pcRange = TraceRegisterUtils.rangeForRegister(pc); @@ -117,13 +115,18 @@ public class DisassembleAtPcDebuggerBot implements DebuggerBot { } private void processQueue(Void __) { - List copy; - synchronized (runQueue) { - copy = List.copyOf(runQueue); - runQueue.clear(); + try { + List copy; + synchronized (runQueue) { + copy = List.copyOf(runQueue); + runQueue.clear(); + } + for (Runnable r : copy) { + r.run(); + } } - for (Runnable r : copy) { - r.run(); + catch (Throwable e) { + Msg.error(this, "Error processing queue", e); } } @@ -139,21 +142,41 @@ public class DisassembleAtPcDebuggerBot implements DebuggerBot { private void stackChanged(TraceStack stack) { queueRunnable(() -> { - usesStacks = true; disassembleStackPcVals(stack, stack.getSnap(), null); }); } + private long findNonScratchSnap(long snap) { + if (snap >= 0) { + return snap; + } + TraceViewportSpanIterator spit = new TraceViewportSpanIterator(trace, snap); + while (spit.hasNext()) { + Range span = spit.next(); + if (span.upperEndpoint() >= 0) { + return span.upperEndpoint(); + } + } + return snap; + } + private void memoryChanged(TraceAddressSnapRange range) { + if (!viewport.containsAnyUpper(range.getLifespan())) { + return; + } + // This is a wonky case, because we care about where the user is looking. + long pcSnap = trace.getProgramView().getSnap(); + long memSnap = range.getY1(); queueRunnable(() -> { - long snap = range.getY1(); - for (TraceThread thread : trace.getThreadManager().getLiveThreads(snap)) { - TraceStack stack = stackManager.getLatestStack(thread, snap); + for (TraceThread thread : trace.getThreadManager() + .getLiveThreads(findNonScratchSnap(pcSnap))) { + TraceStack stack = stackManager.getLatestStack(thread, pcSnap); if (stack != null) { - usesStacks = true; - disassembleStackPcVals(stack, snap, range.getRange()); + disassembleStackPcVals(stack, memSnap, range.getRange()); + } + else { + disassembleRegPcVal(thread, 0, pcSnap, memSnap); } - disassembleRegPcVal(thread, 0, snap); } }); } @@ -166,11 +189,16 @@ public class DisassembleAtPcDebuggerBot implements DebuggerBot { if (!range.getRange().intersects(pcRange)) { return; } - disassembleRegPcVal(space.getThread(), space.getFrameLevel(), range.getY1()); + TraceThread thread = space.getThread(); + long snap = range.getY1(); + if (stackManager.getLatestStack(thread, snap) != null) { + return; + } + disassembleRegPcVal(thread, space.getFrameLevel(), snap, snap); }); } - protected void disassembleStackPcVals(TraceStack stack, long snap, AddressRange range) { + protected void disassembleStackPcVals(TraceStack stack, long memSnap, AddressRange range) { TraceStackFrame frame = stack.getFrame(0, false); if (frame == null) { return; @@ -181,11 +209,12 @@ public class DisassembleAtPcDebuggerBot implements DebuggerBot { } if (range == null || range.contains(pcVal)) { // NOTE: If non-0 frames are ever used, level should be passed in for injects - disassemble(pcVal, stack.getThread(), snap); + disassemble(pcVal, stack.getThread(), memSnap); } } - protected void disassembleRegPcVal(TraceThread thread, int frameLevel, long snap) { + protected void disassembleRegPcVal(TraceThread thread, int frameLevel, long pcSnap, + long memSnap) { TraceData pcUnit = null; try (UndoableTransaction tid = UndoableTransaction.start(trace, "Disassemble: PC is code pointer", true)) { @@ -193,74 +222,80 @@ public class DisassembleAtPcDebuggerBot implements DebuggerBot { codeManager.getCodeRegisterSpace(thread, frameLevel, true); try { pcUnit = regCode.definedData() - .create(Range.atLeast(snap), pc, PointerDataType.dataType); + .create(Range.atLeast(pcSnap), pc, PointerDataType.dataType); } catch (CodeUnitInsertionException e) { // I guess something's already there. Leave it, then! // Try to get it, in case it's already a pointer type - pcUnit = regCode.definedData().getForRegister(snap, pc); + pcUnit = regCode.definedData().getForRegister(pcSnap, pc); } } - if (!usesStacks && pcUnit != null) { + if (pcUnit != null) { Address pcVal = (Address) TraceRegisterUtils.getValueHackPointer(pcUnit); if (pcVal != null) { - disassemble(pcVal, thread, snap); + disassemble(pcVal, thread, memSnap); } } } - protected boolean isKnownRWOrEverKnownRO(Address start, long snap) { - Entry ent = - memoryManager.getMostRecentStateEntry(snap, start); - if (ent == null || ent.getValue() != TraceMemoryState.KNOWN) { + protected Long isKnownRWOrEverKnownRO(Address start, long snap) { + Entry kent = memoryManager.getViewState(snap, start); + if (kent != null && kent.getValue() == TraceMemoryState.KNOWN) { + return kent.getKey(); + } + Entry mrent = + memoryManager.getViewMostRecentStateEntry(snap, start); + if (mrent == null || mrent.getValue() != TraceMemoryState.KNOWN) { // It has never been known up to this snap - return false; + return null; } - if (ent.getKey().getLifespan().contains(snap)) { - // It is known at this snap, so RO vs RW is irrelevant - return true; - } - TraceMemoryRegion region = memoryManager.getRegionContaining(snap, start); - if (region.isWrite()) { + TraceMemoryRegion region = + memoryManager.getRegionContaining(mrent.getKey().getY1(), start); + if (region == null || region.isWrite()) { // It could have changed this snap, so unknown - return false; + return null; } - return true; + return mrent.getKey().getY1(); } protected void disassemble(Address start, TraceThread thread, long snap) { - if (!isKnownRWOrEverKnownRO(start, snap)) { + Long knownSnap = isKnownRWOrEverKnownRO(start, snap); + if (knownSnap == null) { return; } - if (codeManager.definedUnits().containsAddress(snap, start)) { + long ks = knownSnap; + if (codeManager.definedUnits().containsAddress(ks, start)) { return; } /** * TODO: Is this composition of laziness upon laziness efficient enough? * + *

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

* Want addresses satisfying {@code known | (readOnly & everKnown)} */ AddressSetView readOnly = - memoryManager.getRegionsAddressSetWith(snap, r -> !r.isWrite()); - AddressSetView everKnown = memoryManager.getAddressesWithState(Range.atMost(snap), + memoryManager.getRegionsAddressSetWith(ks, r -> !r.isWrite()); + AddressSetView everKnown = memoryManager.getAddressesWithState(Range.atMost(ks), s -> s == TraceMemoryState.KNOWN); AddressSetView roEverKnown = new IntersectionAddressSetView(readOnly, everKnown); AddressSetView known = - memoryManager.getAddressesWithState(snap, s -> s == TraceMemoryState.KNOWN); - AddressSetView disassemblable = new UnionAddressSetView(known, roEverKnown); + memoryManager.getAddressesWithState(ks, s -> s == TraceMemoryState.KNOWN); + AddressSetView disassemblable = + new AddressSet(new UnionAddressSetView(known, roEverKnown)); // TODO: Should I just keep a variable-snap view around? - TraceProgramView view = trace.getFixedProgramView(snap); + TraceProgramView view = trace.getFixedProgramView(ks); DisassembleCommand dis = new DisassembleCommand(start, disassemblable, true) { @Override public boolean applyTo(DomainObject obj, TaskMonitor monitor) { synchronized (injects) { - if (codeManager.definedUnits().containsAddress(snap, start)) { + if (codeManager.definedUnits().containsAddress(ks, start)) { return true; } for (DisassemblyInject i : injects) { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerEmulationService.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerEmulationService.java new file mode 100644 index 0000000000..64ead7e305 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerEmulationService.java @@ -0,0 +1,83 @@ +/* ### + * 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.concurrent.CompletableFuture; + +import ghidra.app.plugin.core.debug.service.emulation.DebuggerEmulationServicePlugin; +import ghidra.app.plugin.core.debug.service.emulation.DebuggerTracePcodeEmulator; +import ghidra.framework.plugintool.ServiceInfo; +import ghidra.trace.model.Trace; +import ghidra.trace.model.time.TraceSchedule; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +@ServiceInfo(defaultProvider = DebuggerEmulationServicePlugin.class) +public interface DebuggerEmulationService { + + /** + * Perform emulation to realize the machine state of the given time coordinates + * + *

+ * Only those address ranges actually modified during emulation are written into the scratch + * space. It is the responsibility of anyone reading from scratch space to retrieve state and/or + * annotations from the initial snap, when needed. The scratch snapshot is given the description + * "{@code emu:[time]}", where {@code [time]} is the given time parameter as a string. + * + *

+ * The service may use a cached emulator in order to realize the requested machine state. This + * is especially important to ensure that a user stepping forward does not incur ever increasing + * costs. On the other hand, the service should be careful to invalidate cached results when the + * recorded machine state in a trace changes. + * + * @param trace the trace containing the initial state + * @param time the time coordinates, including initial snap, steps, and p-code steps + * @param monitor a monitor for cancellation and progress reporting + * @return the snap in the trace's scratch space where the realized state is stored + * @throws CancelledException if the emulation is cancelled + */ + long emulate(Trace trace, TraceSchedule time, TaskMonitor monitor) throws CancelledException; + + /** + * Invoke {@link #emulate(Trace, TraceSchedule, TaskMonitor)} in the background + * + *

+ * This is the preferred means of performing emulation. Because the underlying emulator may + * request blocking read of a target, it is important that + * {@link #emulate(Trace, TraceSchedule, TaskMonitor)} is never called by the Swing + * thread. + * + * @param trace the trace containing the initial state + * @param time the time coordinates, including initial snap, steps, and p-code steps + * @return a future which completes with the result of + * {@link #emulate(Trace, TraceSchedule, TaskMonitor)} + */ + CompletableFuture backgroundEmulate(Trace trace, TraceSchedule time); + + /** + * The the cached emulator for the given trace and time + * + *

+ * To guarantee the emulator is present, call {@link #backgroundEmulate(Trace, TraceSchedule)} + * first. WARNING: This emulator belongs to this service. Stepping it, or otherwise manipulating + * it without the service's knowledge can lead to unintended consequences. + * + * @param trace the trace containing the initial state + * @param time the time coordinates, including initial snap, steps, and p-code steps + * @return the copied p-code frame + */ + DebuggerTracePcodeEmulator getCachedEmulator(Trace trace, TraceSchedule time); +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerStaticMappingService.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerStaticMappingService.java index 47f48c5c5a..3a30f33cc2 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerStaticMappingService.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerStaticMappingService.java @@ -18,6 +18,8 @@ package ghidra.app.services; import java.util.*; import java.util.stream.Collectors; +import org.apache.commons.lang3.tuple.Pair; + import com.google.common.collect.Range; import ghidra.framework.model.DomainFile; @@ -637,7 +639,8 @@ public interface DebuggerStaticMappingService { * @param snap the source snap * @return a map of destination programs to corresponding computed destination address sets */ - Map getOpenMappedViews(Trace trace, AddressSetView set, long snap); + Map> getOpenMappedViews(Trace trace, + AddressSetView set, long snap); /** * Find/compute all source address sets given a destination program address set @@ -646,7 +649,8 @@ public interface DebuggerStaticMappingService { * @param set the destination address set, from which we are mapping back * @return a map of source traces to corresponding computed source address sets */ - Map getOpenMappedViews(Program program, AddressSetView set); + Map> getOpenMappedViews(Program program, + AddressSetView set); /** * Open all destination programs in mappings intersecting the given source trace, address set, diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerTraceManagerService.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerTraceManagerService.java index 44f1eebcbb..722912e048 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerTraceManagerService.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerTraceManagerService.java @@ -26,6 +26,7 @@ import ghidra.program.util.ProgramLocation; import ghidra.trace.model.Trace; import ghidra.trace.model.program.TraceProgramView; import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.model.time.TraceSchedule; import ghidra.util.TriConsumer; @ServiceInfo(defaultProvider = DebuggerTraceManagerServicePlugin.class) @@ -128,6 +129,8 @@ public interface DebuggerTraceManagerService { void activateSnap(long snap); + void activateTime(TraceSchedule time); + void activateFrame(int frameLevel); void setAutoActivatePresent(boolean enabled); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/pcode/exec/AsyncPcodeExecutor.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/pcode/exec/AsyncPcodeExecutor.java index bea3660262..52585a1312 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/pcode/exec/AsyncPcodeExecutor.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/pcode/exec/AsyncPcodeExecutor.java @@ -45,17 +45,27 @@ public class AsyncPcodeExecutor extends PcodeExecutor> { } public CompletableFuture stepOpAsync(PcodeOp op, PcodeFrame frame, - Map useropNames, SleighUseropLibrary> library) { + SleighUseropLibrary> library) { if (op.getOpcode() == PcodeOp.CBRANCH) { return executeConditionalBranchAsync(op, frame); } - stepOp(op, frame, useropNames, library); + stepOp(op, frame, library); return AsyncUtils.NIL; } - public CompletableFuture stepAsync(PcodeFrame frame, Map useropNames, + public CompletableFuture stepAsync(PcodeFrame frame, SleighUseropLibrary> library) { - return stepOpAsync(frame.nextOp(), frame, useropNames, library); + try { + return stepOpAsync(frame.nextOp(), frame, library); + } + catch (PcodeExecutionException e) { + e.frame = frame; + return CompletableFuture.failedFuture(e); + } + catch (Exception e) { + return CompletableFuture.failedFuture( + new PcodeExecutionException("Exception during pcode execution", frame, e)); + } } public CompletableFuture executeConditionalBranchAsync(PcodeOp op, PcodeFrame frame) { @@ -68,23 +78,23 @@ public class AsyncPcodeExecutor extends PcodeExecutor> { }); } - public CompletableFuture executeAsync(SleighProgram program, + public CompletableFuture executeAsync(PcodeProgram program, SleighUseropLibrary> library) { return executeAsync(program.code, program.useropNames, library); } protected CompletableFuture executeAsyncLoop(PcodeFrame frame, - Map useropNames, SleighUseropLibrary> library) { + SleighUseropLibrary> library) { if (frame.isFinished()) { return AsyncUtils.NIL; } - return stepAsync(frame, useropNames, library) - .thenComposeAsync(__ -> executeAsyncLoop(frame, useropNames, library)); + return stepAsync(frame, library) + .thenComposeAsync(__ -> executeAsyncLoop(frame, library)); } public CompletableFuture executeAsync(List code, Map useropNames, SleighUseropLibrary> library) { - PcodeFrame frame = new PcodeFrame(code); - return executeAsyncLoop(frame, useropNames, library); + PcodeFrame frame = new PcodeFrame(language, code, useropNames); + return executeAsyncLoop(frame, library); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/pcode/exec/AsyncWrappedPcodeArithmetic.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/pcode/exec/AsyncWrappedPcodeArithmetic.java index fa3b970d46..4f2938d33b 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/pcode/exec/AsyncWrappedPcodeArithmetic.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/pcode/exec/AsyncWrappedPcodeArithmetic.java @@ -61,6 +61,11 @@ public class AsyncWrappedPcodeArithmetic implements PcodeArithmetic fromConst(BigInteger value, int size) { + return CompletableFuture.completedFuture(arithmetic.fromConst(value, size)); + } + @Override public boolean isTrue(CompletableFuture cond) { if (!cond.isDone()) { @@ -68,4 +73,12 @@ public class AsyncWrappedPcodeArithmetic implements PcodeArithmetic cond) { + if (!cond.isDone()) { + throw new AssertionError("You need a better 8-ball"); + } + return arithmetic.toConcrete(cond.getNow(null)); + } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/pcode/exec/AsyncWrappedPcodeExecutorStatePiece.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/pcode/exec/AsyncWrappedPcodeExecutorStatePiece.java index a9f0ea72d3..a7541e285b 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/pcode/exec/AsyncWrappedPcodeExecutorStatePiece.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/pcode/exec/AsyncWrappedPcodeExecutorStatePiece.java @@ -19,7 +19,9 @@ import java.util.concurrent.CompletableFuture; import java.util.function.Supplier; import ghidra.async.AsyncUtils; +import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressSpace; +import ghidra.program.model.mem.MemBuffer; public class AsyncWrappedPcodeExecutorStatePiece implements PcodeExecutorStatePiece, CompletableFuture> { @@ -30,6 +32,10 @@ public class AsyncWrappedPcodeExecutorStatePiece this.state = state; } + protected boolean isWriteDone() { + return lastWrite.isDone(); + } + protected CompletableFuture nextRead(Supplier> next) { return lastWrite.thenCompose(__ -> next.get()).exceptionally(ex -> null); } @@ -68,4 +74,12 @@ public class AsyncWrappedPcodeExecutorStatePiece public CompletableFuture longToOffset(AddressSpace space, long l) { return CompletableFuture.completedFuture(state.longToOffset(space, l)); } + + @Override + public MemBuffer getConcreteBuffer(Address address) { + if (!isWriteDone()) { + throw new AssertionError("An async write is still pending"); + } + return state.getConcreteBuffer(address); + } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/pcode/exec/TracePcodeUtils.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/pcode/exec/TracePcodeUtils.java index 27d1ed36d8..35c4e49d44 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/pcode/exec/TracePcodeUtils.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/pcode/exec/TracePcodeUtils.java @@ -39,7 +39,7 @@ public enum TracePcodeUtils { PcodeExecutorState> state; if (coordinates.getRecorder() == null) { state = new AsyncWrappedPcodeExecutorState<>( - new TraceBytesPcodeExecutorState(trace, coordinates.getSnap(), + new TraceBytesPcodeExecutorState(trace, coordinates.getViewSnap(), coordinates.getThread(), coordinates.getFrame())); } else { diff --git a/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/pcode/DebuggerPcodeStepperPluginScreenShots.java b/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/pcode/DebuggerPcodeStepperPluginScreenShots.java new file mode 100644 index 0000000000..217e7595eb --- /dev/null +++ b/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/pcode/DebuggerPcodeStepperPluginScreenShots.java @@ -0,0 +1,86 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.debug.gui.pcode; + +import org.junit.*; + +import com.google.common.collect.Range; + +import ghidra.app.plugin.assembler.Assembler; +import ghidra.app.plugin.assembler.Assemblers; +import ghidra.app.plugin.core.debug.service.tracemgr.DebuggerTraceManagerServicePlugin; +import ghidra.app.services.DebuggerTraceManagerService; +import ghidra.pcode.exec.PcodeExecutor; +import ghidra.pcode.exec.trace.TraceSleighUtils; +import ghidra.test.ToyProgramBuilder; +import ghidra.trace.database.ToyDBTraceBuilder; +import ghidra.trace.model.memory.TraceMemoryFlag; +import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.model.time.TraceSchedule; +import ghidra.util.database.UndoableTransaction; +import help.screenshot.GhidraScreenShotGenerator; + +public class DebuggerPcodeStepperPluginScreenShots extends GhidraScreenShotGenerator { + + DebuggerTraceManagerService traceManager; + DebuggerPcodeStepperPlugin pcodePlugin; + DebuggerPcodeStepperProvider pcodeProvider; + ToyDBTraceBuilder tb; + + @Before + public void setUpMine() throws Throwable { + traceManager = addPlugin(tool, DebuggerTraceManagerServicePlugin.class); + pcodePlugin = addPlugin(tool, DebuggerPcodeStepperPlugin.class); + + pcodeProvider = waitForComponentProvider(DebuggerPcodeStepperProvider.class); + + tb = new ToyDBTraceBuilder("echo", ToyProgramBuilder._X64); + } + + @After + public void tearDownMine() { + tb.close(); + } + + @Test + public void testCaptureDebuggerPcodeStepperPlugin() throws Throwable { + try (UndoableTransaction tid = tb.startTransaction()) { + long snap0 = tb.trace.getTimeManager().createSnapshot("First").getKey(); + + tb.trace.getMemoryManager() + .addRegion("[echo:.text]", Range.atLeast(snap0), + tb.range(0x00400000, 0x0040ffff), TraceMemoryFlag.READ, + TraceMemoryFlag.EXECUTE); + + TraceThread thread = tb.getOrAddThread("[1]", snap0); + + PcodeExecutor exe = + TraceSleighUtils.buildByteExecutor(tb.trace, snap0, thread, 0); + exe.executeLine("RIP = 0x00400000"); + exe.executeLine("RSP = 0x0010fff8"); + + Assembler asm = Assemblers.getAssembler(tb.trace.getFixedProgramView(snap0)); + asm.assemble(tb.addr(0x00400000), "SUB RSP,0x40"); + + traceManager.openTrace(tb.trace); + traceManager.activateThread(thread); + traceManager.activateTime(TraceSchedule.parse("0:.t0-7")); + + pcodeProvider.mainPanel.setDividerLocation(0.4); + captureIsolatedProvider(pcodeProvider, 900, 300); + } + } +} diff --git a/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimePluginScreenShots.java b/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimePluginScreenShots.java index 381f0cd4ee..4636230872 100644 --- a/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimePluginScreenShots.java +++ b/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimePluginScreenShots.java @@ -22,6 +22,7 @@ import ghidra.app.services.DebuggerTraceManagerService; import ghidra.test.ToyProgramBuilder; import ghidra.trace.database.ToyDBTraceBuilder; import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.model.time.TraceSchedule; import ghidra.trace.model.time.TraceSnapshot; import ghidra.util.database.UndoableTransaction; import help.screenshot.GhidraScreenShotGenerator; @@ -65,18 +66,18 @@ public class DebuggerTimePluginScreenShots extends GhidraScreenShotGenerator { snap = tb.trace.getTimeManager().createSnapshot("Thread BREAKPOINT_HIT"); snap.setEventThread(thread); snap.setRealTime(fakeClock); - snap.setTicks(1); fakeClock += 2300; snap = tb.trace.getTimeManager().createSnapshot("Thread STEP_COMPLETED"); snap.setEventThread(thread); snap.setRealTime(fakeClock); - snap.setTicks(1); + snap.setSchedule(TraceSchedule.parse(snap.getKey() - 1 + ":1")); fakeClock += 444; snap = tb.trace.getTimeManager().createSnapshot("Thread STEP_COMPLETED"); snap.setEventThread(thread); snap.setRealTime(fakeClock); + snap.setSchedule(TraceSchedule.parse(snap.getKey() - 1 + ":1")); fakeClock += 100; } diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProviderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProviderTest.java index 3fdec87aa7..262f29aafe 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProviderTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProviderTest.java @@ -448,28 +448,28 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerGUI @Test public void testActionStepTraceBackward() throws Exception { - assertFalse(threadsProvider.actionStepTraceBackward.isEnabled()); + assertFalse(threadsProvider.actionStepSnapBackward.isEnabled()); createAndOpenTrace(); addThreads(); traceManager.activateTrace(tb.trace); waitForSwing(); - assertFalse(threadsProvider.actionStepTraceBackward.isEnabled()); + assertFalse(threadsProvider.actionStepSnapBackward.isEnabled()); try (UndoableTransaction tid = tb.startTransaction()) { tb.trace.getTimeManager().getSnapshot(10, true); } waitForDomainObject(tb.trace); - assertFalse(threadsProvider.actionStepTraceBackward.isEnabled()); + assertFalse(threadsProvider.actionStepSnapBackward.isEnabled()); traceManager.activateSnap(2); waitForSwing(); - assertTrue(threadsProvider.actionStepTraceBackward.isEnabled()); + assertTrue(threadsProvider.actionStepSnapBackward.isEnabled()); - performAction(threadsProvider.actionStepTraceBackward); + performAction(threadsProvider.actionStepSnapBackward); waitForSwing(); assertEquals(1, traceManager.getCurrentSnap()); @@ -477,32 +477,32 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerGUI @Test public void testActionStepTraceForward() throws Exception { - assertFalse(threadsProvider.actionStepTraceForward.isEnabled()); + assertFalse(threadsProvider.actionStepSnapForward.isEnabled()); createAndOpenTrace(); addThreads(); traceManager.activateTrace(tb.trace); waitForSwing(); - assertFalse(threadsProvider.actionStepTraceForward.isEnabled()); + assertFalse(threadsProvider.actionStepSnapForward.isEnabled()); try (UndoableTransaction tid = tb.startTransaction()) { tb.trace.getTimeManager().getSnapshot(10, true); } waitForDomainObject(tb.trace); - assertTrue(threadsProvider.actionStepTraceForward.isEnabled()); + assertTrue(threadsProvider.actionStepSnapForward.isEnabled()); - performAction(threadsProvider.actionStepTraceForward); + performAction(threadsProvider.actionStepSnapForward); waitForSwing(); assertEquals(1, traceManager.getCurrentSnap()); - assertTrue(threadsProvider.actionStepTraceForward.isEnabled()); + assertTrue(threadsProvider.actionStepSnapForward.isEnabled()); traceManager.activateSnap(10); waitForSwing(); - assertFalse(threadsProvider.actionStepTraceForward.isEnabled()); + assertFalse(threadsProvider.actionStepSnapForward.isEnabled()); } @Test diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimeProviderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimeProviderTest.java index a4fddb1960..83c491f1b6 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimeProviderTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimeProviderTest.java @@ -26,6 +26,7 @@ import org.junit.Test; import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest; import ghidra.trace.database.time.DBTraceTimeManager; import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.model.time.TraceSchedule; import ghidra.trace.model.time.TraceSnapshot; import ghidra.util.database.UndoableTransaction; @@ -44,11 +45,21 @@ public class DebuggerTimeProviderTest extends AbstractGhidraHeadedDebuggerGUITes DBTraceTimeManager timeManager = tb.trace.getTimeManager(); try (UndoableTransaction tid = tb.startTransaction()) { TraceSnapshot first = timeManager.createSnapshot("First"); - first.setTicks(123); Calendar c = Calendar.getInstance(); // System time zone c.set(2020, 0, 1, 9, 0, 0); first.setRealTime(c.getTimeInMillis()); - timeManager.getSnapshot(10, true).setDescription("Snap 10"); + TraceSnapshot second = timeManager.getSnapshot(10, true); + second.setDescription("Snap 10"); + second.setSchedule(TraceSchedule.parse("0:5,t1-5")); + } + } + + protected void addScratchSnapshot() { + DBTraceTimeManager timeManager = tb.trace.getTimeManager(); + try (UndoableTransaction tid = tb.startTransaction()) { + TraceSnapshot scratch = timeManager.getSnapshot(Long.MIN_VALUE, true); + scratch.setDescription("Scratch"); + scratch.setSchedule(TraceSchedule.parse("0:t0-5")); } } @@ -64,13 +75,13 @@ public class DebuggerTimeProviderTest extends AbstractGhidraHeadedDebuggerGUITes SnapshotRow firstRow = snapsDisplayed.get(0); assertEquals(0, firstRow.getSnap()); assertEquals("First", firstRow.getDescription()); - assertEquals(123, firstRow.getTicks()); + assertEquals("0", firstRow.getSchedule()); // Snap 0 has "0" schedule assertEquals("Jan 01, 2020 09:00 AM", firstRow.getTimeStamp()); SnapshotRow secondRow = snapsDisplayed.get(1); assertEquals(10, secondRow.getSnap()); assertEquals("Snap 10", secondRow.getDescription()); - assertEquals(0, secondRow.getTicks()); + assertEquals("0:5,t1-5", secondRow.getSchedule()); // Timestamp is left unchecked, since default is current time } @@ -289,4 +300,86 @@ public class DebuggerTimeProviderTest extends AbstractGhidraHeadedDebuggerGUITes assertNull(timeProvider.snapshotFilterPanel.getSelectedItem()); } + + @Test + public void testAddScratchThenActivateIsHidden() throws Exception { + createSnaplessTrace(); + traceManager.openTrace(tb.trace); + addSnapshots(); + addScratchSnapshot(); + waitForDomainObject(tb.trace); + + traceManager.activateTrace(tb.trace); + waitForSwing(); + + List data = timeProvider.snapshotTableModel.getModelData(); + assertEquals(2, data.size()); + for (SnapshotRow row : data) { + assertTrue(row.getSnap() >= 0); + } + } + + @Test + public void testActiveThenAddScratchIsHidden() throws Exception { + createSnaplessTrace(); + traceManager.openTrace(tb.trace); + addSnapshots(); + waitForDomainObject(tb.trace); + + traceManager.activateTrace(tb.trace); + waitForSwing(); + + assertEquals(2, timeProvider.snapshotTableModel.getModelData().size()); + + addScratchSnapshot(); + waitForDomainObject(tb.trace); + + List data = timeProvider.snapshotTableModel.getModelData(); + assertEquals(2, data.size()); + for (SnapshotRow row : data) { + assertTrue(row.getSnap() >= 0); + } + } + + @Test + public void testAddScratchThenActivateThenToggleIsShown() throws Exception { + createSnaplessTrace(); + traceManager.openTrace(tb.trace); + addSnapshots(); + addScratchSnapshot(); + waitForDomainObject(tb.trace); + + traceManager.activateTrace(tb.trace); + waitForSwing(); + + assertEquals(true, timeProvider.hideScratch); + assertEquals(2, timeProvider.snapshotTableModel.getModelData().size()); + + performAction(timeProvider.actionHideScratch); + + assertEquals(false, timeProvider.hideScratch); + assertEquals(3, timeProvider.snapshotTableModel.getModelData().size()); + + performAction(timeProvider.actionHideScratch); + + assertEquals(true, timeProvider.hideScratch); + assertEquals(2, timeProvider.snapshotTableModel.getModelData().size()); + } + + @Test + public void testToggleThenAddScratchThenActivateIsShown() throws Exception { + performAction(timeProvider.actionHideScratch); + + createSnaplessTrace(); + traceManager.openTrace(tb.trace); + addSnapshots(); + addScratchSnapshot(); + waitForDomainObject(tb.trace); + + traceManager.activateTrace(tb.trace); + waitForSwing(); + + assertEquals(false, timeProvider.hideScratch); + assertEquals(3, timeProvider.snapshotTableModel.getModelData().size()); + } } diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingServiceTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingServiceTest.java index 57e1d64e7f..e79e3164fd 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingServiceTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingServiceTest.java @@ -20,6 +20,7 @@ import static org.junit.Assert.*; import java.io.File; import java.util.*; +import org.apache.commons.lang3.tuple.Pair; import org.junit.Before; import org.junit.Test; @@ -314,7 +315,7 @@ public class DebuggerStaticMappingServiceTest extends AbstractGhidraHeadedDebugg public void testAddMappingThenTranslateTraceViewToStaticEmpty() throws Exception { addMapping(); - Map views = + Map> views = mappingService.getOpenMappedViews(tb.trace, new AddressSet(), 0); assertTrue(views.isEmpty()); } @@ -335,9 +336,12 @@ public class DebuggerStaticMappingServiceTest extends AbstractGhidraHeadedDebugg // After set.add(dynSpace.getAddress(0xbadbadbadL), dynSpace.getAddress(0xbadbadbadL + 0xff)); - Map views = mappingService.getOpenMappedViews(tb.trace, set, 0); + Map> views = + mappingService.getOpenMappedViews(tb.trace, set, 0); assertEquals(1, views.size()); - AddressSetView inStatic = views.get(program); + Pair pair = views.get(program); + assertEquals(0x100000, pair.getLeft().longValue()); + AddressSetView inStatic = pair.getRight(); assertEquals(3, inStatic.getNumAddressRanges()); AddressSet expected = new AddressSet(); expected.add(stSpace.getAddress(0x00200000), stSpace.getAddress(0x002000ff)); @@ -352,7 +356,7 @@ public class DebuggerStaticMappingServiceTest extends AbstractGhidraHeadedDebugg copyTrace(); add2ndMapping(); - Map views = + Map> views = mappingService.getOpenMappedViews(program, new AddressSet()); assertTrue(views.isEmpty()); } @@ -375,12 +379,13 @@ public class DebuggerStaticMappingServiceTest extends AbstractGhidraHeadedDebugg // After set.add(stSpace.getAddress(0xbadbadbadL), stSpace.getAddress(0xbadbadbadL + 0xff)); - Map views = mappingService.getOpenMappedViews(program, set); + Map> views = + mappingService.getOpenMappedViews(program, set); Msg.info(this, views); assertEquals(2, views.size()); - AddressSetView in1st = views.get(new DefaultTraceSnap(tb.trace, 0)); + AddressSetView in1st = views.get(new DefaultTraceSnap(tb.trace, 0)).getRight(); assertEquals(5, in1st.getNumAddressRanges()); - AddressSetView in2nd = views.get(new DefaultTraceSnap(copy, 0)); + AddressSetView in2nd = views.get(new DefaultTraceSnap(copy, 0)).getRight(); assertEquals(3, in2nd.getNumAddressRanges()); AddressSet expectedIn1st = new AddressSet(); diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/pcode/exec/TraceRecorderAsyncPcodeExecTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/pcode/exec/TraceRecorderAsyncPcodeExecTest.java index fd8b060d59..b0fa832abc 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/pcode/exec/TraceRecorderAsyncPcodeExecTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/pcode/exec/TraceRecorderAsyncPcodeExecTest.java @@ -90,7 +90,7 @@ public class TraceRecorderAsyncPcodeExecTest extends AbstractGhidraHeadedDebugge Trace trace = recorder.getTrace(); Language language = trace.getBaseLanguage(); - SleighProgram prog = SleighProgramCompiler.compileProgram((SleighLanguage) language, "test", + PcodeProgram prog = SleighProgramCompiler.compileProgram((SleighLanguage) language, "test", List.of("r2 = r0 + r1;"), SleighUseropLibrary.NIL); TraceRecorderAsyncPcodeExecutorState asyncState = diff --git a/Ghidra/Debug/Framework-TraceModeling/build.gradle b/Ghidra/Debug/Framework-TraceModeling/build.gradle index d62ac5d5eb..d529b0bf41 100644 --- a/Ghidra/Debug/Framework-TraceModeling/build.gradle +++ b/Ghidra/Debug/Framework-TraceModeling/build.gradle @@ -28,4 +28,5 @@ dependencies { annotationProcessor project(':AnnotationValidator') testCompile project(':Base') + testRuntime project(':ARM') // For its emulator state modifier } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/TraceBytesPcodeExecutorState.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/TraceBytesPcodeExecutorState.java index f48df69778..8ae9af63e8 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/TraceBytesPcodeExecutorState.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/TraceBytesPcodeExecutorState.java @@ -23,11 +23,14 @@ import org.apache.commons.lang3.tuple.Pair; import ghidra.generic.util.datastruct.SemisparseByteArray; import ghidra.pcode.exec.*; import ghidra.pcode.utils.Utils; +import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressSpace; +import ghidra.program.model.mem.MemBuffer; import ghidra.trace.model.Trace; import ghidra.trace.model.memory.TraceMemorySpace; import ghidra.trace.model.memory.TraceMemoryState; import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.util.DefaultTraceTimeViewport; public class TraceBytesPcodeExecutorState extends AbstractLongOffsetPcodeExecutorState { @@ -38,12 +41,17 @@ public class TraceBytesPcodeExecutorState private TraceThread thread; private int frame; + private final DefaultTraceTimeViewport viewport; + public TraceBytesPcodeExecutorState(Trace trace, long snap, TraceThread thread, int frame) { super(trace.getBaseLanguage(), BytesPcodeArithmetic.forLanguage(trace.getBaseLanguage())); this.trace = trace; this.snap = snap; this.thread = thread; this.frame = frame; + + this.viewport = new DefaultTraceTimeViewport(trace); + this.viewport.setSnap(snap); } public PcodeExecutorState> withMemoryState() { @@ -81,6 +89,7 @@ public class TraceBytesPcodeExecutorState public void setSnap(long snap) { this.snap = snap; + this.viewport.setSnap(snap); } public long getSnap() { @@ -147,10 +156,15 @@ public class TraceBytesPcodeExecutorState @Override protected byte[] getFromSpace(TraceMemorySpace space, long offset, int size) { ByteBuffer buf = ByteBuffer.allocate(size); - int read = space.getBytes(snap, space.getAddressSpace().getAddress(offset), buf); + int read = space.getViewBytes(snap, space.getAddressSpace().getAddress(offset), buf); if (read != size) { throw new RuntimeException("Could not read full value from trace"); } return buf.array(); } + + @Override + public MemBuffer getConcreteBuffer(Address address) { + return trace.getMemoryManager().getBufferAt(snap, address); + } } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/TraceCachedWriteBytesPcodeExecutorState.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/TraceCachedWriteBytesPcodeExecutorState.java new file mode 100644 index 0000000000..ee9c0e3360 --- /dev/null +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/TraceCachedWriteBytesPcodeExecutorState.java @@ -0,0 +1,257 @@ +/* ### + * 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.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Map; + +import com.google.common.collect.*; +import com.google.common.primitives.UnsignedLong; + +import ghidra.generic.util.datastruct.SemisparseByteArray; +import ghidra.pcode.exec.AbstractLongOffsetPcodeExecutorState; +import ghidra.pcode.exec.BytesPcodeArithmetic; +import ghidra.pcode.exec.trace.TraceCachedWriteBytesPcodeExecutorState.CachedSpace; +import ghidra.pcode.utils.Utils; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressSpace; +import ghidra.program.model.mem.MemBuffer; +import ghidra.program.model.mem.Memory; +import ghidra.trace.model.Trace; +import ghidra.trace.model.memory.TraceMemorySpace; +import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.util.MemBufferAdapter; +import ghidra.util.MathUtilities; + +/** + * A state which reads bytes from a trace, but caches writes internally. + * + *

+ * This provides for "read-only" emulation on a trace. Writes do not affect the source trace, but + * rather are cached in this state. If desired, those cached writes can be written back out at a + * later time. + */ +public class TraceCachedWriteBytesPcodeExecutorState + extends AbstractLongOffsetPcodeExecutorState { + + protected class StateMemBuffer implements MemBufferAdapter { + protected final Address address; + protected final CachedSpace source; + + public StateMemBuffer(Address address, CachedSpace source) { + this.address = address; + this.source = source; + } + + @Override + public Address getAddress() { + return address; + } + + @Override + public Memory getMemory() { + return null; + } + + @Override + public boolean isBigEndian() { + return trace.getBaseLanguage().isBigEndian(); + } + + @Override + public int getBytes(ByteBuffer buffer, int addressOffset) { + byte[] data = source.read(address.getOffset() + addressOffset, buffer.remaining()); + buffer.put(data); + return data.length; + } + } + + protected final Map spaces = new HashMap<>(); + + protected final Trace trace; + protected final long snap; + protected final TraceThread thread; + protected final int frame; + + public TraceCachedWriteBytesPcodeExecutorState(Trace trace, long snap, TraceThread thread, + int frame) { + super(trace.getBaseLanguage(), BytesPcodeArithmetic.forLanguage(trace.getBaseLanguage())); + this.trace = trace; + this.snap = snap; + this.thread = thread; + this.frame = frame; + } + + protected static class CachedSpace { + protected final SemisparseByteArray cache = new SemisparseByteArray(); + protected final RangeSet written = TreeRangeSet.create(); + protected final AddressSpace space; + protected final TraceMemorySpace source; + protected final long snap; + + public CachedSpace(AddressSpace space, TraceMemorySpace source, long snap) { + this.space = space; + this.source = source; + this.snap = snap; + } + + public void write(long offset, byte[] val) { + cache.putData(offset, val); + UnsignedLong uLoc = UnsignedLong.fromLongBits(offset); + UnsignedLong uEnd = UnsignedLong.fromLongBits(offset + val.length); + written.add(Range.closedOpen(uLoc, uEnd)); + } + + public static long lower(Range rng) { + return rng.lowerBoundType() == BoundType.CLOSED + ? rng.lowerEndpoint().longValue() + : rng.lowerEndpoint().longValue() + 1; + } + + public static long upper(Range rng) { + return rng.upperBoundType() == BoundType.CLOSED + ? rng.upperEndpoint().longValue() + : rng.upperEndpoint().longValue() - 1; + } + + public byte[] read(long offset, int size) { + if (source != null) { + // TODO: Warn or bail when reading UNKNOWN bytes + // NOTE: Not going to worry about gaps here: + RangeSet uninitialized = + cache.getUninitialized(offset, offset + size); + if (!uninitialized.isEmpty()) { + Range toRead = uninitialized.span(); + assert toRead.hasUpperBound() && toRead.hasLowerBound(); + long lower = lower(toRead); + long upper = upper(toRead); + ByteBuffer buf = ByteBuffer.allocate((int) (upper - lower + 1)); + source.getBytes(snap, space.getAddress(lower), buf); + cache.putData(lower, buf.array()); + } + } + byte[] data = new byte[size]; + cache.getData(offset, data); + return data; + } + + // Must already have started a transaction + protected void writeDown(Trace trace, long snap, TraceThread thread, int frame) { + if (space.isUniqueSpace()) { + return; + } + byte[] data = new byte[4096]; + ByteBuffer buf = ByteBuffer.wrap(data); + TraceMemorySpace mem = + TraceSleighUtils.getSpaceForExecution(space, trace, thread, frame, true); + for (Range range : written.asRanges()) { + assert range.lowerBoundType() == BoundType.CLOSED; + assert range.upperBoundType() == BoundType.OPEN; + long lower = range.lowerEndpoint().longValue(); + long fullLen = range.upperEndpoint().longValue() - lower; + while (fullLen > 0) { + int len = MathUtilities.unsignedMin(data.length, fullLen); + cache.getData(lower, data, 0, len); + buf.position(0); + buf.limit(len); + mem.putBytes(snap, space.getAddress(lower), buf); + + lower += len; + fullLen -= len; + } + } + } + } + + public Trace getTrace() { + return trace; + } + + public long getSnap() { + return snap; + } + + public TraceThread getThread() { + return thread; + } + + public int getFrame() { + return frame; + } + + /** + * Write the accumulated writes into the given trace + * + *

+ * NOTE: This method requires a transaction to have already been started on the destination + * trace. + * + * @param trace the trace to modify + * @param snap the snap within the trace + * @param thread the thread to take register writes + * @param frame the frame for register writes + */ + public void writeCacheDown(Trace trace, long snap, TraceThread thread, int frame) { + if (trace.getBaseLanguage() != language) { + throw new IllegalArgumentException("Destination trace must be same language as source"); + } + for (CachedSpace cached : spaces.values()) { + cached.writeDown(trace, snap, thread, frame); + } + } + + @Override + protected long offsetToLong(byte[] offset) { + return Utils.bytesToLong(offset, offset.length, language.isBigEndian()); + } + + @Override + public byte[] longToOffset(AddressSpace space, long l) { + return arithmetic.fromConst(l, space.getPointerSize()); + } + + @Override + protected CachedSpace getForSpace(AddressSpace space, boolean toWrite) { + return spaces.computeIfAbsent(space, s -> { + TraceMemorySpace tms = s.isUniqueSpace() ? null + : TraceSleighUtils.getSpaceForExecution(s, trace, thread, frame, false); + return new CachedSpace(s, tms, snap); + }); + } + + @Override + protected void setInSpace(CachedSpace space, long offset, int size, byte[] val) { + assert size == val.length; + space.write(offset, val); + } + + @Override + protected byte[] getFromSpace(CachedSpace space, long offset, int size) { + byte[] read = space.read(offset, size); + if (read.length != size) { + Address addr = space.space.getAddress(offset); + throw new UnknownStatePcodeExecutionException("Incomplete read (" + read.length + + " of " + size + " bytes)", language, addr.add(read.length), size - read.length); + } + return read; + } + + @Override + public MemBuffer getConcreteBuffer(Address address) { + return new StateMemBuffer(address, getForSpace(address.getAddressSpace(), false)); + } +} diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/TraceMemoryStatePcodeArithmetic.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/TraceMemoryStatePcodeArithmetic.java index dbb0f555e9..e4bc75b3cd 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/TraceMemoryStatePcodeArithmetic.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/TraceMemoryStatePcodeArithmetic.java @@ -15,6 +15,8 @@ */ package ghidra.pcode.exec.trace; +import java.math.BigInteger; + import ghidra.pcode.exec.PcodeArithmetic; import ghidra.pcode.opbehavior.BinaryOpBehavior; import ghidra.pcode.opbehavior.UnaryOpBehavior; @@ -43,8 +45,18 @@ public enum TraceMemoryStatePcodeArithmetic implements PcodeArithmetic { @@ -37,6 +39,8 @@ public class TraceMemoryStatePcodeExecutorStatePiece extends private TraceThread thread; private int frame; + private final DefaultTraceTimeViewport viewport; + public TraceMemoryStatePcodeExecutorStatePiece(Trace trace, long snap, TraceThread thread, int frame) { super(trace.getBaseLanguage(), TraceMemoryStatePcodeArithmetic.INSTANCE); @@ -44,6 +48,9 @@ public class TraceMemoryStatePcodeExecutorStatePiece extends this.snap = snap; this.thread = thread; this.frame = frame; + + this.viewport = new DefaultTraceTimeViewport(trace); + this.viewport.setSnap(snap); } public Trace getTrace() { @@ -52,6 +59,7 @@ public class TraceMemoryStatePcodeExecutorStatePiece extends public void setSnap(long snap) { this.snap = snap; + this.viewport.setSnap(snap); } public long getSnap() { @@ -137,7 +145,15 @@ public class TraceMemoryStatePcodeExecutorStatePiece extends @Override protected TraceMemoryState getFromSpace(TraceMemorySpace space, long offset, int size) { AddressSet set = new AddressSet(range(space.getAddressSpace(), offset, size)); - set.delete(space.getAddressesWithState(snap, set, s -> s == TraceMemoryState.KNOWN)); + for (long snap : viewport.getOrderedSnaps()) { + set.delete( + space.getAddressesWithState(snap, set, state -> state == TraceMemoryState.KNOWN)); + } return set.isEmpty() ? TraceMemoryState.KNOWN : TraceMemoryState.UNKNOWN; } + + @Override + public MemBuffer getConcreteBuffer(Address address) { + throw new AssertionError("Cannot make TraceMemoryState into a concrete buffer"); + } } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/TracePcodeEmulator.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/TracePcodeEmulator.java new file mode 100644 index 0000000000..207fbf82d9 --- /dev/null +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/TracePcodeEmulator.java @@ -0,0 +1,101 @@ +/* ### + * 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 ghidra.app.plugin.processors.sleigh.SleighLanguage; +import ghidra.pcode.emu.AbstractPcodeEmulator; +import ghidra.pcode.emu.PcodeThread; +import ghidra.pcode.exec.PcodeExecutorState; +import ghidra.pcode.exec.SleighUseropLibrary; +import ghidra.program.model.lang.Language; +import ghidra.trace.model.Trace; +import ghidra.trace.model.stack.TraceStack; +import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.model.thread.TraceThreadManager; + +/** + * An emulator that can read initial state from a trace + */ +public class TracePcodeEmulator extends AbstractPcodeEmulator { + private static SleighLanguage assertSleigh(Language language) { + if (!(language instanceof SleighLanguage)) { + throw new IllegalArgumentException("Emulation requires a sleigh language"); + } + return (SleighLanguage) language; + } + + protected final Trace trace; + protected final long snap; + + public TracePcodeEmulator(Trace trace, long snap, SleighUseropLibrary library) { + super(assertSleigh(trace.getBaseLanguage()), library); + this.trace = trace; + this.snap = snap; + } + + public TracePcodeEmulator(Trace trace, long snap) { + this(trace, snap, SleighUseropLibrary.nil()); + } + + @Override + protected PcodeExecutorState createMemoryState() { + return new TraceCachedWriteBytesPcodeExecutorState(trace, snap, null, 0); + } + + @Override + protected PcodeExecutorState createRegisterState(PcodeThread emuThread) { + TraceThread traceThread = + trace.getThreadManager().getLiveThreadByPath(snap, emuThread.getName()); + return new TraceCachedWriteBytesPcodeExecutorState(trace, snap, traceThread, 0); + } + + /** + * Write the accumulated writes into the given trace at the given snap + * + *

+ * NOTE: This method requires a transaction to have already been started on the destination + * trace. The destination threads must have equal names/paths at the given threadsSnap. When + * using scratch space, threadsSnap should be the source snap. If populating a new trace, + * threadsSnap should probably be the destination snap. + * + * @param trace the trace to modify + * @param destSnap the destination snap within the trace + * @param threadsSnap the snap at which to find corresponding threads + * @param synthesizeStacks true to synthesize the innermost stack frame of each thread + */ + public void writeDown(Trace trace, long destSnap, long threadsSnap, boolean synthesizeStacks) { + TraceCachedWriteBytesPcodeExecutorState ms = + (TraceCachedWriteBytesPcodeExecutorState) getMemoryState(); + ms.writeCacheDown(trace, destSnap, null, 0); + TraceThreadManager threadManager = trace.getThreadManager(); + for (PcodeThread emuThread : threads.values()) { + TraceCachedWriteBytesPcodeExecutorState rs = + (TraceCachedWriteBytesPcodeExecutorState) emuThread.getState().getRegisterState(); + TraceThread traceThread = threadManager.getLiveThreadByPath( + threadsSnap, emuThread.getName()); + if (traceThread == null) { + throw new IllegalArgumentException( + "Given trace does not have thread with name/path '" + emuThread.getName() + + "' at snap " + destSnap); + } + rs.writeCacheDown(trace, destSnap, traceThread, 0); + if (synthesizeStacks) { + TraceStack stack = trace.getStackManager().getStack(traceThread, destSnap, true); + stack.getFrame(0, true).setProgramCounter(emuThread.getCounter()); + } + } + } +} diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/TraceSleighUtils.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/TraceSleighUtils.java index e61c8813eb..00679bb6f8 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/TraceSleighUtils.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/TraceSleighUtils.java @@ -39,7 +39,7 @@ public enum TraceSleighUtils { if (space.isRegisterSpace()) { if (thread == null) { throw new IllegalArgumentException( - "Cannot execute with register context unless a thread is given."); + "Cannot access register unless a thread is given."); } return trace.getMemoryManager().getMemoryRegisterSpace(thread, frame, toWrite); } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/UnknownStatePcodeExecutionException.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/UnknownStatePcodeExecutionException.java new file mode 100644 index 0000000000..8112f20b39 --- /dev/null +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/UnknownStatePcodeExecutionException.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.pcode.exec.trace; + +import java.util.Arrays; + +import ghidra.pcode.exec.AccessPcodeExecutionException; +import ghidra.program.model.address.*; +import ghidra.program.model.lang.Language; +import ghidra.program.model.lang.Register; + +public class UnknownStatePcodeExecutionException extends AccessPcodeExecutionException { + + public static String getMessage(Language language, Address address, int size) { + if (address.getAddressSpace().isRegisterSpace()) { + Register reg = language.getRegister(address, size); + if (reg != null) { + return "No recorded value for register " + reg; + } + return "No recorded value for register(s) " + + Arrays.asList(language.getRegisters(address)); + } + try { + return "No recorded value for memory at " + new AddressRangeImpl(address, size); + } + catch (AddressOverflowException e) { + throw new AssertionError(e); + } + } + + public UnknownStatePcodeExecutionException(Language language, Address address, int size) { + super(getMessage(language, address, size)); + } + + public UnknownStatePcodeExecutionException(String message, Language language, Address address, + int size) { + super(message + ": " + getMessage(language, address, size)); + } +} diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/context/DBTraceRegisterContextManager.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/context/DBTraceRegisterContextManager.java index 630ac47ead..3af01638ed 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/context/DBTraceRegisterContextManager.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/context/DBTraceRegisterContextManager.java @@ -185,7 +185,8 @@ public class DBTraceRegisterContextManager extends public RegisterValue getValueWithDefault(Language language, Register register, long snap, Address address) { return delegateRead(address.getAddressSpace(), - m -> m.getValueWithDefault(language, register, snap, address)); + m -> m.getValueWithDefault(language, register, snap, address), + () -> getDefaultValue(language, register, address)); } @Override diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/data/DBTraceDataSettingsAdapter.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/data/DBTraceDataSettingsAdapter.java index f78c2ef86c..20ef264b39 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/data/DBTraceDataSettingsAdapter.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/data/DBTraceDataSettingsAdapter.java @@ -29,10 +29,10 @@ import ghidra.trace.database.DBTraceUtils; import ghidra.trace.database.data.DBTraceDataSettingsAdapter.DBTraceSettingsEntry; import ghidra.trace.database.map.*; import ghidra.trace.database.map.DBTraceAddressSnapRangePropertyMapTree.AbstractDBTraceAddressSnapRangePropertyMapData; -import ghidra.trace.database.space.DBTraceSpaceKey; import ghidra.trace.database.thread.DBTraceThread; import ghidra.trace.database.thread.DBTraceThreadManager; import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.util.TraceAddressSpace; import ghidra.util.database.*; import ghidra.util.database.annot.*; import ghidra.util.exception.VersionException; @@ -226,8 +226,8 @@ public class DBTraceDataSettingsAdapter } @Override - public DBTraceDataSettingsSpace get(DBTraceSpaceKey key, boolean createIfAbsent) { - return (DBTraceDataSettingsSpace) super.get(key, createIfAbsent); + public DBTraceDataSettingsSpace get(TraceAddressSpace space, boolean createIfAbsent) { + return (DBTraceDataSettingsSpace) super.get(space, createIfAbsent); } @Override diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/listing/AbstractDBTraceCodeUnit.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/listing/AbstractDBTraceCodeUnit.java index 402567a424..f7f00df079 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/listing/AbstractDBTraceCodeUnit.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/listing/AbstractDBTraceCodeUnit.java @@ -25,6 +25,7 @@ import ghidra.trace.database.map.DBTraceAddressSnapRangePropertyMapTree; import ghidra.trace.database.map.DBTraceAddressSnapRangePropertyMapTree.AbstractDBTraceAddressSnapRangePropertyMapData; import ghidra.trace.database.memory.DBTraceMemorySpace; import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.util.TraceAddressSpace; import ghidra.util.LockHold; import ghidra.util.database.DBCachedObjectStore; @@ -42,6 +43,11 @@ public abstract class AbstractDBTraceCodeUnit - implements DBTraceDefinedDataAdapter, DataAdapterFromDataType { - static final int[] EMPTY_INT_ARRAY = new int[0]; + implements DBTraceDefinedDataAdapter { private static final String TABLE_NAME = "Data"; static final String LANGUAGE_COLUMN_NAME = "Langauge"; diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/listing/DBTraceDataAdapter.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/listing/DBTraceDataAdapter.java index 3224d9079c..e1d5f36262 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/listing/DBTraceDataAdapter.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/listing/DBTraceDataAdapter.java @@ -20,66 +20,39 @@ import java.util.Collection; import ghidra.docking.settings.Settings; import ghidra.docking.settings.SettingsDefinition; import ghidra.program.model.address.Address; -import ghidra.program.model.data.DataType; -import ghidra.program.model.data.MutabilitySettingsDefinition; import ghidra.program.model.symbol.RefType; import ghidra.program.model.symbol.SourceType; import ghidra.trace.database.data.DBTraceDataSettingsOperations; import ghidra.trace.database.symbol.DBTraceReference; -import ghidra.trace.model.listing.TraceCodeManager; import ghidra.trace.model.listing.TraceData; import ghidra.trace.model.symbol.TraceReference; +import ghidra.trace.util.*; import ghidra.util.LockHold; -public interface DBTraceDataAdapter extends DBTraceCodeUnitAdapter, TraceData { +public interface DBTraceDataAdapter extends DBTraceCodeUnitAdapter, DataAdapterMinimal, + DataAdapterFromDataType, DataAdapterFromSettings, TraceData { static String[] EMPTY_STRING_ARRAY = new String[] {}; - default String doToString() { - StringBuilder builder = new StringBuilder(); - builder.append(getMnemonicString()); - String valueRepresentation = getDefaultValueRepresentation(); - if (valueRepresentation != null) { - builder.append(' '); - builder.append(valueRepresentation); - } - return builder.toString(); - } - - @Override - default String getMnemonicString() { - return getDataType().getMnemonic(this); - } - @Override DBTraceDataAdapter getRoot(); - default String getPrimarySymbolOrDynamicName() { - /** TODO: Use primary symbol or dynamic name as in {@link DataDB#getPathName()} */ - return "DAT_" + getAddressString(false, false); - } - - @Override - default int getNumOperands() { - return 1; - } - @Override default TraceReference[] getValueReferences() { - return getOperandReferences(TraceCodeManager.DATA_OP_INDEX); + return (TraceReference[]) DataAdapterMinimal.super.getValueReferences(); } @Override default void addValueReference(Address refAddr, RefType type) { getTrace().getReferenceManager() .addMemoryReference(getLifespan(), getAddress(), refAddr, - type, SourceType.USER_DEFINED, TraceCodeManager.DATA_OP_INDEX); + type, SourceType.USER_DEFINED, DATA_OP_INDEX); } @Override default void removeValueReference(Address refAddr) { DBTraceReference ref = getTrace().getReferenceManager() .getReference(getStartSnap(), - getAddress(), refAddr, TraceCodeManager.DATA_OP_INDEX); + getAddress(), refAddr, DATA_OP_INDEX); if (ref == null) { return; } @@ -198,38 +171,21 @@ public interface DBTraceDataAdapter extends DBTraceCodeUnitAdapter, TraceData { return space.isEmpty(getLifespan(), getAddress()); } + @Override default T getSettingsDefinition( Class settingsDefinitionClass) { - DataType dt = getBaseDataType(); - for (SettingsDefinition def : dt.getSettingsDefinitions()) { - if (settingsDefinitionClass.isAssignableFrom(def.getClass())) { - return settingsDefinitionClass.cast(def); - } + try (LockHold hold = LockHold.lock(getTrace().getReadWriteLock().readLock())) { + return DataAdapterFromSettings.super.getSettingsDefinition(settingsDefinitionClass); } - return null; } + @Override default boolean hasMutability(int mutabilityType) { try (LockHold hold = LockHold.lock(getTrace().getReadWriteLock().readLock())) { - MutabilitySettingsDefinition def = - getSettingsDefinition(MutabilitySettingsDefinition.class); - if (def != null) { - return def.getChoice(this) == mutabilityType; - } - return false; + return DataAdapterFromSettings.super.hasMutability(mutabilityType); } } - @Override - default boolean isConstant() { - return hasMutability(MutabilitySettingsDefinition.CONSTANT); - } - - @Override - default boolean isVolatile() { - return hasMutability(MutabilitySettingsDefinition.VOLATILE); - } - @Override DBTraceDataAdapter getPrimitiveAt(int offset); } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/listing/DBTraceDataArrayElementComponent.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/listing/DBTraceDataArrayElementComponent.java index 50419958cd..df608f578a 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/listing/DBTraceDataArrayElementComponent.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/listing/DBTraceDataArrayElementComponent.java @@ -15,8 +15,11 @@ */ package ghidra.trace.database.listing; -import ghidra.program.model.address.Address; +import ghidra.program.model.address.*; import ghidra.program.model.data.DataType; +import ghidra.trace.model.ImmutableTraceAddressSnapRange; +import ghidra.trace.model.TraceAddressSnapRange; +import ghidra.trace.util.TraceAddressSpace; public class DBTraceDataArrayElementComponent extends AbstractDBTraceDataComponent { public DBTraceDataArrayElementComponent(DBTraceData root, DBTraceDefinedDataAdapter parent, @@ -24,6 +27,11 @@ public class DBTraceDataArrayElementComponent extends AbstractDBTraceDataCompone super(root, parent, index, address, dataType, length); } + @Override + public TraceAddressSpace getTraceSpace() { + return parent.getTraceSpace(); + } + @Override public String getFieldName() { return "[" + index + "]"; @@ -33,4 +41,16 @@ public class DBTraceDataArrayElementComponent extends AbstractDBTraceDataCompone public String getFieldSyntax() { return getFieldName(); } + + @Override + public AddressRange getRange() { + // TODO: Cache this? + return new AddressRangeImpl(getMinAddress(), getMaxAddress()); + } + + @Override + public TraceAddressSnapRange getBounds() { + // TODO: Cache this? + return new ImmutableTraceAddressSnapRange(getMinAddress(), getMaxAddress(), getLifespan()); + } } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/listing/DBTraceDataCompositeFieldComponent.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/listing/DBTraceDataCompositeFieldComponent.java index 1af1cced5d..f79bc352fc 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/listing/DBTraceDataCompositeFieldComponent.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/listing/DBTraceDataCompositeFieldComponent.java @@ -15,8 +15,11 @@ */ package ghidra.trace.database.listing; -import ghidra.program.model.address.Address; +import ghidra.program.model.address.*; import ghidra.program.model.data.DataTypeComponent; +import ghidra.trace.model.ImmutableTraceAddressSnapRange; +import ghidra.trace.model.TraceAddressSnapRange; +import ghidra.trace.util.TraceAddressSpace; public class DBTraceDataCompositeFieldComponent extends AbstractDBTraceDataComponent { protected final DataTypeComponent dtc; @@ -27,6 +30,11 @@ public class DBTraceDataCompositeFieldComponent extends AbstractDBTraceDataCompo this.dtc = dtc; } + @Override + public TraceAddressSpace getTraceSpace() { + return parent.getTraceSpace(); + } + @Override public String getFieldName() { String fieldName = dtc.getFieldName(); @@ -40,4 +48,16 @@ public class DBTraceDataCompositeFieldComponent extends AbstractDBTraceDataCompo public String getFieldSyntax() { return "." + getFieldName(); } + + @Override + public AddressRange getRange() { + // TODO: Cache this? + return new AddressRangeImpl(getMinAddress(), getMaxAddress()); + } + + @Override + public TraceAddressSnapRange getBounds() { + // TODO: Cache this? + return new ImmutableTraceAddressSnapRange(getMinAddress(), getMaxAddress(), getLifespan()); + } } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/listing/DBTraceInstruction.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/listing/DBTraceInstruction.java index d1041f0621..f7e1db5dfd 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/listing/DBTraceInstruction.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/listing/DBTraceInstruction.java @@ -22,7 +22,6 @@ import java.util.*; import com.google.common.collect.Range; import db.DBRecord; -import ghidra.lifecycle.Unfinished; import ghidra.program.model.address.*; import ghidra.program.model.lang.*; import ghidra.program.model.listing.ContextChangeException; @@ -47,7 +46,7 @@ import ghidra.util.database.annot.*; @DBAnnotatedObjectInfo(version = 0) public class DBTraceInstruction extends AbstractDBTraceCodeUnit implements - TraceInstruction, InstructionAdapterFromPrototype, InstructionContext, Unfinished { + TraceInstruction, InstructionAdapterFromPrototype, InstructionContext { private static final Address[] EMPTY_ADDRESS_ARRAY = new Address[] {}; private static final String TABLE_NAME = "Instructions"; @@ -347,7 +346,7 @@ public class DBTraceInstruction extends AbstractDBTraceCodeUnit refs = - refSpace.getFlowRefrencesFrom(getStartSnap(), getAddress()); + refSpace.getFlowReferencesFrom(getStartSnap(), getAddress()); if (refs.isEmpty()) { return EMPTY_ADDRESS_ARRAY; } @@ -461,7 +460,7 @@ public class DBTraceInstruction extends AbstractDBTraceCodeUnit lifespan; @@ -55,6 +55,11 @@ public class UndefinedDBTraceData this.frameLevel = frameLevel; } + @Override + public TraceAddressSpace getTraceSpace() { + return this; + } + @Override public AddressSpace getAddressSpace() { return address.getAddressSpace(); @@ -75,6 +80,18 @@ public class UndefinedDBTraceData return trace.getBaseLanguage(); } + @Override + public AddressRange getRange() { + // TODO: Cache this? + return new AddressRangeImpl(getMinAddress(), getMaxAddress()); + } + + @Override + public TraceAddressSnapRange getBounds() { + // TODO: Cache this? + return new ImmutableTraceAddressSnapRange(getMinAddress(), getMaxAddress(), getLifespan()); + } + @Override public Range getLifespan() { return lifespan; @@ -207,7 +224,7 @@ public class UndefinedDBTraceData @Override public int[] getComponentPath() { - return DBTraceData.EMPTY_INT_ARRAY; + return EMPTY_INT_ARRAY; } @Override diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/map/DBTraceAddressSnapRangePropertyMapAddressSetView.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/map/DBTraceAddressSnapRangePropertyMapAddressSetView.java index 839eea7ab9..320c5c89a8 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/map/DBTraceAddressSnapRangePropertyMapAddressSetView.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/map/DBTraceAddressSnapRangePropertyMapAddressSetView.java @@ -165,7 +165,8 @@ public class DBTraceAddressSnapRangePropertyMapAddressSetView extends Abstrac : new AddressRangeImpl(fullSpace.getMinAddress(), start); Iterator> mapIt = map .reduce(TraceAddressSnapRangeQuery.intersecting(within, Range.all()) - .starting(forward ? Rectangle2DDirection.LEFTMOST + .starting(forward + ? Rectangle2DDirection.LEFTMOST : Rectangle2DDirection.RIGHTMOST)) .orderedEntries() .iterator(); diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/map/DBTraceAddressSnapRangePropertyMapTree.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/map/DBTraceAddressSnapRangePropertyMapTree.java index 57cd9749f3..9253b1da18 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/map/DBTraceAddressSnapRangePropertyMapTree.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/map/DBTraceAddressSnapRangePropertyMapTree.java @@ -505,6 +505,12 @@ public class DBTraceAddressSnapRangePropertyMapTree span) { + return intersecting( + new ImmutableTraceAddressSnapRange(address, span), + Rectangle2DDirection.TOPMOST, TraceAddressSnapRangeQuery::new); + } + public static TraceAddressSnapRangeQuery equalTo(TraceAddressSnapRange shape) { return equalTo(shape, null, TraceAddressSnapRangeQuery::new); } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceMemBuffer.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceMemBuffer.java index 8500079772..6e4930f880 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceMemBuffer.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceMemBuffer.java @@ -56,7 +56,7 @@ public class DBTraceMemBuffer implements MemBufferAdapter { @Override public int getBytes(ByteBuffer buffer, int offset) { try { - return space.getBytes(snap, start.addNoWrap(offset), buffer); + return space.getViewBytes(snap, start.addNoWrap(offset), buffer); } catch (AddressOverflowException e) { return 0; diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceMemoryManager.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceMemoryManager.java index 8bd2897c82..5c4a32bb84 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceMemoryManager.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceMemoryManager.java @@ -211,6 +211,11 @@ public class DBTraceMemoryManager return delegateRead(address.getAddressSpace(), m -> m.getState(snap, address)); } + @Override + public Entry getViewState(long snap, Address address) { + return delegateRead(address.getAddressSpace(), m -> m.getViewState(snap, address)); + } + @Override public Entry getMostRecentStateEntry(long snap, Address address) { @@ -218,6 +223,13 @@ public class DBTraceMemoryManager m -> m.getMostRecentStateEntry(snap, address)); } + @Override + public Entry getViewMostRecentStateEntry(long snap, + Address address) { + return delegateRead(address.getAddressSpace(), + m -> m.getViewMostRecentStateEntry(snap, address)); + } + @Override public AddressSetView getAddressesWithState(long snap, AddressSetView set, Predicate predicate) { @@ -267,6 +279,16 @@ public class DBTraceMemoryManager }); } + @Override + public int getViewBytes(long snap, Address start, ByteBuffer buf) { + return delegateReadI(start.getAddressSpace(), m -> m.getViewBytes(snap, start, buf), () -> { + Address max = start.getAddressSpace().getMaxAddress(); + int len = MathUtilities.unsignedMin(buf.remaining(), max.subtract(start)); + buf.position(buf.position() + len); + return len; + }); + } + @Override public void removeBytes(long snap, Address start, int len) { delegateDeleteV(start.getAddressSpace(), m -> m.removeBytes(snap, start, len)); diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceMemorySpace.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceMemorySpace.java index 649d5f2488..02be120bb6 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceMemorySpace.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceMemorySpace.java @@ -46,7 +46,9 @@ import ghidra.trace.model.*; import ghidra.trace.model.Trace.*; import ghidra.trace.model.memory.*; import ghidra.trace.util.TraceChangeRecord; +import ghidra.trace.util.TraceViewportSpanIterator; import ghidra.util.LockHold; +import ghidra.util.MathUtilities; import ghidra.util.database.*; import ghidra.util.database.spatial.rect.Rectangle2DDirection; import ghidra.util.exception.DuplicateNameException; @@ -329,8 +331,7 @@ public class DBTraceMemorySpace implements Unfinished, TraceMemorySpace, DBTrace protected void checkState(TraceMemoryState state) { if (state == null || state == TraceMemoryState.UNKNOWN) { - throw new IllegalArgumentException( - "User cannot erase memory state without removing bytes"); + throw new IllegalArgumentException("Cannot erase memory state without removing bytes"); } } @@ -376,6 +377,26 @@ public class DBTraceMemorySpace implements Unfinished, TraceMemorySpace, DBTrace return state == null ? TraceMemoryState.UNKNOWN : state; } + @Override + public Entry getViewState(long snap, Address address) { + TraceViewportSpanIterator spit = new TraceViewportSpanIterator(trace, snap); + while (spit.hasNext()) { + Range span = spit.next(); + TraceMemoryState state = getState(span.upperEndpoint(), address); + switch (state) { + case KNOWN: + case ERROR: + return Map.entry(span.upperEndpoint(), state); + default: // fall through + } + // Only the snap with the schedule specified gets the source snap's states + if (span.upperEndpoint() - span.lowerEndpoint() > 0) { + return Map.entry(snap, TraceMemoryState.UNKNOWN); + } + } + return Map.entry(snap, TraceMemoryState.UNKNOWN); + } + @Override public Entry getMostRecentStateEntry(long snap, Address address) { @@ -383,6 +404,22 @@ public class DBTraceMemorySpace implements Unfinished, TraceMemorySpace, DBTrace TraceAddressSnapRangeQuery.mostRecent(address, snap)).firstEntry(); } + @Override + public Entry getViewMostRecentStateEntry(long snap, + Address address) { + TraceViewportSpanIterator spit = new TraceViewportSpanIterator(trace, snap); + while (spit.hasNext()) { + Range span = spit.next(); + Entry entry = + stateMapSpace.reduce(TraceAddressSnapRangeQuery.mostRecent(address, span)) + .firstEntry(); + if (entry != null) { + return entry; + } + } + return null; + } + @Override public AddressSetView getAddressesWithState(long snap, Predicate predicate) { return new DBTraceAddressSnapRangePropertyMapAddressSetView<>(space, lock, @@ -658,6 +695,58 @@ public class DBTraceMemorySpace implements Unfinished, TraceMemorySpace, DBTrace return result; } + @Override + public int getViewBytes(long snap, Address start, ByteBuffer buf) { + AddressRange toRead; + int len = MathUtilities.unsignedMin(buf.remaining(), + start.getAddressSpace().getMaxAddress().subtract(start) + 1); + try { + toRead = new AddressRangeImpl(start, len); + } + catch (AddressOverflowException e) { + throw new AssertionError(e); + } + Map sources = new TreeMap<>(); + AddressSet remains = new AddressSet(toRead); + TraceViewportSpanIterator spit = new TraceViewportSpanIterator(trace, snap); + spans: while (spit.hasNext()) { + Range span = spit.next(); + Iterator arit = + getAddressesWithState(span, s -> s == TraceMemoryState.KNOWN).iterator(start, true); + while (arit.hasNext()) { + AddressRange rng = arit.next(); + if (rng.getMinAddress().compareTo(toRead.getMaxAddress()) > 0) { + break; + } + for (AddressRange sub : remains.intersectRange(rng.getMinAddress(), + rng.getMaxAddress())) { + sources.put(sub, span.upperEndpoint()); + } + remains.delete(rng); + if (remains.isEmpty()) { + break spans; + } + } + } + int lim = buf.limit(); + int pos = buf.position(); + for (Map.Entry ent : sources.entrySet()) { + AddressRange rng = ent.getKey(); + int offset = (int) rng.getMinAddress().subtract(toRead.getMinAddress()); + int length = (int) rng.getLength(); + buf.position(pos + offset); + buf.limit(pos + offset + length); + int read = getBytes(ent.getValue(), rng.getMinAddress(), buf); + if (read < length) { + break; + } + } + // We "got it all", even if there were gaps in "KNOWN" + buf.limit(lim); + buf.position(pos + len); + return len; + } + @Override public Address findBytes(long snap, AddressRange range, ByteBuffer data, ByteBuffer mask, boolean forward, TaskMonitor monitor) { diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/AbstractDBTraceProgramViewListing.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/AbstractDBTraceProgramViewListing.java index db37e457eb..9888c57eea 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/AbstractDBTraceProgramViewListing.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/AbstractDBTraceProgramViewListing.java @@ -15,8 +15,12 @@ */ package ghidra.trace.database.program; +import java.nio.ByteBuffer; import java.util.*; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.RemovalNotification; +import com.google.common.collect.Iterators; import com.google.common.collect.Range; import generic.NestedIterator; @@ -32,17 +36,21 @@ import ghidra.program.model.symbol.Namespace; import ghidra.program.model.symbol.SourceType; import ghidra.program.model.util.CodeUnitInsertionException; import ghidra.program.model.util.PropertyMap; +import ghidra.trace.database.DBTrace; +import ghidra.trace.database.listing.UndefinedDBTraceData; import ghidra.trace.database.memory.DBTraceMemoryRegion; +import ghidra.trace.database.memory.DBTraceMemorySpace; import ghidra.trace.database.symbol.DBTraceFunctionSymbol; -import ghidra.trace.model.Trace; -import ghidra.trace.model.listing.TraceCodeOperations; -import ghidra.trace.model.listing.TraceData; +import ghidra.trace.database.thread.DBTraceThread; +import ghidra.trace.model.*; +import ghidra.trace.model.listing.*; import ghidra.trace.model.map.TracePropertyMap; import ghidra.trace.model.program.TraceProgramView; import ghidra.trace.model.program.TraceProgramViewListing; import ghidra.trace.model.symbol.TraceFunctionSymbol; -import ghidra.util.IntersectionAddressSetView; -import ghidra.util.LockHold; +import ghidra.trace.util.*; +import ghidra.util.*; +import ghidra.util.AddressIteratorAdapter; import ghidra.util.exception.*; import ghidra.util.task.TaskMonitor; @@ -50,72 +58,19 @@ public abstract class AbstractDBTraceProgramViewListing implements TraceProgramV public static final String[] EMPTY_STRING_ARRAY = new String[] {}; public static final String TREE_NAME = "Trace Tree"; - protected static class WrappingCodeUnitIterator implements CodeUnitIterator { - protected final Iterator it; - - public WrappingCodeUnitIterator(Iterator it) { - this.it = it; + protected class DBTraceProgramViewUndefinedData extends UndefinedDBTraceData { + public DBTraceProgramViewUndefinedData(DBTrace trace, long snap, Address address, + DBTraceThread thread, int frameLevel) { + super(trace, snap, address, thread, frameLevel); } @Override - public Iterator iterator() { - return this; - } - - @Override - public boolean hasNext() { - return it.hasNext(); - } - - @Override - public CodeUnit next() { - return it.next(); - } - } - - protected static class WrappingInstructionIterator implements InstructionIterator { - protected final Iterator it; - - public WrappingInstructionIterator(Iterator it) { - this.it = it; - } - - @Override - public Iterator iterator() { - return this; - } - - @Override - public boolean hasNext() { - return it.hasNext(); - } - - @Override - public Instruction next() { - return it.next(); - } - } - - protected static class WrappingDataIterator implements DataIterator { - protected final Iterator it; - - public WrappingDataIterator(Iterator it) { - this.it = it; - } - - @Override - public Iterator iterator() { - return this; - } - - @Override - public boolean hasNext() { - return it.hasNext(); - } - - @Override - public Data next() { - return it.next(); + public int getBytes(ByteBuffer buffer, int addressOffset) { + DBTraceMemorySpace mem = trace.getMemoryManager().get(this, false); + if (mem == null) { + // TODO: 0-fill instead? Will need to check memory space bounds. + } + return mem.getViewBytes(program.snap, address.add(addressOffset), buffer); } } @@ -126,6 +81,14 @@ public abstract class AbstractDBTraceProgramViewListing implements TraceProgramV protected final Map fragmentsByRegion = new HashMap<>(); + protected final Map undefinedCache = + CacheBuilder.newBuilder() + .removalListener( + this::undefinedRemovedFromCache) + .weakValues() + .build() + .asMap(); + public AbstractDBTraceProgramViewListing(DBTraceProgramView program, TraceCodeOperations codeOperations) { this.program = program; @@ -134,6 +97,11 @@ public abstract class AbstractDBTraceProgramViewListing implements TraceProgramV this.rootModule = new DBTraceProgramViewRootModule(this); } + private void undefinedRemovedFromCache( + RemovalNotification rn) { + // Do nothing + } + @Override public TraceProgramView getProgram() { return program; @@ -149,32 +117,241 @@ public abstract class AbstractDBTraceProgramViewListing implements TraceProgramV return program.snap; } + protected T getTopCode( + java.util.function.Function codeFunc) { + return program.viewport.getTop(s -> { + T cu = codeFunc.apply(s); + if (cu != null && program.isCodeVisible(cu, cu.getLifespan())) { + return cu; + } + return null; + }); + } + + protected TraceCodeUnit orUndef(TraceCodeUnit cu, Address address) { + if (cu != null) { + return cu; + } + return doCreateUndefinedUnit(address); + } + + protected TraceData orUndefData(TraceData data, Address address) { + return (TraceData) orUndef(data, address); + } + + protected TraceData reqUndef(TraceCodeUnit cu, Address address) { + if (cu != null) { + return null; + } + return doCreateUndefinedUnit(address); + } + + protected T next(Iterator it) { + if (it.hasNext()) { + return it.next(); + } + return null; + } + + protected Comparator getUnitComparator(boolean forward) { + return forward + ? (u1, u2) -> u1.getMinAddress().compareTo(u2.getMinAddress()) + : (u1, u2) -> -u1.getMinAddress().compareTo(u2.getMinAddress()); + } + + protected Iterator getTopCodeIterator( + java.util.function.Function> iterFunc, boolean forward) { + return Iterators.filter( + program.viewport.mergedIterator(iterFunc, getUnitComparator(forward)), + cu -> program.isCodeVisible(cu, cu.getLifespan())); + } + + protected AddressSet getAddressSet(Address start, boolean forward) { + AddressFactory factory = program.getAddressFactory(); + AddressSet all = program.allAddresses; + return forward + ? factory.getAddressSet(start, all.getMaxAddress()) + : factory.getAddressSet(all.getMinAddress(), start); + } + + protected UndefinedDBTraceData doCreateUndefinedUnit(Address address) { + return undefinedCache.computeIfAbsent(new DefaultAddressSnap(address, program.snap), + ot -> new DBTraceProgramViewUndefinedData(program.trace, program.snap, address, null, + 0)); + } + + protected Iterator getInstructionIterator(Address start, + boolean forward) { + return getTopCodeIterator( + s -> codeOperations.instructions().get(s, start, forward).iterator(), forward); + } + + protected Iterator getInstructionIterator(AddressSetView set, + boolean forward) { + return getTopCodeIterator( + s -> codeOperations.instructions().get(s, set, forward).iterator(), forward); + } + + protected Iterator getInstructionIterator(boolean forward) { + return getTopCodeIterator( + s -> codeOperations.instructions().get(s, forward).iterator(), forward); + } + + protected Iterator getDefinedDataIterator(Address start, boolean forward) { + return getTopCodeIterator( + s -> codeOperations.definedData().get(s, start, forward).iterator(), forward); + } + + protected Iterator getDefinedDataIterator(AddressSetView set, + boolean forward) { + return getTopCodeIterator( + s -> codeOperations.definedData().get(s, set, forward).iterator(), forward); + } + + protected Iterator getDefinedDataIterator(boolean forward) { + return getTopCodeIterator( + s -> codeOperations.definedData().get(s, forward).iterator(), forward); + } + + protected Iterator getDefinedUnitIterator(Address start, + boolean forward) { + return getTopCodeIterator( + s -> codeOperations.definedUnits().get(s, start, forward).iterator(), forward); + } + + protected Iterator getDefinedUnitIterator(AddressSetView set, + boolean forward) { + return getTopCodeIterator( + s -> codeOperations.definedUnits().get(s, set, forward).iterator(), forward); + } + + protected Iterator getUndefinedDataIterator(Address start, boolean forward) { + AddressSet set = getAddressSet(start, forward); + Address defStart = start; + if (forward) { + CodeUnit defUnit = + getTopCode(s -> codeOperations.definedUnits().getContaining(s, start)); + if (defUnit != null) { + defStart = defUnit.getMinAddress(); + } + } + Iterator defIter = Iterators.transform( + getDefinedUnitIterator(defStart, forward), u -> u.getRange()); + AddressRangeIterator undefIter = + AddressRangeIterators.subtract(set.iterator(forward), defIter, start, forward); + AddressIteratorAdapter undefAddrIter = new AddressIteratorAdapter(undefIter, forward); + return Iterators.transform(undefAddrIter.iterator(), a -> doCreateUndefinedUnit(a)); + } + + protected AddressRangeIterator getUndefinedRangeIterator(AddressSetView set, boolean forward) { + Iterator defIter = Iterators.transform( + getDefinedUnitIterator(set, forward), u -> u.getRange()); + return AddressRangeIterators.subtract(set.iterator(forward), defIter, + forward ? set.getMinAddress() : set.getMaxAddress(), forward); + } + + protected boolean isUndefinedRange(long snap, AddressRange range) { + if (codeOperations.undefinedData().coversRange(Range.singleton(snap), range)) { + return true; + } + TraceCodeUnit minUnit = + codeOperations.definedUnits().getContaining(snap, range.getMinAddress()); + if (minUnit != null && program.isCodeVisible(minUnit, minUnit.getLifespan())) { + return false; + } + TraceCodeUnit maxUnit = + codeOperations.definedUnits().getContaining(snap, range.getMaxAddress()); + if (maxUnit != null && program.isCodeVisible(maxUnit, maxUnit.getLifespan())) { + return false; + } + return true; + } + + protected Iterator getUndefinedDataIterator(AddressSetView set, boolean forward) { + AddressRangeIterator undefIter = getUndefinedRangeIterator(set, forward); + AddressIteratorAdapter undefAddrIter = new AddressIteratorAdapter(undefIter, forward); + return Iterators.transform(undefAddrIter.iterator(), a -> doCreateUndefinedUnit(a)); + } + + protected Iterator getCodeUnitIterator(AddressSetView set, boolean forward) { + return new MergeSortingIterator<>(List.of( + getDefinedUnitIterator(set, forward), + getUndefinedDataIterator(set, forward)), + getUnitComparator(forward)); + } + + protected Iterator getCodeUnitIterator(Address start, boolean forward) { + return new MergeSortingIterator<>(List.of( + getDefinedUnitIterator(start, forward), + getUndefinedDataIterator(start, forward)), + getUnitComparator(forward)); + } + + protected Iterator getCodeUnitIterator(boolean forward) { + AddressSetView set = program.allAddresses; + return getCodeUnitIterator(forward ? set.getMinAddress() : set.getMaxAddress(), forward); + } + + protected Iterator getDataIterator(AddressSetView set, boolean forward) { + return new MergeSortingIterator<>(List.of( + getDefinedDataIterator(set, forward), + getUndefinedDataIterator(set, forward)), + getUnitComparator(forward)); + } + + protected Iterator getDataIterator(Address start, boolean forward) { + return new MergeSortingIterator<>(List.of( + getDefinedDataIterator(start, forward), + getUndefinedDataIterator(start, forward)), + getUnitComparator(forward)); + } + + protected Iterator getDataIterator(boolean forward) { + AddressSetView set = program.allAddresses; + return getDataIterator(forward ? set.getMinAddress() : set.getMaxAddress(), forward); + } + @Override public CodeUnit getCodeUnitAt(Address addr) { - return codeOperations.codeUnits().getAt(program.snap, addr); + CodeUnit containing = getCodeUnitContaining(addr); + if (containing == null) { + return doCreateUndefinedUnit(addr); + } + if (!containing.getMinAddress().equals(addr)) { + return null; + } + return containing; } @Override public CodeUnit getCodeUnitContaining(Address addr) { - return codeOperations.codeUnits().getContaining(program.snap, addr); + try (LockHold hold = program.trace.lockRead()) { + return orUndef(getTopCode(s -> codeOperations.definedUnits().getContaining(s, addr)), + addr); + } } @Override public CodeUnit getCodeUnitAfter(Address addr) { - return codeOperations.codeUnits().getAfter(program.snap, addr); + addr = addr.next(); + try (LockHold hold = program.trace.lockRead()) { + return addr == null ? null : next(getCodeUnitIterator(addr, true)); + } } @Override public CodeUnit getCodeUnitBefore(Address addr) { - return codeOperations.codeUnits().getBefore(program.snap, addr); + addr = addr.previous(); + try (LockHold hold = program.trace.lockRead()) { + return addr == null ? null : next(getCodeUnitIterator(addr, false)); + } } @Override public CodeUnitIterator getCodeUnitIterator(String property, boolean forward) { // HACK if (CodeUnit.INSTRUCTION_PROPERTY.equals(property)) { - return new WrappingCodeUnitIterator( - codeOperations.instructions().get(program.snap, forward).iterator()); + return new WrappingCodeUnitIterator(getInstructionIterator(forward)); } // TODO: Other "special" property types @@ -184,28 +361,19 @@ public abstract class AbstractDBTraceProgramViewListing implements TraceProgramV if (map == null) { return new WrappingCodeUnitIterator(Collections.emptyIterator()); } + // TODO: The property map doesn't heed forking. return new WrappingCodeUnitIterator(NestedIterator.start( map.getAddressSetView(Range.singleton(program.snap)).iterator(forward), - rng -> program.trace.getCodeManager() - .codeUnits() - .get(program.snap, rng, forward) - .iterator())); - } - - protected static AddressRange fixRange(AddressRange range, Address start, boolean forward) { - if (!range.contains(start)) { - return range; - } - return forward ? new AddressRangeImpl(start, range.getMaxAddress()) - : new AddressRangeImpl(range.getMinAddress(), start); + rng -> getTopCodeIterator( + s -> codeOperations.codeUnits().get(s, rng, forward).iterator(), + forward))); } @Override public CodeUnitIterator getCodeUnitIterator(String property, Address addr, boolean forward) { // HACK if (CodeUnit.INSTRUCTION_PROPERTY.equals(property)) { - return new WrappingCodeUnitIterator( - codeOperations.instructions().get(program.snap, addr, forward).iterator()); + return new WrappingCodeUnitIterator(getInstructionIterator(addr, forward)); } // TODO: Other "special" property types @@ -215,12 +383,12 @@ public abstract class AbstractDBTraceProgramViewListing implements TraceProgramV if (map == null) { return new WrappingCodeUnitIterator(Collections.emptyIterator()); } + // TODO: The property map doesn't heed forking. return new WrappingCodeUnitIterator(NestedIterator.start( map.getAddressSetView(Range.singleton(program.snap)).iterator(addr, forward), - rng -> program.trace.getCodeManager() - .codeUnits() - .get(program.snap, fixRange(rng, addr, forward), forward) - .iterator())); + rng -> getTopCodeIterator( + s -> codeOperations.codeUnits().get(s, rng, forward).iterator(), + forward))); } @Override @@ -228,8 +396,7 @@ public abstract class AbstractDBTraceProgramViewListing implements TraceProgramV boolean forward) { // HACK if (CodeUnit.INSTRUCTION_PROPERTY.equals(property)) { - return new WrappingCodeUnitIterator( - codeOperations.instructions().get(program.snap, addrSet, forward).iterator()); + return new WrappingCodeUnitIterator(getInstructionIterator(addrSet, forward)); } // TODO: Other "special" property types @@ -239,13 +406,13 @@ public abstract class AbstractDBTraceProgramViewListing implements TraceProgramV if (map == null) { return new WrappingCodeUnitIterator(Collections.emptyIterator()); } + // TODO: The property map doesn't heed forking. return new WrappingCodeUnitIterator(NestedIterator.start( new IntersectionAddressSetView(map.getAddressSetView(Range.singleton(program.snap)), addrSet).iterator(forward), - rng -> program.trace.getCodeManager() - .codeUnits() - .get(program.snap, rng, forward) - .iterator())); + rng -> getTopCodeIterator( + s -> codeOperations.codeUnits().get(s, rng, forward).iterator(), + forward))); } @Override @@ -269,7 +436,10 @@ public abstract class AbstractDBTraceProgramViewListing implements TraceProgramV @Override public String getComment(int commentType, Address address) { - return program.trace.getCommentAdapter().getComment(program.snap, address, commentType); + try (LockHold hold = program.trace.lockRead()) { + return program.viewport.getTop( + s -> program.trace.getCommentAdapter().getComment(s, address, commentType)); + } } @Override @@ -281,190 +451,227 @@ public abstract class AbstractDBTraceProgramViewListing implements TraceProgramV @Override public CodeUnitIterator getCodeUnits(boolean forward) { - return new WrappingCodeUnitIterator( - codeOperations.codeUnits().get(program.snap, forward).iterator()); + return new WrappingCodeUnitIterator(getCodeUnitIterator(forward)); } @Override public CodeUnitIterator getCodeUnits(Address start, boolean forward) { - return new WrappingCodeUnitIterator( - codeOperations.codeUnits().get(program.snap, start, forward).iterator()); + return new WrappingCodeUnitIterator(getCodeUnitIterator(start, forward)); } @Override public CodeUnitIterator getCodeUnits(AddressSetView addressSet, boolean forward) { - return new WrappingCodeUnitIterator( - codeOperations.codeUnits().get(program.snap, addressSet, forward).iterator()); + return new WrappingCodeUnitIterator(getCodeUnitIterator(addressSet, forward)); } @Override public Instruction getInstructionAt(Address addr) { - return codeOperations.instructions().getAt(program.snap, addr); + try (LockHold hold = program.trace.lockRead()) { + return getTopCode(s -> codeOperations.instructions().getAt(s, addr)); + } } @Override public Instruction getInstructionContaining(Address addr) { - return codeOperations.instructions().getContaining(program.snap, addr); + try (LockHold hold = program.trace.lockRead()) { + return getTopCode(s -> codeOperations.instructions().getContaining(s, addr)); + } } @Override public Instruction getInstructionAfter(Address addr) { - return codeOperations.instructions().getAfter(program.snap, addr); + addr = addr.next(); + try (LockHold hold = program.trace.lockRead()) { + return addr == null ? null : next(getInstructionIterator(addr, true)); + } } @Override public Instruction getInstructionBefore(Address addr) { - return codeOperations.instructions().getBefore(program.snap, addr); + addr = addr.previous(); + try (LockHold hold = program.trace.lockRead()) { + return addr == null ? null : next(getInstructionIterator(addr, false)); + } } @Override public InstructionIterator getInstructions(boolean forward) { - return new WrappingInstructionIterator( - codeOperations.instructions().get(program.snap, forward).iterator()); + return new WrappingInstructionIterator(getInstructionIterator(forward)); } @Override public InstructionIterator getInstructions(Address start, boolean forward) { - return new WrappingInstructionIterator( - codeOperations.instructions().get(program.snap, start, forward).iterator()); + return new WrappingInstructionIterator(getInstructionIterator(start, forward)); } @Override public InstructionIterator getInstructions(AddressSetView addressSet, boolean forward) { - return new WrappingInstructionIterator( - codeOperations.instructions().get(program.snap, addressSet, forward).iterator()); + return new WrappingInstructionIterator(getInstructionIterator(addressSet, forward)); } @Override public Data getDataAt(Address addr) { - return codeOperations.data().getAt(program.snap, addr); + CodeUnit containing = getCodeUnitContaining(addr); + if (containing == null) { + return doCreateUndefinedUnit(addr); + } + if (!(containing instanceof Data)) { + return null; + } + if (!containing.getMinAddress().equals(addr)) { + return null; + } + return (Data) containing; } @Override public Data getDataContaining(Address addr) { - return codeOperations.data().getContaining(program.snap, addr); + CodeUnit cu = getCodeUnitContaining(addr); + if (cu instanceof Data) { + return (Data) cu; + } + return null; } @Override public Data getDataAfter(Address addr) { - return codeOperations.data().getAfter(program.snap, addr); + addr = addr.next(); + try (LockHold hold = program.trace.lockRead()) { + return addr == null ? null : next(getDataIterator(addr, true)); + } } @Override public Data getDataBefore(Address addr) { - return codeOperations.data().getBefore(program.snap, addr); + addr = addr.previous(); + try (LockHold hold = program.trace.lockRead()) { + return addr == null ? null : next(getDataIterator(addr, false)); + } } @Override public DataIterator getData(boolean forward) { - return new WrappingDataIterator( - codeOperations.data().get(program.snap, forward).iterator()); + return new WrappingDataIterator(getDataIterator(forward)); } @Override public DataIterator getData(Address start, boolean forward) { - return new WrappingDataIterator( - codeOperations.data().get(program.snap, start, forward).iterator()); + return new WrappingDataIterator(getDataIterator(start, forward)); } @Override public DataIterator getData(AddressSetView addressSet, boolean forward) { - return new WrappingDataIterator( - codeOperations.data().get(program.snap, addressSet, forward).iterator()); + return new WrappingDataIterator(getDataIterator(addressSet, forward)); } @Override public Data getDefinedDataAt(Address addr) { - return codeOperations.definedData().getAt(program.snap, addr); + try (LockHold hold = program.trace.lockRead()) { + return getTopCode(s -> codeOperations.definedData().getAt(s, addr)); + } } @Override public Data getDefinedDataContaining(Address addr) { - return codeOperations.definedData().getContaining(program.snap, addr); + try (LockHold hold = program.trace.lockRead()) { + return getTopCode(s -> codeOperations.definedData().getContaining(s, addr)); + } } @Override public Data getDefinedDataAfter(Address addr) { - return codeOperations.definedData().getAfter(program.snap, addr); + addr = addr.next(); + try (LockHold hold = program.trace.lockRead()) { + return addr == null ? null : next(getDefinedDataIterator(addr, true)); + } } @Override public Data getDefinedDataBefore(Address addr) { - return codeOperations.definedData().getBefore(program.snap, addr); + addr = addr.previous(); + try (LockHold hold = program.trace.lockRead()) { + return addr == null ? null : next(getDefinedDataIterator(addr, false)); + } } @Override public DataIterator getDefinedData(boolean forward) { - return new WrappingDataIterator( - codeOperations.definedData().get(program.snap, forward).iterator()); + return new WrappingDataIterator(getDefinedDataIterator(forward)); } @Override public DataIterator getDefinedData(Address start, boolean forward) { - return new WrappingDataIterator( - codeOperations.definedData().get(program.snap, start, forward).iterator()); + return new WrappingDataIterator(getDefinedDataIterator(start, forward)); } @Override public DataIterator getDefinedData(AddressSetView addressSet, boolean forward) { - return new WrappingDataIterator( - codeOperations.definedData().get(program.snap, addressSet, forward).iterator()); + return new WrappingDataIterator(getDefinedDataIterator(addressSet, forward)); } @Override public Data getUndefinedDataAt(Address addr) { - return codeOperations.undefinedData().getAt(program.snap, addr); + try (LockHold hold = program.trace.lockRead()) { + return reqUndef(getTopCode(s -> codeOperations.definedUnits().getContaining(s, addr)), + addr); + } } @Override public Data getUndefinedDataAfter(Address addr, TaskMonitor monitor) { - return codeOperations.undefinedData().getAfter(program.snap, addr); + addr = addr.next(); + try (LockHold hold = program.trace.lockRead()) { + return addr == null ? null : next(getUndefinedDataIterator(addr, true)); + } } @Override public Data getUndefinedDataBefore(Address addr, TaskMonitor monitor) { - return codeOperations.undefinedData().getBefore(program.snap, addr); + addr = addr.previous(); + try (LockHold hold = program.trace.lockRead()) { + return addr == null ? null : next(getUndefinedDataIterator(addr, false)); + } } @Override public Data getFirstUndefinedData(AddressSetView addressSet, TaskMonitor monitor) { try (LockHold hold = program.trace.lockRead()) { - for (TraceData u : codeOperations.undefinedData().get(program.snap, addressSet, true)) { - return u; - } - return null; + return next(getUndefinedDataIterator(addressSet, true)); } } /** * {@inheritDoc} * - * @implNote This could technically use a (lazy) view; however, to be consistent with - * expectations established by {@link ProgramDB}, it constructs the actual set, and - * permits cancellation by the monitor. + * @implNote This could maybe use a (lazy) view; however, to be consistent with expectations + * established by {@link ProgramDB}, it constructs the actual set, and permits + * cancellation by the monitor. */ @Override public AddressSet getUndefinedRanges(AddressSetView set, boolean initializedMemoryOnly, TaskMonitor monitor) throws CancelledException { AddressSet result = new AddressSet(); - for (AddressRange range : set) { - for (AddressRange und : codeOperations.undefinedData() - .getAddressSetView(program.snap, range)) { - monitor.checkCanceled(); - result.add(und.intersect(range)); - } + for (AddressRange range : getUndefinedRangeIterator(set, true)) { + result.add(range); + monitor.checkCanceled(); } return result; } @Override public CodeUnit getDefinedCodeUnitAfter(Address addr) { - return codeOperations.definedUnits().getAfter(program.snap, addr); + addr = addr.next(); + try (LockHold hold = program.trace.lockRead()) { + return next(getDefinedUnitIterator(addr, true)); + } } @Override public CodeUnit getDefinedCodeUnitBefore(Address addr) { - return codeOperations.definedUnits().getBefore(program.snap, addr); + addr = addr.previous(); + try (LockHold hold = program.trace.lockRead()) { + return next(getDefinedUnitIterator(addr, false)); + } } @Override @@ -561,8 +768,8 @@ public abstract class AbstractDBTraceProgramViewListing implements TraceProgramV @Override public ProgramFragment getFragment(String treeName, Address addr) { - DBTraceMemoryRegion region = - program.trace.getMemoryManager().getRegionContaining(program.snap, addr); + DBTraceMemoryRegion region = program.memory.getTopRegion( + s -> program.trace.getMemoryManager().getRegionContaining(s, addr)); if (region == null) { return null; } @@ -580,8 +787,8 @@ public abstract class AbstractDBTraceProgramViewListing implements TraceProgramV @Override public ProgramFragment getFragment(String treeName, String name) { - DBTraceMemoryRegion region = - program.trace.getMemoryManager().getLiveRegionByPath(program.snap, name); + DBTraceMemoryRegion region = program.memory.getTopRegion( + s -> program.trace.getMemoryManager().getLiveRegionByPath(s, name)); if (region == null) { return null; } @@ -648,8 +855,8 @@ public abstract class AbstractDBTraceProgramViewListing implements TraceProgramV @Override public long getNumInstructions() { - // TODO: See getNumCodeUnits - return Long.MAX_VALUE; + // TODO: See getNumCodeUnits... Why was this Long.MAX_VALUE before? + return codeOperations.instructions().size(); } @Override diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/AbstractDBTraceProgramViewMemory.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/AbstractDBTraceProgramViewMemory.java index 670456394a..76b8d5097f 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/AbstractDBTraceProgramViewMemory.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/AbstractDBTraceProgramViewMemory.java @@ -18,7 +18,6 @@ package ghidra.trace.database.program; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; -import java.nio.ByteOrder; import java.util.*; import com.google.common.cache.RemovalNotification; @@ -31,12 +30,14 @@ import ghidra.trace.database.memory.*; import ghidra.trace.model.Trace; import ghidra.trace.model.program.TraceProgramView; import ghidra.trace.model.program.TraceProgramViewMemory; +import ghidra.trace.util.MemoryAdapter; import ghidra.util.MathUtilities; import ghidra.util.exception.CancelledException; import ghidra.util.exception.NotFoundException; import ghidra.util.task.TaskMonitor; -public abstract class AbstractDBTraceProgramViewMemory implements TraceProgramViewMemory { +public abstract class AbstractDBTraceProgramViewMemory + implements TraceProgramViewMemory, MemoryAdapter { protected final DBTraceProgramView program; protected final DBTraceMemoryManager memoryManager; @@ -95,7 +96,7 @@ public abstract class AbstractDBTraceProgramViewMemory implements TraceProgramVi public AddressSetView getExecuteSet() { AddressSet result = new AddressSet(); for (DBTraceMemoryRegion region : memoryManager.getRegionsInternal()) { - if (!region.isExecute()) { + if (!region.isExecute() || !program.isRegionVisible(region, region.getLifespan())) { continue; } result.add(region.getRange()); @@ -247,6 +248,8 @@ public abstract class AbstractDBTraceProgramViewMemory implements TraceProgramVi if (space == null) { continue; } + // TODO: findBytes must heed fork, or there should exist a variant that does.... + // Lest I have to implement the forked search here. Address found = space.findBytes(snap, range, bufBytes, bufMasks, forward, monitor); if (found != null) { @@ -265,11 +268,6 @@ public abstract class AbstractDBTraceProgramViewMemory implements TraceProgramVi return block.getByte(addr); } - @Override - public int getBytes(Address addr, byte[] dest) throws MemoryAccessException { - return getBytes(addr, dest, 0, dest.length); - } - @Override public int getBytes(Address addr, byte[] dest, int destIndex, int size) throws MemoryAccessException { @@ -283,109 +281,6 @@ public abstract class AbstractDBTraceProgramViewMemory implements TraceProgramVi return block.getBytes(addr, dest, destIndex, size); } - protected ByteBuffer mustRead(Address addr, int length, boolean bigEndian) - throws MemoryAccessException { - ByteBuffer buf = ByteBuffer.allocate(length); - if (getBytes(addr, buf.array()) != length) { - throw new MemoryAccessException(); - } - buf.order(bigEndian ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN); - return buf; - } - - @Override - public short getShort(Address addr) throws MemoryAccessException { - return mustRead(addr, Short.BYTES, true).getShort(0); - } - - @Override - public short getShort(Address addr, boolean bigEndian) throws MemoryAccessException { - return mustRead(addr, Short.BYTES, bigEndian).getShort(0); - } - - @Override - public int getShorts(Address addr, short[] dest) throws MemoryAccessException { - return getShorts(addr, dest, 0, dest.length, true); - } - - @Override - public int getShorts(Address addr, short[] dest, int dIndex, int nElem) - throws MemoryAccessException { - return getShorts(addr, dest, dIndex, nElem, true); - } - - @Override - public int getShorts(Address addr, short[] dest, int dIndex, int nElem, boolean bigEndian) - throws MemoryAccessException { - ByteBuffer buf = ByteBuffer.allocate(Short.BYTES * nElem); - int got = getBytes(addr, buf.array()) / Short.BYTES; - buf.order(bigEndian ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN); - buf.asShortBuffer().get(dest, dIndex, got); - return got; - } - - @Override - public int getInt(Address addr) throws MemoryAccessException { - return mustRead(addr, Integer.BYTES, true).getInt(0); - } - - @Override - public int getInt(Address addr, boolean bigEndian) throws MemoryAccessException { - return mustRead(addr, Integer.BYTES, bigEndian).getInt(0); - } - - @Override - public int getInts(Address addr, int[] dest) throws MemoryAccessException { - return getInts(addr, dest, 0, dest.length, true); - } - - @Override - public int getInts(Address addr, int[] dest, int dIndex, int nElem) - throws MemoryAccessException { - return getInts(addr, dest, dIndex, nElem, true); - } - - @Override - public int getInts(Address addr, int[] dest, int dIndex, int nElem, boolean bigEndian) - throws MemoryAccessException { - ByteBuffer buf = ByteBuffer.allocate(Integer.BYTES * nElem); - int got = getBytes(addr, buf.array()) / Integer.BYTES; - buf.order(bigEndian ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN); - buf.asIntBuffer().get(dest, dIndex, got); - return got; - } - - @Override - public long getLong(Address addr) throws MemoryAccessException { - return mustRead(addr, Long.BYTES, true).getLong(0); - } - - @Override - public long getLong(Address addr, boolean bigEndian) throws MemoryAccessException { - return mustRead(addr, Long.BYTES, bigEndian).getLong(0); - } - - @Override - public int getLongs(Address addr, long[] dest) throws MemoryAccessException { - return getLongs(addr, dest, 0, dest.length, true); - } - - @Override - public int getLongs(Address addr, long[] dest, int dIndex, int nElem) - throws MemoryAccessException { - return getLongs(addr, dest, dIndex, nElem, true); - } - - @Override - public int getLongs(Address addr, long[] dest, int dIndex, int nElem, boolean bigEndian) - throws MemoryAccessException { - ByteBuffer buf = ByteBuffer.allocate(Long.BYTES * nElem); - int got = getBytes(addr, buf.array()) / Long.BYTES; - buf.order(bigEndian ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN); - buf.asLongBuffer().get(dest, dIndex, got); - return got; - } - @Override public void setByte(Address addr, byte value) throws MemoryAccessException { DBTraceMemorySpace space = memoryManager.getMemorySpace(addr.getAddressSpace(), true); @@ -394,11 +289,6 @@ public abstract class AbstractDBTraceProgramViewMemory implements TraceProgramVi } } - @Override - public void setBytes(Address addr, byte[] source) throws MemoryAccessException { - setBytes(addr, source, 0, source.length); - } - @Override public void setBytes(Address addr, byte[] source, int sIndex, int size) throws MemoryAccessException { @@ -408,46 +298,6 @@ public abstract class AbstractDBTraceProgramViewMemory implements TraceProgramVi } } - @Override - public void setShort(Address addr, short value) throws MemoryAccessException { - setShort(addr, value, true); - } - - @Override - public void setShort(Address addr, short value, boolean bigEndian) - throws MemoryAccessException { - ByteBuffer buf = ByteBuffer.allocate(Short.BYTES); - buf.order(bigEndian ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN); - buf.putShort(value); - setBytes(addr, buf.array()); - } - - @Override - public void setInt(Address addr, int value) throws MemoryAccessException { - setInt(addr, value, true); - } - - @Override - public void setInt(Address addr, int value, boolean bigEndian) throws MemoryAccessException { - ByteBuffer buf = ByteBuffer.allocate(Integer.BYTES); - buf.order(bigEndian ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN); - buf.putInt(value); - setBytes(addr, buf.array()); - } - - @Override - public void setLong(Address addr, long value) throws MemoryAccessException { - setLong(addr, value, true); - } - - @Override - public void setLong(Address addr, long value, boolean bigEndian) throws MemoryAccessException { - ByteBuffer buf = ByteBuffer.allocate(Long.BYTES); - buf.order(bigEndian ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN); - buf.putLong(value); - setBytes(addr, buf.array()); - } - @Override public FileBytes createFileBytes(String filename, long offset, long size, InputStream is, TaskMonitor monitor) throws IOException, CancelledException { diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/AbstractDBTraceProgramViewReferenceManager.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/AbstractDBTraceProgramViewReferenceManager.java index 58f33084b4..9e55f25e49 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/AbstractDBTraceProgramViewReferenceManager.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/AbstractDBTraceProgramViewReferenceManager.java @@ -17,8 +17,9 @@ package ghidra.trace.database.program; import static ghidra.lifecycle.Unfinished.TODO; -import java.util.Collection; -import java.util.Collections; +import java.util.*; +import java.util.function.Function; +import java.util.function.Predicate; import javax.help.UnsupportedOperationException; @@ -175,21 +176,42 @@ public abstract class AbstractDBTraceProgramViewReferenceManager implements Refe dbRef.setPrimary(isPrimary); } + protected boolean any(boolean noSpace, Predicate predicate) { + if (refs(false) == null) { + return noSpace; + } + for (long s : program.viewport.getOrderedSnaps()) { + if (predicate.test(s)) { + return true; + } + } + return false; + } + + protected Collection collect( + Function> refFunc) { + if (refs(false) == null) { + return Collections.emptyList(); + } + Set result = new LinkedHashSet<>(); + for (long s : program.viewport.getOrderedSnaps()) { + Collection from = refFunc.apply(s); + if (from != null) { + result.addAll(from); + } + } + return result; + } + @Override public boolean hasFlowReferencesFrom(Address addr) { - if (refs(false) == null) { - return false; - } - return !refs.getFlowRefrencesFrom(program.snap, addr).isEmpty(); + return any(false, s -> !refs.getFlowReferencesFrom(s, addr).isEmpty()); } @Override public Reference[] getFlowReferencesFrom(Address addr) { - Collection from = refs(false) == null - ? Collections.emptyList() - : refs.getFlowRefrencesFrom(program.snap, addr); - // TODO: Requires two traversals. Not terrible for this size.... - return from.toArray(new Reference[from.size()]); + Collection result = collect(s -> refs.getFlowReferencesFrom(s, addr)); + return result.toArray(new Reference[result.size()]); } @Override @@ -199,138 +221,151 @@ public abstract class AbstractDBTraceProgramViewReferenceManager implements Refe @Override public ReferenceIterator getReferencesTo(Address addr) { - Collection to = refs(false) == null - ? Collections.emptyList() - : refs.getReferencesTo(program.snap, addr); - return new ReferenceIteratorAdapter(to.iterator()); + Collection result = collect(s -> refs.getReferencesTo(s, addr)); + return new ReferenceIteratorAdapter(result.iterator()); + } + + protected Comparator getReferenceFromComparator(boolean forward) { + return forward + ? (r1, r2) -> r1.getFromAddress().compareTo(r2.getFromAddress()) + : (r1, r2) -> -r1.getFromAddress().compareTo(r2.getFromAddress()); } @Override public ReferenceIterator getReferenceIterator(Address startAddr) { - Collection from = refs(false) == null - ? Collections.emptyList() - : refs.getReferencesFrom(program.snap, startAddr); - return new ReferenceIteratorAdapter(from.iterator()); + if (refs(false) == null) { + return new ReferenceIteratorAdapter(Collections.emptyIterator()); + } + return new ReferenceIteratorAdapter( + program.viewport.mergedIterator(s -> refs.getReferencesFrom(s, startAddr).iterator(), + getReferenceFromComparator(true))); } @Override public Reference getReference(Address fromAddr, Address toAddr, int opIndex) { - return refs(false) == null - ? null - : refs.getReference(program.snap, fromAddr, toAddr, opIndex); + if (refs(false) == null) { + return null; + } + return program.viewport.getTop(s -> refs.getReference(s, fromAddr, toAddr, opIndex)); } @Override public Reference[] getReferencesFrom(Address addr) { - Collection from = refs(false) == null - ? Collections.emptyList() - : refs.getReferencesFrom(program.snap, addr); - return from.toArray(new Reference[from.size()]); + Collection result = collect(s -> refs.getReferencesFrom(s, addr)); + return result.toArray(new Reference[result.size()]); } @Override public Reference[] getReferencesFrom(Address fromAddr, int opIndex) { - Collection from = refs(false) == null - ? Collections.emptyList() - : refs.getReferencesFrom(program.snap, fromAddr, opIndex); - return from.toArray(new Reference[from.size()]); + Collection result = collect(s -> refs.getReferencesFrom(s, fromAddr, opIndex)); + return result.toArray(new Reference[result.size()]); } @Override public boolean hasReferencesFrom(Address fromAddr, int opIndex) { - return refs(false) == null - ? false - : !refs.getReferencesFrom(program.snap, fromAddr, opIndex).isEmpty(); + return any(false, s -> !refs.getReferencesFrom(s, fromAddr, opIndex).isEmpty()); } @Override public boolean hasReferencesFrom(Address fromAddr) { - return refs(false) == null - ? false - : !refs.getReferencesFrom(program.snap, fromAddr).isEmpty(); + return any(false, s -> !refs.getReferencesFrom(s, fromAddr).isEmpty()); } @Override public Reference getPrimaryReferenceFrom(Address addr, int opIndex) { - return refs(false) == null - ? null - : refs.getPrimaryReferenceFrom(program.snap, addr, opIndex); + if (refs(false) == null) { + return null; + } + return program.viewport.getTop(s -> refs.getPrimaryReferenceFrom(s, addr, opIndex)); } @Override public AddressIterator getReferenceSourceIterator(Address startAddr, boolean forward) { - return refs(false) == null - ? new EmptyAddressIterator() - : refs.getReferenceSources(Range.closed(program.snap, program.snap)) - .getAddresses(startAddr, forward); + if (refs(false) == null) { + return new EmptyAddressIterator(); + } + return program.viewport.unionedAddresses( + s -> refs.getReferenceSources(Range.singleton(s))).getAddresses(startAddr, forward); } @Override public AddressIterator getReferenceSourceIterator(AddressSetView addrSet, boolean forward) { - return refs(false) == null - ? new EmptyAddressIterator() - : new IntersectionAddressSetView( - refs.getReferenceSources(Range.closed(program.snap, program.snap)), addrSet) - .getAddresses(forward); + if (refs(false) == null) { + return new EmptyAddressIterator(); + } + return new IntersectionAddressSetView(addrSet, program.viewport.unionedAddresses( + s -> refs.getReferenceSources(Range.singleton(s)))).getAddresses(forward); } @Override public AddressIterator getReferenceDestinationIterator(Address startAddr, boolean forward) { - return refs(false) == null - ? new EmptyAddressIterator() - : refs.getReferenceDestinations(Range.closed(program.snap, program.snap)) - .getAddresses(startAddr, forward); + if (refs(false) == null) { + return new EmptyAddressIterator(); + } + return program.viewport.unionedAddresses( + s -> refs.getReferenceDestinations(Range.singleton(s))) + .getAddresses(startAddr, forward); } @Override public AddressIterator getReferenceDestinationIterator(AddressSetView addrSet, boolean forward) { - return refs(false) == null - ? new EmptyAddressIterator() - : new IntersectionAddressSetView( - refs.getReferenceDestinations(Range.closed(program.snap, program.snap)), - addrSet).getAddresses(forward); + if (refs(false) == null) { + return new EmptyAddressIterator(); + } + return new IntersectionAddressSetView(addrSet, program.viewport.unionedAddresses( + s -> refs.getReferenceDestinations(Range.singleton(s)))).getAddresses(forward); } @Override public int getReferenceCountTo(Address toAddr) { - return refs(false) == null - ? 0 - : refs.getReferenceCountTo(program.snap, toAddr); + if (refs(false) == null) { + return 0; + } + if (!program.viewport.isForked()) { + return refs.getReferenceCountTo(program.snap, toAddr); + } + return collect(s -> refs.getReferencesTo(s, toAddr)).size(); } @Override public int getReferenceCountFrom(Address fromAddr) { - return refs(false) == null - ? 0 - : refs.getReferenceCountFrom(program.snap, fromAddr); + if (refs(false) == null) { + return 0; + } + if (!program.viewport.isForked()) { + return refs.getReferenceCountFrom(program.snap, fromAddr); + } + return collect(s -> refs.getReferencesFrom(s, fromAddr)).size(); } @Override public int getReferenceDestinationCount() { // TODO: It is unclear if the interface definition means to include unique addresses // or also unique references - return refs(false) == null - ? 0 - : (int) refs.getReferenceDestinations(Range.closed(program.snap, program.snap)) - .getNumAddresses(); + if (refs(false) == null) { + return 0; + } + return (int) program.viewport + .unionedAddresses(s -> refs.getReferenceDestinations(Range.singleton(s))) + .getNumAddresses(); } @Override public int getReferenceSourceCount() { // TODO: It is unclear if the interface definition means to include unique addresses // or also unique references - return refs(false) == null - ? 0 - : (int) refs.getReferenceSources(Range.closed(program.snap, program.snap)) - .getNumAddresses(); + if (refs(false) == null) { + return 0; + } + return (int) program.viewport + .unionedAddresses(s -> refs.getReferenceSources(Range.singleton(s))) + .getNumAddresses(); } @Override public boolean hasReferencesTo(Address toAddr) { - return refs(false) == null - ? false - : !refs.getReferencesTo(program.snap, toAddr).isEmpty(); + return any(false, s -> !refs.getReferencesTo(s, toAddr).isEmpty()); } @Override @@ -361,9 +396,11 @@ public abstract class AbstractDBTraceProgramViewReferenceManager implements Refe /** * Get the reference level for a given reference type * + *

* TODO: Why is this not a property of {@link RefType}, or a static method of * {@link SymbolUtilities}? * + *

* Note that this was copy-pasted from {@code BigRefListV0}, and there's an exact copy also in * {@code RefListV0}. * @@ -389,11 +426,13 @@ public abstract class AbstractDBTraceProgramViewReferenceManager implements Refe /** * {@inheritDoc} * + *

* To clarify, "reference level" is a sort of priority assigned to each reference type. See, * e.g., {@link SymbolUtilities#SUB_LEVEL}. Each is a byte constant, and greater values imply * higher priority. This method returns the highest priority of any reference to the given * address. * + *

* TODO: Track this in the database? */ @Override @@ -402,8 +441,10 @@ public abstract class AbstractDBTraceProgramViewReferenceManager implements Refe return SymbolUtilities.UNK_LEVEL; } byte highest = SymbolUtilities.UNK_LEVEL; - for (TraceReference ref : refs.getReferencesTo(program.snap, toAddr)) { - highest = (byte) Math.max(highest, getRefLevel(ref.getReferenceType())); + for (long s : program.viewport.getOrderedSnaps()) { + for (TraceReference ref : refs.getReferencesTo(s, toAddr)) { + highest = (byte) Math.max(highest, getRefLevel(ref.getReferenceType())); + } } return highest; } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramView.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramView.java index 27f84a3bf4..596da292e8 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramView.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramView.java @@ -17,8 +17,10 @@ package ghidra.trace.database.program; import java.io.File; import java.io.IOException; +import java.nio.ByteBuffer; import java.util.*; import java.util.Map.Entry; +import java.util.function.BiFunction; import org.apache.commons.lang3.tuple.Pair; @@ -34,6 +36,7 @@ import ghidra.program.model.address.*; import ghidra.program.model.data.*; import ghidra.program.model.lang.*; import ghidra.program.model.listing.*; +import ghidra.program.model.mem.MemoryAccessException; import ghidra.program.model.pcode.Varnode; import ghidra.program.model.reloc.RelocationTable; import ghidra.program.model.symbol.*; @@ -42,9 +45,9 @@ import ghidra.program.model.util.PropertyMapManager; import ghidra.program.util.ChangeManager; import ghidra.program.util.ProgramChangeRecord; import ghidra.trace.database.DBTrace; -import ghidra.trace.database.listing.DBTraceCodeRegisterSpace; -import ghidra.trace.database.memory.DBTraceMemoryRegion; -import ghidra.trace.database.memory.DBTraceMemoryRegisterSpace; +import ghidra.trace.database.listing.*; +import ghidra.trace.database.memory.*; +import ghidra.trace.database.symbol.DBTraceFunctionSymbolView; import ghidra.trace.model.Trace.*; import ghidra.trace.model.TraceAddressSnapRange; import ghidra.trace.model.TraceDomainObjectListener; @@ -53,26 +56,29 @@ import ghidra.trace.model.bookmark.TraceBookmarkType; import ghidra.trace.model.data.TraceBasedDataTypeManager; import ghidra.trace.model.listing.*; import ghidra.trace.model.memory.TraceMemoryRegion; +import ghidra.trace.model.memory.TraceMemoryState; import ghidra.trace.model.program.TraceProgramView; import ghidra.trace.model.program.TraceProgramViewMemory; import ghidra.trace.model.symbol.*; import ghidra.trace.model.thread.TraceThread; -import ghidra.trace.util.TraceAddressSpace; -import ghidra.util.PairingIteratorMerger; -import ghidra.util.UniversalID; +import ghidra.trace.util.*; +import ghidra.trace.util.TraceTimeViewport.*; +import ghidra.util.*; import ghidra.util.datastruct.WeakValueHashMap; import ghidra.util.exception.CancelledException; import ghidra.util.exception.DuplicateNameException; import ghidra.util.task.TaskMonitor; /** - * TODO + * A wrapper on a trace, which given a snap, implements the {@link Program} interface * + *

* NOTE: Calling {@link CodeUnit#getProgram()} from units contained in this view may not necessarily * return this same view. If the code unit comes from a less-recent snap than the snap associated * with this view, the view for that snap is returned instead. * - * TODO: Unit tests for all of this + *

+ * TODO: Unit tests for all of this. */ public class DBTraceProgramView implements TraceProgramView { public static final int TIME_INTERVAL = 100; @@ -160,58 +166,6 @@ public class DBTraceProgramView implements TraceProgramView { listenFor(TraceSymbolChangeType.DELETED, this::symbolDeleted); } - protected DomainObjectEventQueues getEventQueues(TraceAddressSpace space) { - // TODO: Should there be views on other frames? - // IIRC, this was an abandoned experiment for "register listings" - TraceThread thread = space == null ? null : space.getThread(); - if (thread == null) { - return eventQueues; - } - DBTraceProgramViewRegisters viewRegisters; - synchronized (regViewsByThread) { - viewRegisters = regViewsByThread.get(thread); - } - return viewRegisters == null ? null : viewRegisters.eventQueues; - } - - protected DomainObjectEventQueues isVisible(TraceAddressSpace space, - TraceAddressSnapRange range) { - if (!range.getLifespan().contains(snap)) { - return null; - } - return getEventQueues(space); - } - - protected DomainObjectEventQueues isVisible(TraceAddressSpace space, TraceCodeUnit cu) { - if (!cu.getLifespan().contains(snap)) { - return null; - } - return getEventQueues(space); - } - - protected DomainObjectEventQueues isVisible(TraceAddressSpace space, TraceSymbol symbol) { - DomainObjectEventQueues queues = getEventQueues(space); - if (queues == null) { - return null; - } - if (symbol instanceof TraceVariableSymbol) { - TraceVariableSymbol var = (TraceVariableSymbol) symbol; - TraceFunctionSymbol func = var.getFunction(); - if (func == null) { - return queues; - } - return func.getLifespan().contains(snap) ? queues : null; - } - if (!(symbol instanceof TraceSymbolWithLifespan)) { - return queues; - } - TraceSymbolWithLifespan symWl = (TraceSymbolWithLifespan) symbol; - if (!symWl.getLifespan().contains(snap)) { - return null; - } - return queues; - } - private void eventPassthrough(DomainObjectChangeRecord rec) { fireEventAllViews(rec); } @@ -227,13 +181,10 @@ public class DBTraceProgramView implements TraceProgramView { } private void bookmarkAdded(TraceAddressSpace space, TraceBookmark bm) { - DomainObjectEventQueues queues = getEventQueues(space); + DomainObjectEventQueues queues = isBookmarkVisible(space, bm); if (queues == null) { return; } - if (!bm.getLifespan().contains(snap)) { - return; - } fireBookmarkAdded(queues, bm); } @@ -243,13 +194,10 @@ public class DBTraceProgramView implements TraceProgramView { } private void bookmarkChanged(TraceAddressSpace space, TraceBookmark bm) { - DomainObjectEventQueues queues = getEventQueues(space); + DomainObjectEventQueues queues = isBookmarkVisible(space, bm); if (queues == null) { return; } - if (!bm.getLifespan().contains(snap)) { - return; - } fireBookmarkChanged(queues, bm); } @@ -259,14 +207,13 @@ public class DBTraceProgramView implements TraceProgramView { } private void bookmarkLifespanChanged(TraceAddressSpace space, TraceBookmark bm, - Range oldSpan, - Range newSpan) { + Range oldSpan, Range newSpan) { DomainObjectEventQueues queues = getEventQueues(space); if (queues == null) { return; } - boolean inOld = oldSpan.contains(snap); - boolean inNew = newSpan.contains(snap); + boolean inOld = isBookmarkVisible(bm, oldSpan); + boolean inNew = isBookmarkVisible(bm, newSpan); if (inOld && !inNew) { fireBookmarkRemoved(queues, bm); } @@ -276,13 +223,10 @@ public class DBTraceProgramView implements TraceProgramView { } private void bookmarkDeleted(TraceAddressSpace space, TraceBookmark bm) { - DomainObjectEventQueues queues = getEventQueues(space); + DomainObjectEventQueues queues = isBookmarkVisible(space, bm); if (queues == null) { return; } - if (!bm.getLifespan().contains(snap)) { - return; - } fireBookmarkRemoved(queues, bm); } @@ -317,7 +261,9 @@ public class DBTraceProgramView implements TraceProgramView { private void codeAdded(TraceAddressSpace space, TraceAddressSnapRange range, TraceCodeUnit oldIsNull, TraceCodeUnit added) { // NOTE: Added code may be coalesced range. -added- is just first unit. - DomainObjectEventQueues queues = isVisible(space, range); + // TODO: The range may contain many units, so this could be broken down + DomainObjectEventQueues queues = + isCodeVisible(space, range) ? getEventQueues(space) : null; if (queues == null) { return; } @@ -335,8 +281,8 @@ public class DBTraceProgramView implements TraceProgramView { if (queues == null) { return; } - boolean inOld = oldSpan.contains(snap); - boolean inNew = newSpan.contains(snap); + boolean inOld = isCodeVisible(unit, oldSpan); + boolean inNew = isCodeVisible(unit, newSpan); if (inOld && !inNew) { fireCodeRemoved(queues, unit.getMinAddress(), unit.getMaxAddress(), unit); } @@ -348,7 +294,7 @@ public class DBTraceProgramView implements TraceProgramView { private void codeRemoved(TraceAddressSpace space, TraceAddressSnapRange range, TraceCodeUnit removed, TraceCodeUnit newIsNull) { // NOTE: Removed code may be coalesced range. -removed- is just first unit. - DomainObjectEventQueues queues = isVisible(space, range); + DomainObjectEventQueues queues = isCodeVisible(space, removed); if (queues == null) { return; } @@ -373,6 +319,7 @@ public class DBTraceProgramView implements TraceProgramView { private void codeDataTypeReplaced(TraceAddressSpace space, TraceAddressSnapRange range, Long oldDataTypeID, Long newDataTypeID) { + // TODO??: "code" visibility check may not be necessary or advantageous DomainObjectEventQueues queues = isVisible(space, range); if (queues == null) { return; @@ -383,7 +330,7 @@ public class DBTraceProgramView implements TraceProgramView { private void compositeDataAdded(TraceAddressSpace space, TraceAddressSnapRange range, TraceData oldIsNull, TraceData added) { - DomainObjectEventQueues queues = isVisible(space, range); + DomainObjectEventQueues queues = isCodeVisible(space, added); if (queues == null) { return; } @@ -397,8 +344,8 @@ public class DBTraceProgramView implements TraceProgramView { if (queues == null) { return; } - boolean inOld = oldSpan.contains(snap); - boolean inNew = newSpan.contains(snap); + boolean inOld = isCodeVisible(data, oldSpan); + boolean inNew = isCodeVisible(data, newSpan); if (inOld && !inNew) { queues.fireEvent(new ProgramChangeRecord(ChangeManager.DOCR_COMPOSITE_REMOVED, data.getMinAddress(), data.getMaxAddress(), null, data, null)); @@ -411,7 +358,7 @@ public class DBTraceProgramView implements TraceProgramView { private void compositeDataRemoved(TraceAddressSpace space, TraceAddressSnapRange range, TraceData removed, TraceData newIsNull) { - DomainObjectEventQueues queues = isVisible(space, range); + DomainObjectEventQueues queues = isCodeVisible(space, removed); if (queues == null) { return; } @@ -473,7 +420,7 @@ public class DBTraceProgramView implements TraceProgramView { private void functionChangedGeneric(TraceAddressSpace space, TraceFunctionSymbol function, int type, int subType) { - DomainObjectEventQueues queues = isVisible(space, function); + DomainObjectEventQueues queues = isFunctionVisible(space, function); if (queues == null) { return; } @@ -557,7 +504,7 @@ public class DBTraceProgramView implements TraceProgramView { private void instructionFlowOverrideChanged(TraceAddressSpace space, TraceInstruction instruction, FlowOverride oldOverride, FlowOverride newOverride) { - DomainObjectEventQueues queues = isVisible(space, instruction); + DomainObjectEventQueues queues = isCodeVisible(space, instruction); if (queues == null) { return; } @@ -567,7 +514,7 @@ public class DBTraceProgramView implements TraceProgramView { private void instructionFallThroughChanged(TraceAddressSpace space, TraceInstruction instruction, boolean oldFallThrough, boolean newFallThrough) { - DomainObjectEventQueues queues = isVisible(space, instruction); + DomainObjectEventQueues queues = isCodeVisible(space, instruction); if (queues == null) { return; } @@ -577,7 +524,7 @@ public class DBTraceProgramView implements TraceProgramView { private void memoryBytesChanged(TraceAddressSpace space, TraceAddressSnapRange range, byte[] oldIsNull, byte[] bytes) { - DomainObjectEventQueues queues = isVisible(space, range); + DomainObjectEventQueues queues = isBytesVisible(space, range); if (queues == null) { return; } @@ -591,7 +538,7 @@ public class DBTraceProgramView implements TraceProgramView { } private void memoryRegionAdded(TraceAddressSpace space, TraceMemoryRegion region) { - if (!region.getLifespan().contains(snap)) { + if (!isRegionVisible(region)) { return; } // NOTE: Register view regions are fixed @@ -602,7 +549,7 @@ public class DBTraceProgramView implements TraceProgramView { } private void memoryRegionChanged(TraceAddressSpace space, TraceMemoryRegion region) { - if (!region.getLifespan().contains(snap)) { + if (!isRegionVisible(region)) { return; } eventQueues.fireEvent(new ProgramChangeRecord(ChangeManager.DOCR_MEMORY_BLOCK_CHANGED, @@ -614,8 +561,8 @@ public class DBTraceProgramView implements TraceProgramView { private void memoryRegionLifespanChanged(TraceAddressSpace space, TraceMemoryRegion region, Range oldSpan, Range newSpan) { - boolean inOld = oldSpan.contains(snap); - boolean inNew = newSpan.contains(snap); + boolean inOld = isRegionVisible(region, oldSpan); + boolean inNew = isRegionVisible(region, newSpan); if (inOld && !inNew) { eventQueues.fireEvent( new ProgramChangeRecord(ChangeManager.DOCR_MEMORY_BLOCK_REMOVED, @@ -637,7 +584,7 @@ public class DBTraceProgramView implements TraceProgramView { // HACK listing.fragmentsByRegion.remove(region); // END HACK - if (!region.getLifespan().contains(snap)) { + if (!isRegionVisible(region)) { return; } eventQueues.fireEvent(new ProgramChangeRecord(ChangeManager.DOCR_MEMORY_BLOCK_REMOVED, @@ -678,7 +625,7 @@ public class DBTraceProgramView implements TraceProgramView { } private void symbolAdded(TraceAddressSpace space, TraceSymbol symbol) { - DomainObjectEventQueues queues = isVisible(space, symbol); + DomainObjectEventQueues queues = isSymbolVisible(space, symbol); if (queues == null) { return; } @@ -697,7 +644,7 @@ public class DBTraceProgramView implements TraceProgramView { } private void symbolSourceChanged(TraceAddressSpace space, TraceSymbol symbol) { - DomainObjectEventQueues queues = isVisible(space, symbol); + DomainObjectEventQueues queues = isSymbolVisible(space, symbol); if (queues == null) { return; } @@ -709,11 +656,11 @@ public class DBTraceProgramView implements TraceProgramView { private void symbolSetAsPrimary(TraceAddressSpace space, TraceSymbol symbol, TraceSymbol oldPrimary, TraceSymbol newPrimary) { // NOTE symbol == newPrimary - DomainObjectEventQueues newQueues = isVisible(space, symbol); + DomainObjectEventQueues newQueues = isSymbolVisible(space, symbol); if (newQueues == null) { return; } - DomainObjectEventQueues oldQueues = isVisible(space, oldPrimary); + DomainObjectEventQueues oldQueues = isSymbolVisible(space, oldPrimary); if (oldPrimary != null && oldQueues == null) { oldPrimary = null; } @@ -725,7 +672,7 @@ public class DBTraceProgramView implements TraceProgramView { private void symbolRenamed(TraceAddressSpace space, TraceSymbol symbol, String oldName, String newName) { - DomainObjectEventQueues queues = isVisible(space, symbol); + DomainObjectEventQueues queues = isSymbolVisible(space, symbol); if (queues == null) { return; } @@ -736,7 +683,7 @@ public class DBTraceProgramView implements TraceProgramView { private void symbolParentChanged(TraceAddressSpace space, TraceSymbol symbol, TraceNamespaceSymbol oldParent, TraceNamespaceSymbol newParent) { - DomainObjectEventQueues queues = isVisible(space, symbol); + DomainObjectEventQueues queues = isSymbolVisible(space, symbol); if (queues == null) { return; } @@ -747,7 +694,7 @@ public class DBTraceProgramView implements TraceProgramView { private void symbolAssociationAdded(TraceAddressSpace space, TraceSymbol symbol, TraceReference oldRefIsNull, TraceReference newRef) { - DomainObjectEventQueues queues = isVisible(space, symbol); + DomainObjectEventQueues queues = isSymbolVisible(space, symbol); if (queues == null) { return; } @@ -759,7 +706,7 @@ public class DBTraceProgramView implements TraceProgramView { private void symbolAssociationRemoved(TraceAddressSpace space, TraceSymbol symbol, TraceReference oldRef, TraceReference newRefIsNull) { - DomainObjectEventQueues queues = isVisible(space, symbol); + DomainObjectEventQueues queues = isSymbolVisible(space, symbol); if (queues == null) { return; } @@ -770,7 +717,7 @@ public class DBTraceProgramView implements TraceProgramView { private void symbolAddressChanged(TraceAddressSpace space, TraceSymbol symbol, Address oldAddress, Address newAddress) { - DomainObjectEventQueues queues = isVisible(space, symbol); + DomainObjectEventQueues queues = isSymbolVisible(space, symbol); if (queues == null) { return; } @@ -779,14 +726,14 @@ public class DBTraceProgramView implements TraceProgramView { checkVariableFunctionChanged(space, symbol); } - private void symbolLifespanChanged(TraceAddressSpace space, TraceSymbol symbol, + private void symbolLifespanChanged(TraceAddressSpace space, TraceSymbolWithLifespan symbol, Range oldSpan, Range newSpan) { DomainObjectEventQueues queues = getEventQueues(space); if (queues == null) { return; } - boolean inOld = oldSpan.contains(snap); - boolean inNew = newSpan.contains(snap); + boolean inOld = isSymbolWithLifespanVisible(symbol, oldSpan); + boolean inNew = isSymbolWithLifespanVisible(symbol, newSpan); if (inOld && !inNew) { fireSymbolRemoved(queues, symbol); if (symbol instanceof TraceFunctionSymbol) { @@ -807,7 +754,7 @@ public class DBTraceProgramView implements TraceProgramView { } private void symbolDeleted(TraceAddressSpace space, TraceSymbol symbol) { - DomainObjectEventQueues queues = isVisible(space, symbol); + DomainObjectEventQueues queues = isSymbolVisible(space, symbol); if (queues == null) { return; } @@ -830,6 +777,7 @@ public class DBTraceProgramView implements TraceProgramView { protected static class OverlappingAddressRangeKeyIteratorMerger extends PairingIteratorMerger, Entry, Entry> { + protected static Iterable, Entry>> iter( Iterable> left, Iterable> right) { return new Iterable<>() { @@ -864,6 +812,7 @@ public class DBTraceProgramView implements TraceProgramView { protected final DomainObjectEventQueues eventQueues; protected EventTranslator eventTranslator; + protected final AddressSet allAddresses = new AddressSet(); protected final DBTraceProgramViewBookmarkManager bookmarkManager; protected final DBTraceProgramViewEquateTable equateTable; @@ -881,17 +830,28 @@ public class DBTraceProgramView implements TraceProgramView { protected final Map regViewsByThread; protected long snap; + protected final DefaultTraceTimeViewport viewport; + protected final Runnable viewportChangeListener = this::viewportChanged; // This is a strange thing Long versionTag = 0L; public DBTraceProgramView(DBTrace trace, long snap, CompilerSpec compilerSpec) { + for (AddressSpace space : trace.getBaseAddressFactory().getPhysicalSpaces()) { + if (space.getType() == AddressSpace.TYPE_OTHER) { + continue; + } + allAddresses.add(space.getMinAddress(), space.getMaxAddress()); + } this.trace = trace; this.snap = snap; this.languageID = compilerSpec.getLanguage().getLanguageID(); this.language = compilerSpec.getLanguage(); this.compilerSpec = compilerSpec; + this.viewport = new DefaultTraceTimeViewport(trace); + this.viewport.setSnap(snap); + this.eventQueues = new DomainObjectEventQueues(this, TIME_INTERVAL, BUF_SIZE, trace.getLock()); @@ -911,6 +871,10 @@ public class DBTraceProgramView implements TraceProgramView { } + protected void viewportChanged() { + eventQueues.fireEvent(new DomainObjectChangeRecord(DomainObject.DO_OBJECT_RESTORED)); + } + protected void fireEventAllViews(DomainObjectChangeRecord ev) { // TODO: Do I need to make copies? eventQueues.fireEvent(ev); @@ -934,6 +898,11 @@ public class DBTraceProgramView implements TraceProgramView { return snap; } + @Override + public TraceTimeViewport getViewport() { + return viewport; + } + @Override public Long getMaxSnap() { return trace.getTimeManager().getMaxSnap(); @@ -1527,21 +1496,21 @@ public class DBTraceProgramView implements TraceProgramView { } public void updateMemoryAddBlock(DBTraceMemoryRegion region) { - if (!region.getLifespan().contains(snap)) { + if (!isRegionVisible(region)) { return; } memory.updateAddBlock(region); } public void updateMemoryChangeBlockName(DBTraceMemoryRegion region) { - if (!region.getLifespan().contains(snap)) { + if (!isRegionVisible(region)) { return; } memory.updateChangeBlockName(region); } public void updateMemoryChangeBlockFlags(DBTraceMemoryRegion region) { - if (!region.getLifespan().contains(snap)) { + if (!isRegionVisible(region)) { return; } memory.updateChangeBlockFlags(region); @@ -1549,7 +1518,7 @@ public class DBTraceProgramView implements TraceProgramView { public void updateMemoryChangeBlockRange(DBTraceMemoryRegion region, AddressRange oldRange, AddressRange newRange) { - if (!region.getLifespan().contains(snap)) { + if (!isRegionVisible(region)) { return; } memory.updateChangeBlockRange(region, oldRange, newRange); @@ -1557,8 +1526,8 @@ public class DBTraceProgramView implements TraceProgramView { public void updateMemoryChangeBlockLifespan(DBTraceMemoryRegion region, Range oldLifespan, Range newLifespan) { - boolean inOld = oldLifespan.contains(snap); - boolean inNew = newLifespan.contains(snap); + boolean inOld = isRegionVisible(region, oldLifespan); + boolean inNew = isRegionVisible(region, newLifespan); if (inOld && !inNew) { memory.updateDeleteBlock(region); } @@ -1568,7 +1537,7 @@ public class DBTraceProgramView implements TraceProgramView { } public void updateMemoryDeleteBlock(DBTraceMemoryRegion region) { - if (!region.getLifespan().contains(snap)) { + if (!isRegionVisible(region)) { return; } memory.updateAddBlock(region); @@ -1577,4 +1546,234 @@ public class DBTraceProgramView implements TraceProgramView { public void updateMemoryRefreshBlocks() { memory.updateRefreshBlocks(); } + + protected DomainObjectEventQueues getEventQueues(TraceAddressSpace space) { + // TODO: Should there be views on other frames? + // IIRC, this was an abandoned experiment for "register listings" + TraceThread thread = space == null ? null : space.getThread(); + if (thread == null) { + return eventQueues; + } + DBTraceProgramViewRegisters viewRegisters; + synchronized (regViewsByThread) { + viewRegisters = regViewsByThread.get(thread); + } + return viewRegisters == null ? null : viewRegisters.eventQueues; + } + + protected DomainObjectEventQueues isVisible(TraceAddressSpace space, + TraceAddressSnapRange range) { + return viewport.containsAnyUpper(range.getLifespan()) ? getEventQueues(space) : null; + } + + protected boolean isBookmarkVisible(TraceBookmark bm, Range lifespan) { + return viewport.containsAnyUpper(lifespan); + } + + protected DomainObjectEventQueues isBookmarkVisible(TraceAddressSpace space, TraceBookmark bm) { + return isBookmarkVisible(bm, bm.getLifespan()) ? getEventQueues(space) : null; + } + + protected boolean bytesDifferForSet(byte[] b1, byte[] b2, AddressSetView set) { + Address min = set.getMinAddress(); + for (AddressRange rng : set) { + int beg = (int) rng.getMinAddress().subtract(min); + int end = beg + (int) rng.getLength(); + if (!Arrays.equals(b1, beg, end, b2, beg, end)) { + return true; + } + } + return false; + } + + protected Occlusion getCodeOcclusion(TraceAddressSpace space) { + return new RangeQueryOcclusion<>() { + final DBTraceCodeSpace codeSpace = trace.getCodeManager().get(space, false); + final DBTraceMemorySpace memSpace = trace.getMemoryManager().get(space, false); + final DBTraceDefinedUnitsView definedUnits = + codeSpace == null ? null : codeSpace.definedUnits(); + + public boolean occluded(TraceCodeUnit cu, AddressRange range, Range span) { + if (cu == null) { + return RangeQueryOcclusion.super.occluded(cu, range, span); + } + AddressSetView known = + memSpace.getAddressesWithState(span, s -> s == TraceMemoryState.KNOWN); + if (!known.intersects(range.getMinAddress(), range.getMaxAddress())) { + return RangeQueryOcclusion.super.occluded(cu, range, span); + } + byte[] memBytes = new byte[cu.getLength()]; + memSpace.getBytes(span.upperEndpoint(), cu.getMinAddress(), + ByteBuffer.wrap(memBytes)); + byte[] cuBytes; + try { + cuBytes = cu.getBytes(); + } + catch (MemoryAccessException e) { + throw new AssertionError(e); + } + AddressSetView intersectKnown = + new IntersectionAddressSetView(new AddressSet(range), known); + if (bytesDifferForSet(memBytes, cuBytes, intersectKnown)) { + return true; + } + return RangeQueryOcclusion.super.occluded(cu, range, span); + } + + @Override + public Iterable query(AddressRange range, Range span) { + return definedUnits == null + ? Collections.emptyList() + : definedUnits.get(span.upperEndpoint(), range, true); + } + + @Override + public AddressRange range(TraceCodeUnit cu) { + return cu.getRange(); + } + }; + } + + protected T getTopCode(Address address, + BiFunction codeFunc) { + DBTraceCodeSpace codeSpace = + trace.getCodeManager().getCodeSpace(address.getAddressSpace(), false); + if (codeSpace == null) { + return null; + } + return viewport.getTop(s -> { + T t = codeFunc.apply(codeSpace, s); + if (t != null && isCodeVisible(t, t.getLifespan())) { + return t; + } + return null; + }); + } + + protected boolean isCodeVisible(TraceCodeUnit cu, Range lifespan) { + return viewport.isCompletelyVisible(cu.getRange(), lifespan, cu, + getCodeOcclusion(cu.getTraceSpace())); + } + + protected boolean isCodeVisible(TraceAddressSpace space, TraceAddressSnapRange range) { + return viewport.isCompletelyVisible(range.getRange(), range.getLifespan(), null, + getCodeOcclusion(space)); + } + + protected DomainObjectEventQueues isCodeVisible(TraceAddressSpace space, TraceCodeUnit cu) { + if (!isCodeVisible(cu, cu.getLifespan())) { + return null; + } + return getEventQueues(space); + } + + protected Occlusion getFunctionOcclusion(TraceFunctionSymbol func) { + return new QueryOcclusion<>() { + DBTraceFunctionSymbolView functions = trace.getSymbolManager().functions(); + AddressSetView body = func.getBody(); + + @Override + public Iterable query(AddressRange range, + Range span) { + // NB. No functions in register space! + return functions.getIntersecting(Range.singleton(span.upperEndpoint()), null, range, + false); + } + + public boolean itemOccludes(AddressRange range, TraceFunctionSymbol f) { + return body.intersects(f.getBody()); + } + + @Override + public void removeItem(AddressSet remains, TraceFunctionSymbol t) { + remains.delete(t.getBody()); + } + }; + } + + protected boolean isFunctionVisible(TraceFunctionSymbol function, Range lifespan) { + AddressSetView body = function.getBody(); + AddressRange bodySpan = + new AddressRangeImpl(body.getMinAddress(), body.getMaxAddress()); + return viewport.isCompletelyVisible(bodySpan, function.getLifespan(), function, + getFunctionOcclusion(function)); + } + + protected DomainObjectEventQueues isFunctionVisible(TraceAddressSpace space, + TraceFunctionSymbol function) { + DomainObjectEventQueues queues = getEventQueues(space); + if (queues == null) { + return null; + } + return isFunctionVisible(function, function.getLifespan()) ? queues : null; + } + + protected boolean isSymbolWithLifespanVisible(TraceSymbolWithLifespan symbol, + Range lifespan) { + if (symbol instanceof TraceFunctionSymbol) { + TraceFunctionSymbol func = (TraceFunctionSymbol) symbol; + return isFunctionVisible(func, lifespan); + } + if (!viewport.containsAnyUpper(lifespan)) { + return false; + } + return true; + } + + protected DomainObjectEventQueues isSymbolVisible(TraceAddressSpace space, + TraceSymbol symbol) { + // NB. Most symbols do not occlude each other + DomainObjectEventQueues queues = getEventQueues(space); + if (queues == null) { + return null; + } + if (symbol instanceof TraceVariableSymbol) { + TraceVariableSymbol var = (TraceVariableSymbol) symbol; + TraceFunctionSymbol func = var.getFunction(); + if (func == null) { + return queues; + } + return isFunctionVisible(space, func); + } + if (!(symbol instanceof TraceSymbolWithLifespan)) { + return queues; + } + TraceSymbolWithLifespan symWl = (TraceSymbolWithLifespan) symbol; + return isSymbolWithLifespanVisible(symWl, symWl.getLifespan()) ? queues : null; + } + + protected DomainObjectEventQueues isBytesVisible(TraceAddressSpace space, + TraceAddressSnapRange range) { + // NB. This need not be precise.... + DomainObjectEventQueues queues = getEventQueues(space); + if (queues == null) { + return null; + } + if (!viewport.containsAnyUpper(range.getLifespan())) { + return null; + } + return queues; + } + + protected Occlusion regionOcclusion = new RangeQueryOcclusion<>() { + @Override + public Iterable query(AddressRange range, Range span) { + return trace.getMemoryManager() + .getRegionsIntersecting(Range.singleton(span.upperEndpoint()), range); + } + + @Override + public AddressRange range(TraceMemoryRegion r) { + return r.getRange(); + } + }; + + protected boolean isRegionVisible(TraceMemoryRegion reg) { + return isRegionVisible(reg, reg.getLifespan()); + } + + protected boolean isRegionVisible(TraceMemoryRegion reg, Range lifespan) { + return viewport.isCompletelyVisible(reg.getRange(), lifespan, reg, + regionOcclusion); + } } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewBookmarkManager.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewBookmarkManager.java index 094f573033..8006e3bc19 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewBookmarkManager.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewBookmarkManager.java @@ -23,6 +23,7 @@ import javax.swing.ImageIcon; import org.apache.commons.collections4.IteratorUtils; +import com.google.common.collect.Iterators; import com.google.common.collect.Range; import generic.NestedIterator; @@ -96,14 +97,16 @@ public class DBTraceProgramViewBookmarkManager implements TraceProgramViewBookma if (space == null) { return null; } - for (TraceBookmark bm : space.getBookmarksAt(program.snap, addr)) { - if (!type.equals(bm.getTypeString())) { - continue; + for (long s : program.viewport.getOrderedSnaps()) { + for (TraceBookmark bm : space.getBookmarksAt(s, addr)) { + if (!type.equals(bm.getTypeString())) { + continue; + } + if (!category.equals(bm.getCategory())) { + continue; + } + return bm; } - if (!category.equals(bm.getCategory())) { - continue; - } - return bm; } return null; } @@ -222,11 +225,13 @@ public class DBTraceProgramViewBookmarkManager implements TraceProgramViewBookma return EMPTY_BOOKMARK_ARRAY; } List list = new ArrayList<>(); - for (TraceBookmark bm : space.getBookmarksAt(program.snap, addr)) { - if (!bm.getLifespan().contains(program.snap)) { - continue; + for (long s : program.viewport.getOrderedSnaps()) { + for (TraceBookmark bm : space.getBookmarksAt(s, addr)) { + if (!bm.getLifespan().contains(program.snap)) { + continue; + } + list.add(bm); } - list.add(bm); } return list.toArray(new Bookmark[list.size()]); } @@ -241,11 +246,13 @@ public class DBTraceProgramViewBookmarkManager implements TraceProgramViewBookma return EMPTY_BOOKMARK_ARRAY; } List list = new ArrayList<>(); - for (TraceBookmark bm : space.getBookmarksAt(program.snap, address)) { - if (!type.equals(bm.getTypeString())) { - continue; + for (long s : program.viewport.getOrderedSnaps()) { + for (TraceBookmark bm : space.getBookmarksAt(s, address)) { + if (!type.equals(bm.getTypeString())) { + continue; + } + list.add(bm); } - list.add(bm); } return list.toArray(new Bookmark[list.size()]); } @@ -261,10 +268,10 @@ public class DBTraceProgramViewBookmarkManager implements TraceProgramViewBookma return result; } for (TraceBookmark bm : bmt.getBookmarks()) { - if (bm.getAddress().isRegisterAddress()) { + if (bm.getAddress().getAddressSpace().isRegisterSpace()) { continue; } - if (!bm.getLifespan().contains(program.snap)) { + if (!program.viewport.containsAnyUpper(bm.getLifespan())) { continue; } result.add(bm.getAddress()); @@ -287,7 +294,7 @@ public class DBTraceProgramViewBookmarkManager implements TraceProgramViewBookma @SuppressWarnings("unchecked") protected static Iterator filteredIterator(Iterator it, Predicate predicate) { - return IteratorUtils.filteredIterator(it, e -> predicate.test((U) e)); + return (Iterator) Iterators.filter(it, e -> predicate.test(e)); } @Override @@ -298,14 +305,22 @@ public class DBTraceProgramViewBookmarkManager implements TraceProgramViewBookma } // TODO: May want to offer memory-only and/or register-only bookmark iterators return filteredIterator(bmt.getBookmarks().iterator(), - bm -> !bm.getAddress().isRegisterAddress() && bm.getLifespan().contains(program.snap)); + bm -> !bm.getAddress().getAddressSpace().isRegisterSpace() && + program.viewport.containsAnyUpper(bm.getLifespan())); } @Override public Iterator getBookmarksIterator() { + // TODO: This seems terribly inefficient. We'll have to see how/when it's used. return NestedIterator.start(bookmarkManager.getActiveMemorySpaces().iterator(), space -> filteredIterator(space.getAllBookmarks().iterator(), - bm -> bm.getLifespan().contains(program.snap))); + bm -> program.viewport.containsAnyUpper(bm.getLifespan()))); + } + + protected Comparator getBookmarkComparator(boolean forward) { + return forward + ? (b1, b2) -> b1.getAddress().compareTo(b2.getAddress()) + : (b1, b2) -> -b1.getAddress().compareTo(b2.getAddress()); } @Override @@ -320,8 +335,9 @@ public class DBTraceProgramViewBookmarkManager implements TraceProgramViewBookma if (space == null) { return Collections.emptyIterator(); } - return space.getBookmarksIntersecting(Range.closed(program.snap, program.snap), - rng).iterator(); + return program.viewport.mergedIterator( + s -> space.getBookmarksIntersecting(Range.closed(s, s), rng).iterator(), + getBookmarkComparator(forward)); }); } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewEquateTable.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewEquateTable.java index 1db945fa2f..c21753d302 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewEquateTable.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewEquateTable.java @@ -27,6 +27,7 @@ import ghidra.program.model.symbol.Equate; import ghidra.program.model.symbol.EquateTable; import ghidra.trace.database.symbol.DBTraceEquate; import ghidra.trace.database.symbol.DBTraceEquateManager; +import ghidra.trace.model.listing.TraceCodeUnit; import ghidra.util.IntersectionAddressSetView; import ghidra.util.LockHold; import ghidra.util.exception.*; @@ -97,8 +98,13 @@ public class DBTraceProgramViewEquateTable implements EquateTable { @Override public Equate getEquate(Address reference, int opndPosition, long value) { try (LockHold hold = program.trace.lockRead()) { - return doGetViewEquate( - equateManager.getReferencedByValue(program.snap, reference, opndPosition, value)); + TraceCodeUnit cu = program.getTopCode(reference, + (space, s) -> space.definedUnits().getContaining(s, reference)); + if (cu == null) { + return null; + } + return doGetViewEquate(equateManager.getReferencedByValue(cu.getStartSnap(), reference, + opndPosition, value)); } } @@ -106,7 +112,12 @@ public class DBTraceProgramViewEquateTable implements EquateTable { public List getEquates(Address reference, int opndPosition) { try (LockHold hold = program.trace.lockRead()) { List result = new ArrayList<>(); - for (DBTraceEquate equate : equateManager.getReferenced(program.snap, reference, + TraceCodeUnit cu = program.getTopCode(reference, + (space, s) -> space.definedUnits().getContaining(s, reference)); + if (cu == null) { + return result; + } + for (DBTraceEquate equate : equateManager.getReferenced(cu.getStartSnap(), reference, opndPosition)) { result.add(doGetViewEquate(equate)); } @@ -118,7 +129,12 @@ public class DBTraceProgramViewEquateTable implements EquateTable { public List getEquates(Address reference) { try (LockHold hold = program.trace.lockRead()) { List result = new ArrayList<>(); - for (DBTraceEquate equate : equateManager.getReferenced(program.snap, reference)) { + TraceCodeUnit cu = program.getTopCode(reference, + (space, s) -> space.definedUnits().getContaining(s, reference)); + if (cu == null) { + return result; + } + for (DBTraceEquate equate : equateManager.getReferenced(cu.getStartSnap(), reference)) { result.add(doGetViewEquate(equate)); } return result; @@ -127,9 +143,9 @@ public class DBTraceProgramViewEquateTable implements EquateTable { @Override public AddressIterator getEquateAddresses() { - return equateManager.getReferringAddresses(Range.singleton(program.snap)) - .getAddresses( - true); + return program.viewport + .unionedAddresses(s -> equateManager.getReferringAddresses(Range.singleton(s))) + .getAddresses(true); } @Override @@ -150,14 +166,15 @@ public class DBTraceProgramViewEquateTable implements EquateTable { @Override public AddressIterator getEquateAddresses(Address start) { - return equateManager.getReferringAddresses(Range.singleton(program.snap)) - .getAddresses( - start, true); + return program.viewport + .unionedAddresses(s -> equateManager.getReferringAddresses(Range.singleton(s))) + .getAddresses(start, true); } @Override public AddressIterator getEquateAddresses(AddressSetView asv) { - return new IntersectionAddressSetView(asv, - equateManager.getReferringAddresses(Range.singleton(program.snap))).getAddresses(true); + return new IntersectionAddressSetView(asv, program.viewport + .unionedAddresses(s -> equateManager.getReferringAddresses(Range.singleton(s)))) + .getAddresses(true); } } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewFunctionManager.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewFunctionManager.java index 81c04921b2..cce1b44939 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewFunctionManager.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewFunctionManager.java @@ -21,8 +21,6 @@ import java.io.IOException; import java.util.Iterator; import java.util.List; -import com.google.common.base.Predicate; -import com.google.common.collect.Iterators; import com.google.common.collect.Range; import generic.NestedIterator; @@ -35,8 +33,10 @@ import ghidra.program.model.symbol.Namespace; import ghidra.program.model.symbol.SourceType; import ghidra.trace.database.DBTraceUtils; import ghidra.trace.database.symbol.*; +import ghidra.trace.model.listing.TraceData; import ghidra.trace.model.symbol.TraceFunctionSymbol; import ghidra.trace.util.EmptyFunctionIterator; +import ghidra.trace.util.WrappingFunctionIterator; import ghidra.util.LockHold; import ghidra.util.exception.CancelledException; import ghidra.util.exception.InvalidInputException; @@ -44,34 +44,6 @@ import ghidra.util.task.TaskMonitor; public class DBTraceProgramViewFunctionManager implements FunctionManager { - public static class FunctionIteratorAdapter implements FunctionIterator { - private Iterator it; - - public FunctionIteratorAdapter(Iterator it) { - this.it = it; - } - - public FunctionIteratorAdapter(Iterator it, - Predicate filter) { - this.it = Iterators.filter(it, filter); - } - - @Override - public boolean hasNext() { - return it.hasNext(); - } - - @Override - public Function next() { - return it.next(); - } - - @Override - public Iterator iterator() { - return this; - } - } - protected final DBTraceProgramView program; protected final DBTraceFunctionSymbolView functions; protected final DBTraceNamespaceSymbol global; @@ -176,10 +148,16 @@ public class DBTraceProgramViewFunctionManager implements FunctionManager { if (!entryPoint.getAddressSpace().isMemorySpace()) { return null; } - // NOTE: There ought only to be one, since no overlaps allowed. - for (TraceFunctionSymbol at : functions.getAt(program.snap, null, entryPoint, false)) { - if (entryPoint.equals(at.getEntryPoint())) { - return at; + + for (long s : program.viewport.getOrderedSnaps()) { + // NOTE: There ought only to be one, since no overlaps allowed. + for (TraceFunctionSymbol at : functions.getAt(s, null, entryPoint, false)) { + if (entryPoint.equals(at.getEntryPoint())) { + return at; + } + else { + return null; // Anything below is occluded by the found function + } } } return null; @@ -187,16 +165,20 @@ public class DBTraceProgramViewFunctionManager implements FunctionManager { @Override public TraceFunctionSymbol getReferencedFunction(Address address) { + if (!address.getAddressSpace().isMemorySpace()) { + return null; + } TraceFunctionSymbol found = getFunctionAt(address); if (found != null) { return found; } - // We're assuming a data reference - if (program.trace.getCodeManager().data().getContaining(program.snap, address) == null) { + TraceData data = + program.getTopCode(address, (space, s) -> space.data().getContaining(s, address)); + if (data == null) { return null; } - DBTraceReference ref = - program.trace.getReferenceManager().getPrimaryReferenceFrom(program.snap, address, 0); + DBTraceReference ref = program.trace.getReferenceManager() + .getPrimaryReferenceFrom(data.getStartSnap(), address, 0); return ref == null ? null : getFunctionAt(ref.getToAddress()); } @@ -228,7 +210,7 @@ public class DBTraceProgramViewFunctionManager implements FunctionManager { @Override public FunctionIterator getFunctions(AddressSetView asv, boolean forward) { - return new FunctionIteratorAdapter( + return new WrappingFunctionIterator( NestedIterator.start(asv.iterator(forward), rng -> getFunctionsInRange(rng, forward)), f -> { if (!asv.contains(f.getEntryPoint())) { @@ -251,7 +233,7 @@ public class DBTraceProgramViewFunctionManager implements FunctionManager { @Override public FunctionIterator getFunctionsNoStubs(AddressSetView asv, boolean forward) { - return new FunctionIteratorAdapter( + return new WrappingFunctionIterator( NestedIterator.start(asv.iterator(forward), rng -> getFunctionsInRange(rng, forward)), f -> { if (f.isThunk()) { @@ -316,7 +298,7 @@ public class DBTraceProgramViewFunctionManager implements FunctionManager { @Override public Iterator getFunctionsOverlapping(AddressSetView set) { - return new FunctionIteratorAdapter( + return new WrappingFunctionIterator( NestedIterator.start(set.iterator(true), rng -> getFunctionsInRange(rng, true))); } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewListing.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewListing.java index e525b065e6..b25729576f 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewListing.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewListing.java @@ -22,8 +22,6 @@ import ghidra.util.LockHold; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; -// TODO: Make at/since configurable? -// NOTE: Probably not, esp., if I get the coloring right. public class DBTraceProgramViewListing extends AbstractDBTraceProgramViewListing { protected final AddressSet allMemory; @@ -36,10 +34,10 @@ public class DBTraceProgramViewListing extends AbstractDBTraceProgramViewListing public boolean isUndefined(Address start, Address end) { try (LockHold hold = program.trace.lockRead()) { for (AddressRange range : program.getAddressFactory().getAddressSet(start, end)) { - if (!codeOperations.undefinedData() - .coversRange( - Range.closed(program.snap, program.snap), range)) { - return false; + for (long s : program.viewport.getOrderedSnaps()) { + if (!isUndefinedRange(s, range)) { + return false; + } } } return true; diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewMemory.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewMemory.java index 6560cfd01c..9c49319bd5 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewMemory.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewMemory.java @@ -16,6 +16,8 @@ package ghidra.trace.database.program; import java.util.*; +import java.util.function.Consumer; +import java.util.function.Function; import com.google.common.cache.CacheBuilder; @@ -32,13 +34,33 @@ public class DBTraceProgramViewMemory extends AbstractDBTraceProgramViewMemory { super(program); } + protected DBTraceMemoryRegion getTopRegion(Function regFunc) { + return program.viewport.getTop(s -> { + // TODO: There is probably an early-bail condition I can check for. + DBTraceMemoryRegion reg = regFunc.apply(s); + if (reg != null && program.isRegionVisible(reg)) { + return reg; + } + return null; + }); + } + + protected void forVisibleRegions(Consumer action) { + for (long s : program.viewport.getOrderedSnaps()) { + // NOTE: This is slightly faster than new AddressSet(mm.getRegionsAddressSet(snap)) + for (DBTraceMemoryRegion reg : memoryManager.getRegionsAtSnap(s)) { + if (program.isRegionVisible(reg)) { + action.accept(reg); + } + } + } + } + @Override protected void recomputeAddressSet() { AddressSet temp = new AddressSet(); - // NOTE: This is slightly faster than new AddressSet(mm.getRegionsAddressSet(snap)) - for (DBTraceMemoryRegion reg : memoryManager.getRegionsAtSnap(snap)) { - temp.add(reg.getRange()); - } + // TODO: Performance test this + forVisibleRegions(reg -> temp.add(reg.getRange())); addressSet = temp; } @@ -49,26 +71,21 @@ public class DBTraceProgramViewMemory extends AbstractDBTraceProgramViewMemory { @Override public MemoryBlock getBlock(Address addr) { - DBTraceMemoryRegion region = memoryManager.getRegionContaining(snap, addr); + DBTraceMemoryRegion region = getTopRegion(s -> memoryManager.getRegionContaining(s, addr)); return region == null ? null : getBlock(region); } @Override public MemoryBlock getBlock(String blockName) { - DBTraceMemoryRegion region = memoryManager.getLiveRegionByPath(snap, blockName); + DBTraceMemoryRegion region = + getTopRegion(s -> memoryManager.getLiveRegionByPath(s, blockName)); return region == null ? null : getBlock(region); } @Override public MemoryBlock[] getBlocks() { List result = new ArrayList<>(); - for (DBTraceMemoryRegion region : memoryManager.getRegionsInternal()) { - MemoryBlock block = getBlock(region); - if (block == null) { - continue; - } - result.add(block); - } + forVisibleRegions(reg -> result.add(getBlock(reg))); return result.toArray(new MemoryBlock[result.size()]); } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewMemoryBlock.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewMemoryBlock.java index e28ee264d6..d0b4e6211b 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewMemoryBlock.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewMemoryBlock.java @@ -241,7 +241,7 @@ public class DBTraceProgramViewMemoryBlock implements MemoryBlock { throw new MemoryAccessException("Space does not exist"); } ByteBuffer buf = ByteBuffer.allocate(1); - if (space.getBytes(program.snap, addr, buf) != 1) { + if (space.getViewBytes(program.snap, addr, buf) != 1) { throw new MemoryAccessException(); } return buf.get(0); @@ -264,7 +264,7 @@ public class DBTraceProgramViewMemoryBlock implements MemoryBlock { throw new MemoryAccessException("Space does not exist"); } len = (int) Math.min(len, range.getMaxAddress().subtract(addr) + 1); - return space.getBytes(program.snap, addr, ByteBuffer.wrap(b, off, len)); + return space.getViewBytes(program.snap, addr, ByteBuffer.wrap(b, off, len)); } @Override diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewProgramContext.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewProgramContext.java index 01301765d9..b4ba7f92aa 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewProgramContext.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewProgramContext.java @@ -53,8 +53,11 @@ public class DBTraceProgramViewProgramContext extends AbstractProgramContext { List registers = language.getRegisters(); List result = new ArrayList<>(registers.size()); for (Register register : registers) { - if (registerContextManager.hasRegisterValue(language, register, program.snap)) { - result.add(register); + for (long s : program.viewport.getReversedSnaps()) { + if (registerContextManager.hasRegisterValue(language, register, s)) { + result.add(register); + break; + } } } return result.toArray(new Register[result.size()]); @@ -66,10 +69,27 @@ public class DBTraceProgramViewProgramContext extends AbstractProgramContext { return value == null ? null : signed ? value.getSignedValue() : value.getUnsignedValue(); } + protected RegisterValue combine(RegisterValue v1, RegisterValue v2) { + if (v1 == null) { + return v2; + } + else if (v2 == null) { + return v1; + } + return v1.combineValues(v2); + } + + protected RegisterValue stack(RegisterValue value, Register register, Address address) { + for (long s : program.viewport.getReversedSnaps()) { + value = combine(value, registerContextManager.getValue(language, register, s, address)); + } + return value; + } + @Override public RegisterValue getRegisterValue(Register register, Address address) { - return registerContextManager.getValueWithDefault(language, register, program.snap, - address); + RegisterValue value = registerContextManager.getDefaultValue(language, register, address); + return stack(value, register, address); } @Override @@ -81,7 +101,8 @@ public class DBTraceProgramViewProgramContext extends AbstractProgramContext { @Override public RegisterValue getNonDefaultValue(Register register, Address address) { - return registerContextManager.getValue(language, register, program.snap, address); + RegisterValue value = new RegisterValue(register); + return stack(value, register, address); } @Override @@ -105,22 +126,24 @@ public class DBTraceProgramViewProgramContext extends AbstractProgramContext { @Override public AddressRangeIterator getRegisterValueAddressRanges(Register register) { - return registerContextManager.getRegisterValueAddressRanges(language, register, - program.snap).getAddressRanges(); + return program.viewport.unionedAddresses( + s -> registerContextManager.getRegisterValueAddressRanges(language, register, + s)).getAddressRanges(); } @Override public AddressRangeIterator getRegisterValueAddressRanges(Register register, Address start, Address end) { return new NestedAddressRangeIterator<>( - language.getAddressFactory().getAddressSet(start, end).iterator(), range -> { - return registerContextManager.getRegisterValueAddressRanges(language, register, - program.snap, range).iterator(); - }); + language.getAddressFactory().getAddressSet(start, end).iterator(), + range -> program.viewport.unionedAddresses( + s -> registerContextManager.getRegisterValueAddressRanges(language, register, + s, range)).iterator()); } @Override public AddressRange getRegisterValueRangeContaining(Register register, Address address) { + // TODO: I don't know the value of making this work through the viewport. Entry entry = registerContextManager.getEntry(language, register, program.snap, address); if (entry != null) { @@ -164,6 +187,7 @@ public class DBTraceProgramViewProgramContext extends AbstractProgramContext { @Override public boolean hasValueOverRange(Register register, BigInteger value, AddressSetView addressSet) { + // TODO: Not sure the value of making this use the viewport RegisterValue regVal = new RegisterValue(register, value); try (LockHold hold = program.trace.lockRead()) { AddressSet remains = new AddressSet(addressSet); diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewRegisterListing.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewRegisterListing.java index 9048e877b9..de39e49d0a 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewRegisterListing.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewRegisterListing.java @@ -19,6 +19,7 @@ import com.google.common.collect.Range; import ghidra.program.model.address.*; import ghidra.trace.database.listing.DBTraceCodeRegisterSpace; +import ghidra.trace.database.listing.UndefinedDBTraceData; import ghidra.trace.database.thread.DBTraceThread; import ghidra.trace.model.program.TraceProgramViewRegisterListing; import ghidra.util.exception.CancelledException; @@ -45,6 +46,11 @@ public class DBTraceProgramViewRegisterListing extends AbstractDBTraceProgramVie return thread; } + @Override + public UndefinedDBTraceData doCreateUndefinedUnit(Address address) { + throw new UnsupportedOperationException(); + } + @Override public boolean isUndefined(Address start, Address end) { return codeOperations.undefinedData() diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewRegisterMemoryBlock.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewRegisterMemoryBlock.java index d7b3398524..c351184833 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewRegisterMemoryBlock.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewRegisterMemoryBlock.java @@ -225,7 +225,7 @@ public class DBTraceProgramViewRegisterMemoryBlock implements MemoryBlock { throw new MemoryAccessException(); } ByteBuffer buf = ByteBuffer.allocate(1); - if (space.getBytes(program.snap, addr, buf) != 1) { + if (space.getViewBytes(program.snap, addr, buf) != 1) { throw new MemoryAccessException(); } return buf.get(0); @@ -242,7 +242,7 @@ public class DBTraceProgramViewRegisterMemoryBlock implements MemoryBlock { throw new MemoryAccessException(); } len = (int) Math.min(len, range.getMaxAddress().subtract(addr) + 1); - return space.getBytes(program.snap, addr, ByteBuffer.wrap(b, off, len)); + return space.getViewBytes(program.snap, addr, ByteBuffer.wrap(b, off, len)); } @Override diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewRegisters.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewRegisters.java index 999b7b5982..40682e03bc 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewRegisters.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewRegisters.java @@ -41,6 +41,7 @@ import ghidra.trace.model.Trace; import ghidra.trace.model.data.TraceBasedDataTypeManager; import ghidra.trace.model.program.TraceProgramView; import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.util.TraceTimeViewport; import ghidra.util.exception.CancelledException; import ghidra.util.exception.DuplicateNameException; import ghidra.util.task.TaskMonitor; @@ -616,6 +617,11 @@ public class DBTraceProgramViewRegisters implements TraceProgramView { return view.getSnap(); } + @Override + public TraceTimeViewport getViewport() { + return view.getViewport(); + } + @Override public Long getMaxSnap() { return view.getMaxSnap(); diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewRootModule.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewRootModule.java index 45682ce399..fc5d6b9f19 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewRootModule.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewRootModule.java @@ -17,11 +17,14 @@ package ghidra.trace.database.program; import java.util.ArrayList; import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Consumer; import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressSetView; import ghidra.program.model.listing.*; -import ghidra.trace.database.memory.DBTraceMemoryRegion; +import ghidra.trace.model.memory.TraceMemoryRegion; +import ghidra.util.ComparatorMath; import ghidra.util.LockHold; import ghidra.util.exception.*; @@ -102,11 +105,10 @@ public class DBTraceProgramViewRootModule implements ProgramModule { // NOTE: Would flush on snap change try (LockHold hold = LockHold.lock(program.trace.getReadWriteLock().readLock())) { List frags = new ArrayList<>(); - for (DBTraceMemoryRegion region : program.trace.getMemoryManager() - .getRegionsAtSnap(program.snap)) { + program.memory.forVisibleRegions(region -> { frags.add(listing.fragmentsByRegion.computeIfAbsent(region, r -> new DBTraceProgramViewFragment(listing, r))); - } + }); return frags.toArray(new DBTraceProgramViewFragment[frags.size()]); } } @@ -114,17 +116,11 @@ public class DBTraceProgramViewRootModule implements ProgramModule { @Override public int getIndex(String name) { // TODO: This isn't pretty at all. Really should database these. + List names = new ArrayList<>(); try (LockHold hold = LockHold.lock(program.trace.getReadWriteLock().readLock())) { - int i = 0; - for (DBTraceMemoryRegion region : program.trace.getMemoryManager() - .getRegionsAtSnap(program.snap)) { - if (name.equals(region.getName())) { - return i; - } - i++; - } + program.memory.forVisibleRegions(region -> names.add(region.getName())); } - return -1; + return names.indexOf(names); } @Override @@ -173,14 +169,44 @@ public class DBTraceProgramViewRootModule implements ProgramModule { return true; } + protected T reduceRegions(java.util.function.Function func, + BiFunction reducer) { + var action = new Consumer() { + public T cur; + + @Override + public void accept(TraceMemoryRegion region) { + if (cur == null) { + cur = func.apply(region); + } + else { + cur = reducer.apply(cur, func.apply(region)); + } + } + }; + return action.cur; + } + @Override public Address getMinAddress() { - return program.trace.getMemoryManager().getRegionsAddressSet(program.snap).getMinAddress(); + if (!program.viewport.isForked()) { + return program.trace.getMemoryManager() + .getRegionsAddressSet(program.snap) + .getMinAddress(); + } + // TODO: There has got to be a better way + return reduceRegions(TraceMemoryRegion::getMinAddress, ComparatorMath::cmin); } @Override public Address getMaxAddress() { - return program.trace.getMemoryManager().getRegionsAddressSet(program.snap).getMaxAddress(); + if (!program.viewport.isForked()) { + return program.trace.getMemoryManager() + .getRegionsAddressSet(program.snap) + .getMaxAddress(); + } + // TODO: There has got to be a better way + return reduceRegions(TraceMemoryRegion::getMaxAddress, ComparatorMath::cmax); } @Override @@ -195,7 +221,8 @@ public class DBTraceProgramViewRootModule implements ProgramModule { @Override public AddressSetView getAddressSet() { - return program.trace.getMemoryManager().getRegionsAddressSet(program.snap); + return program.viewport + .unionedAddresses(s -> program.trace.getMemoryManager().getRegionsAddressSet(s)); } @Override diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewSymbolTable.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewSymbolTable.java index a86aeea4fb..e2ce61b20e 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewSymbolTable.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewSymbolTable.java @@ -153,12 +153,16 @@ public class DBTraceProgramViewSymbolTable implements SymbolTable { } } - protected T requireInLifespan(T sym) { + protected T requireVisible(T sym) { if (!(sym instanceof TraceSymbolWithLifespan)) { return sym; } + if (sym instanceof TraceFunctionSymbol) { + TraceFunctionSymbol function = (TraceFunctionSymbol) sym; + return program.isFunctionVisible(function, function.getLifespan()) ? sym : null; + } TraceSymbolWithLifespan wl = (TraceSymbolWithLifespan) sym; - if (wl.getLifespan().contains(program.snap)) { + if (program.viewport.containsAnyUpper(wl.getLifespan())) { return sym; } return null; @@ -166,7 +170,7 @@ public class DBTraceProgramViewSymbolTable implements SymbolTable { @Override public Symbol getSymbol(long symbolID) { - return requireInLifespan(symbolManager.getSymbolByID(symbolID)); + return requireVisible(symbolManager.getSymbolByID(symbolID)); } @Override @@ -178,7 +182,7 @@ public class DBTraceProgramViewSymbolTable implements SymbolTable { if (!addr.equals(sym.getAddress())) { continue; } - if (requireInLifespan(sym) == null) { + if (requireVisible(sym) == null) { continue; } return sym; @@ -198,7 +202,7 @@ public class DBTraceProgramViewSymbolTable implements SymbolTable { for (TraceSymbol sym : symbolManager.allSymbols() .getChildrenNamed(name, assertTraceNamespace(namespace))) { - if (requireInLifespan(sym) == null) { + if (requireVisible(sym) == null) { continue; } return sym; @@ -218,7 +222,7 @@ public class DBTraceProgramViewSymbolTable implements SymbolTable { try (LockHold hold = program.trace.lockRead()) { List result = new ArrayList<>(); for (TraceSymbol sym : symbolManager.allSymbols().getChildrenNamed(name, parent)) { - if (requireInLifespan(sym) != null) { + if (requireVisible(sym) != null) { result.add(sym); } } @@ -239,7 +243,7 @@ public class DBTraceProgramViewSymbolTable implements SymbolTable { for (TraceSymbol sym : symbolManager.labelsAndFunctions() .getChildrenNamed(name, parent)) { - if (requireInLifespan(sym) != null) { + if (requireVisible(sym) != null) { result.add(sym); } } @@ -352,7 +356,7 @@ public class DBTraceProgramViewSymbolTable implements SymbolTable { if (addr.isMemoryAddress()) { return symbolManager.labelsAndFunctions().hasAt(program.snap, null, addr, true); } - if (addr.isRegisterAddress() || addr.isStackAddress()) { + if (addr.getAddressSpace().isRegisterSpace() || addr.isStackAddress()) { return symbolManager.allVariables().hasAt(addr, true); } return false; @@ -527,7 +531,7 @@ public class DBTraceProgramViewSymbolTable implements SymbolTable { return sym.getParentNamespace(); } } - if (addr.isRegisterAddress() || addr.isStackAddress()) { + if (addr.getAddressSpace().isRegisterSpace() || addr.isStackAddress()) { for (TraceSymbol sym : symbolManager.allVariables().getAt(addr, true)) { return sym.getParentNamespace(); } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceVariableSnapProgramView.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceVariableSnapProgramView.java index 2f5ff9bb2b..bba16c2c5f 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceVariableSnapProgramView.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceVariableSnapProgramView.java @@ -30,6 +30,7 @@ import ghidra.trace.model.program.TraceVariableSnapProgramView; */ public class DBTraceVariableSnapProgramView extends DBTraceProgramView implements TraceVariableSnapProgramView { + //private static final int SNAP_CHANGE_EVENT_THRESHHOLD = 100; public DBTraceVariableSnapProgramView(DBTrace trace, long snap, CompilerSpec compilerSpec) { @@ -50,6 +51,7 @@ public class DBTraceVariableSnapProgramView extends DBTraceProgramView } //long oldSnap = this.snap; this.snap = newSnap; + viewport.setSnap(newSnap); memory.setSnap(newSnap); // TODO: I could be more particular, but this seems to work fast enough, now. diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/space/AbstractDBTraceSpaceBasedManager.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/space/AbstractDBTraceSpaceBasedManager.java index 4d3e652a20..456c4bcd1a 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/space/AbstractDBTraceSpaceBasedManager.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/space/AbstractDBTraceSpaceBasedManager.java @@ -33,7 +33,9 @@ import ghidra.trace.database.thread.DBTraceThread; import ghidra.trace.database.thread.DBTraceThreadManager; import ghidra.trace.model.stack.TraceStackFrame; import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.util.TraceAddressSpace; import ghidra.util.LockHold; +import ghidra.util.Msg; import ghidra.util.database.*; import ghidra.util.database.annot.*; import ghidra.util.exception.VersionException; @@ -127,7 +129,11 @@ public abstract class AbstractDBTraceSpaceBasedManager { void accept(T t) throws E1, E2; } + interface ExcSupplier { + T get() throws E1, E2; + } + interface ExcPredicate { boolean test(T t) throws E1, E2; } @@ -79,7 +83,7 @@ public interface DBTraceDelegatingManager { default T delegateRead(AddressSpace space, ExcFunction func) throws E1, E2 { - return delegateRead(space, func, null); + return delegateRead(space, func, (T) null); } default T delegateRead(AddressSpace space, @@ -94,6 +98,18 @@ public interface DBTraceDelegatingManager { } } + default T delegateRead(AddressSpace space, + ExcFunction func, ExcSupplier ifNull) throws E1, E2 { + checkIsInMemory(space); + try (LockHold hold = LockHold.lock(readLock())) { + M m = getForSpace(space, false); + if (m == null) { + return ifNull.get(); + } + return func.apply(m); + } + } + default int delegateReadI(AddressSpace space, ToIntFunction func, int ifNull) { checkIsInMemory(space); try (LockHold hold = LockHold.lock(readLock())) { diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/stack/DBTraceStackManager.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/stack/DBTraceStackManager.java index c34cde1747..81bd781db3 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/stack/DBTraceStackManager.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/stack/DBTraceStackManager.java @@ -115,7 +115,9 @@ public class DBTraceStackManager implements TraceStackManager, DBTraceManager { if (found == null) { return null; } - if (found.getThread() != thread) { + if (found.getThread() != thread || found.getSnap() > snap) { + // Encoded field results in unsigned index + // NB. Conventionally, a search should never traverse 0 (real to scratch space) return null; } return found; diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/symbol/DBTraceReferenceManager.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/symbol/DBTraceReferenceManager.java index 23c71d8691..b3b6b529e8 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/symbol/DBTraceReferenceManager.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/symbol/DBTraceReferenceManager.java @@ -230,10 +230,10 @@ public class DBTraceReferenceManager extends } @Override - public Collection getFlowRefrencesFrom(long snap, + public Collection getFlowReferencesFrom(long snap, Address fromAddress) { return delegateRead(fromAddress.getAddressSpace(), - s -> s.getFlowRefrencesFrom(snap, fromAddress), Collections.emptyList()); + s -> s.getFlowReferencesFrom(snap, fromAddress), Collections.emptyList()); } @Override diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/symbol/DBTraceReferenceSpace.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/symbol/DBTraceReferenceSpace.java index a430e0a1db..162ca73e5a 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/symbol/DBTraceReferenceSpace.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/symbol/DBTraceReferenceSpace.java @@ -564,7 +564,7 @@ public class DBTraceReferenceSpace implements DBTraceSpaceBased, TraceReferenceS } @Override - public Collection getFlowRefrencesFrom(long snap, + public Collection getFlowReferencesFrom(long snap, Address fromAddress) { return Collections2.filter(getReferencesFrom(snap, fromAddress), r -> r.getReferenceType().isFlow()); diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/time/DBTraceSnapshot.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/time/DBTraceSnapshot.java index 90f002677f..3e7c75122e 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/time/DBTraceSnapshot.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/time/DBTraceSnapshot.java @@ -22,9 +22,11 @@ import ghidra.trace.database.thread.DBTraceThread; import ghidra.trace.model.Trace; import ghidra.trace.model.Trace.TraceSnapshotChangeType; import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.model.time.TraceSchedule; import ghidra.trace.model.time.TraceSnapshot; import ghidra.trace.util.TraceChangeRecord; import ghidra.util.LockHold; +import ghidra.util.Msg; import ghidra.util.database.*; import ghidra.util.database.annot.*; @@ -33,14 +35,14 @@ public class DBTraceSnapshot extends DBAnnotatedObject implements TraceSnapshot protected static final String TABLE_NAME = "Snapshots"; protected static final String REAL_TIME_COLUMN_NAME = "RealTime"; - protected static final String TICKS_COLUMN_NAME = "Ticks"; + protected static final String SCHEDULE_COLUMN_NAME = "Schedule"; protected static final String DESCRIPTION_COLUMN_NAME = "Description"; protected static final String THREAD_COLUMN_NAME = "Thread"; @DBAnnotatedColumn(REAL_TIME_COLUMN_NAME) static DBObjectColumn REAL_TIME_COLUMN; - @DBAnnotatedColumn(TICKS_COLUMN_NAME) - static DBObjectColumn TICKS_COLUMN; + @DBAnnotatedColumn(SCHEDULE_COLUMN_NAME) + static DBObjectColumn SCHEDULE_COLUMN; @DBAnnotatedColumn(DESCRIPTION_COLUMN_NAME) static DBObjectColumn DESCRIPTION_COLUMN; @DBAnnotatedColumn(THREAD_COLUMN_NAME) @@ -48,8 +50,8 @@ public class DBTraceSnapshot extends DBAnnotatedObject implements TraceSnapshot @DBAnnotatedField(column = REAL_TIME_COLUMN_NAME) long realTime; // milliseconds - @DBAnnotatedField(column = TICKS_COLUMN_NAME) - long ticks; + @DBAnnotatedField(column = SCHEDULE_COLUMN_NAME, indexed = true) + String scheduleStr = ""; @DBAnnotatedField(column = DESCRIPTION_COLUMN_NAME) String description; @DBAnnotatedField(column = THREAD_COLUMN_NAME) @@ -58,6 +60,7 @@ public class DBTraceSnapshot extends DBAnnotatedObject implements TraceSnapshot public final DBTraceTimeManager manager; private DBTraceThread eventThread; + private TraceSchedule schedule; public DBTraceSnapshot(DBTraceTimeManager manager, DBCachedObjectStore store, DBRecord record) { @@ -69,23 +72,33 @@ public class DBTraceSnapshot extends DBAnnotatedObject implements TraceSnapshot protected void fresh(boolean created) throws IOException { if (created) { threadKey = -1; + scheduleStr = ""; } else { eventThread = manager.threadManager.getThread(threadKey); + if (!"".equals(scheduleStr)) { + try { + schedule = TraceSchedule.parse(scheduleStr); + } + catch (IllegalArgumentException e) { + Msg.error(this, "Could not parse schedule: " + schedule, e); + // Leave as null (or previous value?) + } + } } } @Override public String toString() { - return String.format("", realTime, - ticks, description); + return String.format( + "", + key, realTime, scheduleStr, description); } - protected void set(long realTime, String description, long ticks) { + protected void set(long realTime, String description) { this.realTime = realTime; this.description = description; - this.ticks = ticks; - update(REAL_TIME_COLUMN, DESCRIPTION_COLUMN, TICKS_COLUMN); + update(REAL_TIME_COLUMN, DESCRIPTION_COLUMN); } @Override @@ -146,15 +159,21 @@ public class DBTraceSnapshot extends DBAnnotatedObject implements TraceSnapshot } @Override - public long getTicks() { - return ticks; + public TraceSchedule getSchedule() { + return schedule; } @Override - public void setTicks(long ticks) { + public String getScheduleString() { + return scheduleStr; + } + + @Override + public void setSchedule(TraceSchedule schedule) { try (LockHold hold = LockHold.lock(manager.lock.writeLock())) { - this.ticks = ticks; - update(TICKS_COLUMN); + this.schedule = schedule; + this.scheduleStr = schedule == null ? "" : schedule.toString(); + update(SCHEDULE_COLUMN); } manager.trace.setChanged( new TraceChangeRecord<>(TraceSnapshotChangeType.CHANGED, null, this)); diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/time/DBTraceTimeManager.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/time/DBTraceTimeManager.java index eeeda94db7..fd485fd937 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/time/DBTraceTimeManager.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/time/DBTraceTimeManager.java @@ -18,6 +18,7 @@ package ghidra.trace.database.time; import java.io.IOException; import java.util.Collection; import java.util.Collections; +import java.util.Map.Entry; import java.util.concurrent.locks.ReadWriteLock; import db.DBHandle; @@ -25,8 +26,7 @@ import ghidra.trace.database.DBTrace; import ghidra.trace.database.DBTraceManager; import ghidra.trace.database.thread.DBTraceThreadManager; import ghidra.trace.model.Trace.TraceSnapshotChangeType; -import ghidra.trace.model.time.TraceSnapshot; -import ghidra.trace.model.time.TraceTimeManager; +import ghidra.trace.model.time.*; import ghidra.trace.util.TraceChangeRecord; import ghidra.util.LockHold; import ghidra.util.database.*; @@ -40,6 +40,7 @@ public class DBTraceTimeManager implements TraceTimeManager, DBTraceManager { protected final DBTraceThreadManager threadManager; protected final DBCachedObjectStore snapshotStore; + protected final DBCachedObjectIndex snapshotsBySchedule; public DBTraceTimeManager(DBHandle dbh, DBOpenMode openMode, ReadWriteLock lock, TaskMonitor monitor, DBTrace trace, DBTraceThreadManager threadManager) @@ -52,6 +53,7 @@ public class DBTraceTimeManager implements TraceTimeManager, DBTraceManager { snapshotStore = factory.getOrCreateCachedStore(DBTraceSnapshot.TABLE_NAME, DBTraceSnapshot.class, (s, r) -> new DBTraceSnapshot(this, s, r), true); + snapshotsBySchedule = snapshotStore.getIndex(String.class, DBTraceSnapshot.SCHEDULE_COLUMN); } @Override @@ -68,7 +70,11 @@ public class DBTraceTimeManager implements TraceTimeManager, DBTraceManager { public DBTraceSnapshot createSnapshot(String description) { try (LockHold hold = LockHold.lock(lock.writeLock())) { DBTraceSnapshot snapshot = snapshotStore.create(); - snapshot.set(System.currentTimeMillis(), description, 0); + snapshot.set(System.currentTimeMillis(), description); + if (snapshot.getKey() == 0) { + // Convention for first snap + snapshot.setSchedule(TraceSchedule.snap(0)); + } trace.setChanged( new TraceChangeRecord<>(TraceSnapshotChangeType.ADDED, null, snapshot)); return snapshot; @@ -76,7 +82,7 @@ public class DBTraceTimeManager implements TraceTimeManager, DBTraceManager { } @Override - public TraceSnapshot getSnapshot(long snap, boolean createIfAbsent) { + public DBTraceSnapshot getSnapshot(long snap, boolean createIfAbsent) { if (!createIfAbsent) { try (LockHold hold = LockHold.lock(lock.readLock())) { return snapshotStore.getObjectAt(snap); @@ -86,7 +92,11 @@ public class DBTraceTimeManager implements TraceTimeManager, DBTraceManager { DBTraceSnapshot snapshot = snapshotStore.getObjectAt(snap); if (snapshot == null) { snapshot = snapshotStore.create(snap); - snapshot.set(System.currentTimeMillis(), "", 0); + snapshot.set(System.currentTimeMillis(), ""); + if (snapshot.getKey() == 0) { + // Convention for first snap + snapshot.setSchedule(TraceSchedule.snap(0)); + } trace.setChanged( new TraceChangeRecord<>(TraceSnapshotChangeType.ADDED, null, snapshot)); } @@ -94,11 +104,31 @@ public class DBTraceTimeManager implements TraceTimeManager, DBTraceManager { } } + @Override + public DBTraceSnapshot getMostRecentSnapshot(long snap) { + try (LockHold hold = LockHold.lock(lock.readLock())) { + Entry ent = snapshotStore.asMap().floorEntry(snap); + return ent == null ? null : ent.getValue(); + } + } + + @Override + public Collection getSnapshotsWithSchedule(TraceSchedule schedule) { + return snapshotsBySchedule.get(schedule.toString()); + } + @Override public Collection getAllSnapshots() { return Collections.unmodifiableCollection(snapshotStore.asMap().values()); } + @Override + public Collection getSnapshots(long fromSnap, boolean fromInclusive, + long toSnap, boolean toInclusive) { + return Collections.unmodifiableCollection( + snapshotStore.asMap().subMap(fromSnap, fromInclusive, toSnap, toInclusive).values()); + } + @Override public Long getMaxSnap() { return snapshotStore.getMaxKey(); diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/Trace.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/Trace.java index 7f87135b50..328c935c62 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/Trace.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/Trace.java @@ -273,8 +273,8 @@ public interface Trace extends DataTypeManagerDomainObject { new TraceSymbolChangeType<>(); public static final TraceSymbolChangeType

ADDRESS_CHANGED = new TraceSymbolChangeType<>(); - public static final TraceSymbolChangeType> LIFESPAN_CHANGED = - new TraceSymbolChangeType<>(); + public static final DefaultTraceChangeType> LIFESPAN_CHANGED = + new DefaultTraceChangeType<>(); public static final TraceSymbolChangeType DELETED = new TraceSymbolChangeType<>(); // Other changes not captured above public static final TraceSymbolChangeType CHANGED = new TraceSymbolChangeType<>(); diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/listing/TraceCodeManager.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/listing/TraceCodeManager.java index 8f41edb178..66e859d883 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/listing/TraceCodeManager.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/listing/TraceCodeManager.java @@ -15,25 +15,57 @@ */ package ghidra.trace.model.listing; +import ghidra.lifecycle.Experimental; import ghidra.program.model.address.AddressSetView; import ghidra.program.model.address.AddressSpace; import ghidra.trace.model.stack.TraceStackFrame; import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.util.TraceAddressSpace; public interface TraceCodeManager extends TraceCodeOperations { - /** Operand index for data. Will always be zero */ - int DATA_OP_INDEX = 0; + /** + * Get the code space for the memory or registers of the given trace address space + * + * @param space the trace address space (thread, stack frame, address space) + * @param createIfAbsent true to create the space if it's not already present + * @return the space, of {@code null} if absent and not created + */ + TraceCodeSpace getCodeSpace(TraceAddressSpace space, boolean createIfAbsent); + + /** + * Get the code space for the memory of the given address space + * + * @param space the address space + * @param createIfAbsent true to create the space if it's not already present + * @return the space, of {@code null} if absent and not created + */ TraceCodeSpace getCodeSpace(AddressSpace space, boolean createIfAbsent); + /** + * Get the code space for registers of the given thread's innermost frame + * + * @param thread the thread + * @param createIfAbsent true to create the space if it's not already present + * @return the space, of {@code null} if absent and not created + */ TraceCodeRegisterSpace getCodeRegisterSpace(TraceThread thread, boolean createIfAbsent); + /** + * Get the code space for registers of the given thread and frame + * + * @param thread the thread + * @param frameLevel the frame (0 for innermost) + * @param createIfAbsent true to create the space if it's not already present + * @return the space, of {@code null} if absent and not created + */ TraceCodeRegisterSpace getCodeRegisterSpace(TraceThread thread, int frameLevel, boolean createIfAbsent); /** * Get the code space for registers of the given stack frame * + *

* Note this is simply a shortcut for {@link #getCodeRegisterSpace(TraceThread, int, boolean)}, * and does not in any way bind the space to the lifetime of the given frame. Nor, if the frame * is moved, will this space move with it. @@ -44,8 +76,23 @@ public interface TraceCodeManager extends TraceCodeOperations { */ TraceCodeRegisterSpace getCodeRegisterSpace(TraceStackFrame frame, boolean createIfAbsent); + /** + * Query for the address set where code units have been added between the two given snaps + * + * @param from the beginning snap + * @param to the ending snap + * @return the view of addresses where units have been added + */ + @Experimental AddressSetView getCodeAdded(long from, long to); + /** + * Query for the address set where code units have been removed between the two given snaps + * + * @param from the beginning snap + * @param to the ending snap + * @return the view of addresses where units have been removed + */ + @Experimental AddressSetView getCodeRemoved(long from, long to); - } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/listing/TraceCodeUnit.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/listing/TraceCodeUnit.java index a65698634f..33e3cdd693 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/listing/TraceCodeUnit.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/listing/TraceCodeUnit.java @@ -19,15 +19,21 @@ import java.nio.ByteBuffer; import com.google.common.collect.Range; +import ghidra.program.model.address.AddressRange; import ghidra.program.model.lang.Language; import ghidra.program.model.listing.CodeUnit; import ghidra.program.model.util.TypeMismatchException; import ghidra.trace.model.Trace; +import ghidra.trace.model.TraceAddressSnapRange; import ghidra.trace.model.program.TraceProgramView; import ghidra.trace.model.symbol.TraceReference; import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.util.TraceAddressSpace; import ghidra.util.Saveable; +/** + * A code unit in a {@link Trace} + */ public interface TraceCodeUnit extends CodeUnit { /** * Get the trace in which this code unit exists @@ -39,9 +45,12 @@ public interface TraceCodeUnit extends CodeUnit { @Override TraceProgramView getProgram(); + TraceAddressSpace getTraceSpace(); + /** * Get the thread associated with this code unit * + *

* A thread is associated with a code unit if it exists in a register space * * @return the thread @@ -51,6 +60,7 @@ public interface TraceCodeUnit extends CodeUnit { /** * Get the language of this code unit * + *

* Currently, for data units, this is always the base or "host" language of the trace. For * instructions, this may be a guest language. * @@ -58,6 +68,20 @@ public interface TraceCodeUnit extends CodeUnit { */ Language getLanguage(); + /** + * Get the bounds of this unit in space and time + * + * @return the bounds + */ + TraceAddressSnapRange getBounds(); + + /** + * Get the address range covered by this unit + * + * @return the range + */ + AddressRange getRange(); + /** * Get the lifespan of this code unit * @@ -95,6 +119,7 @@ public interface TraceCodeUnit extends CodeUnit { /** * Read bytes starting at this unit's address plus the given offset into the given buffer * + *

* This method honors the markers (position and limit) of the destination buffer. Use those * markers to control the destination offset and maximum length. * @@ -107,15 +132,19 @@ public interface TraceCodeUnit extends CodeUnit { /** * Set a property of the given type to the given value * + *

* This method is preferred to {@link #setTypedProperty(String, Object)}, because in the case * the property map does not already exist, the desired type is given explicitly. * - * While it is best practice to match -valueClass- exactly with the type of the map, this method - * will work so long as the given -valueClass- is a subtype of the map's type. If the property - * map does not already exist, it is created with the given -valueClass-. Note that there is no - * established mechanism for restoring values of a subtype from the underlying database. + *

+ * While it is best practice to match {@code valueClass} exactly with the type of the map, this + * method will work so long as the given {@code valueClass} is a subtype of the map's type. If + * the property map does not already exist, it is created with the given {@code valueClass}. + * Note that there is no established mechanism for restoring values of a subtype from the + * underlying database. * - * Currently, the only supported types are {@link Integer},{@link String}, {@link Void}, and + *

+ * Currently, the only supported types are {@link Integer}, {@link String}, {@link Void}, and * subtypes of {@link Saveable}. * * @param name the name of the property @@ -127,10 +156,12 @@ public interface TraceCodeUnit extends CodeUnit { /** * Set a property having the same type as the given value * + *

* If the named property has a super-type of the value's type, the value is accepted. If not, a * {@link TypeMismatchException} is thrown. If the property map does not already exist, it is * created having exactly the type of the given value. * + *

* This method exists for two reasons: 1) To introduce the type variable U, which is more * existential, and 2) to remove the requirement to subtype {@link Saveable}. Otherwise, this * method is identical in operation to {@link #setProperty(String, Saveable)}. @@ -143,11 +174,13 @@ public interface TraceCodeUnit extends CodeUnit { /** * Get a property having the given type * - * If the named property has a sub-type of the given -valueClass-, the value (possibly + *

+ * If the named property has a sub-type of the given {@code valueClass}, the value (possibly * {@code null}) is returned. If the property does not exist, {@code null} is returned. * Otherwise {@link TypeMismatchException} is thrown, even if the property is not set at this * unit's address. * + *

* Note that getting a {@link Void} property will always return {@code null}. Use * {@link #getVoidProperty(String)} instead to detect if the property is set. * {@link #hasProperty(String)} will also work, but it does not verify that the property's type diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/memory/TraceMemoryOperations.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/memory/TraceMemoryOperations.java index fdc5d26e67..7e59ecfc8c 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/memory/TraceMemoryOperations.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/memory/TraceMemoryOperations.java @@ -27,6 +27,7 @@ import com.google.common.collect.Range; import ghidra.program.model.address.*; import ghidra.program.model.mem.MemBuffer; import ghidra.trace.model.*; +import ghidra.trace.model.time.TraceSnapshot; import ghidra.util.exception.DuplicateNameException; import ghidra.util.task.TaskMonitor; @@ -45,6 +46,19 @@ import ghidra.util.task.TaskMonitor; * states can be manipulated directly; however, this is recommended only to record read failures, * using the state {@link TraceMemoryState#ERROR}. A state of {@code null} is equivalent to * {@link TraceMemoryState#UNKNOWN} and indicates no observation has been made. + * + *

+ * Negative snaps may have different semantics than positive, since negative snaps are used as + * "scratch space". These snaps are not presumed to have any temporal relation to their neighbors, + * or any other snap for that matter. Clients may use the description field of the + * {@link TraceSnapshot} to indicate a relationship to another snap. Operations which seek the + * "most-recent" data might not retrieve anything from scratch snaps, and writing to a scratch snap + * might not cause any changes to others. Note the "integrity" of data where the memory state is not + * {@link TraceMemoryState#KNOWN} may be neglected to some extent. For example, writing bytes to + * snap -10 may cause bytes in snap -9 to change, where the effected range at snap -9 has state + * {@link TraceMemoryState#UNKNOWN}. The time semantics are not necessarily prohibited in scratch + * space, but implementations may choose cheaper semantics if desired. Clients should be wary not to + * accidentally rely on implied temporal relationships in scratch space. */ public interface TraceMemoryOperations { /** @@ -219,6 +233,15 @@ public interface TraceMemoryOperations { */ TraceMemoryState getState(long snap, Address address); + /** + * Get the state of memory at a given snap and address, following schedule forks + * + * @param snap the time + * @param address the location + * @return the state, and the snap where it was found + */ + Entry getViewState(long snap, Address address); + /** * Get the entry recording the most recent state at the given snap and address * @@ -233,6 +256,17 @@ public interface TraceMemoryOperations { Entry getMostRecentStateEntry(long snap, Address address); + /** + * Get the entry recording the most recent state at the given snap and address, following + * schedule forks + * + * @param snap the time + * @param address the location + * @return the state + */ + Entry getViewMostRecentStateEntry(long snap, + Address address); + /** * Get at least the subset of addresses having state satisfying the given predicate * @@ -359,6 +393,22 @@ public interface TraceMemoryOperations { */ int getBytes(long snap, Address start, ByteBuffer buf); + /** + * Read the most recent bytes from the given snap and address, following schedule forks + * + *

+ * This behaves similarly to {@link #getBytes(long, Address, ByteBuffer)}, except it checks for + * the {@link TraceMemoryState#KNOWN} state among each involved snap range and reads the + * applicable address ranges, preferring the most recent. Where memory is never known the buffer + * is left unmodified. + * + * @param snap the time + * @param start the location + * @param buf the destination buffer of bytes + * @return the number of bytes read + */ + int getViewBytes(long snap, Address start, ByteBuffer buf); + /** * Search the given address range at the given snap for a given byte pattern * diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/memory/TraceMemoryRegisterSpace.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/memory/TraceMemoryRegisterSpace.java index f37cd0e6bb..b7e55d3473 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/memory/TraceMemoryRegisterSpace.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/memory/TraceMemoryRegisterSpace.java @@ -19,8 +19,6 @@ import java.nio.ByteBuffer; import java.util.*; import java.util.Map.Entry; -import org.apache.commons.lang3.ArrayUtils; - import ghidra.program.model.lang.Register; import ghidra.program.model.lang.RegisterValue; import ghidra.trace.model.TraceAddressSnapRange; @@ -71,20 +69,13 @@ public interface TraceMemoryRegisterSpace extends TraceMemorySpace { } default RegisterValue getValue(long snap, Register register) { - int byteLength = TraceRegisterUtils.byteLengthOf(register); - byte[] mask = register.getBaseMask(); - ByteBuffer buf = ByteBuffer.allocate(mask.length * 2); - buf.put(mask); - int maskOffset = TraceRegisterUtils.computeMaskOffset(mask); - int startVal = buf.position() + maskOffset; - buf.position(startVal); - buf.limit(buf.position() + byteLength); - getBytes(snap, register.getAddress(), buf); - byte[] arr = buf.array(); - if (!register.isBigEndian()) { - ArrayUtils.reverse(arr, startVal, startVal + byteLength); - } - return new RegisterValue(register, arr); + return TraceRegisterUtils.getRegisterValue(register, + (a, buf) -> getBytes(snap, a, buf)); + } + + default RegisterValue getViewValue(long snap, Register register) { + return TraceRegisterUtils.getRegisterValue(register, + (a, buf) -> getViewBytes(snap, a, buf)); } default int getBytes(long snap, Register register, ByteBuffer buf) { diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/program/TraceProgramView.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/program/TraceProgramView.java index 715ff7b6e2..e4ba9b8aea 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/program/TraceProgramView.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/program/TraceProgramView.java @@ -18,9 +18,10 @@ package ghidra.trace.model.program; import ghidra.program.model.listing.Program; import ghidra.trace.model.Trace; import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.util.TraceTimeViewport; /** - * View of a trace at a particular time,as a program + * View of a trace at a particular time, as a program */ public interface TraceProgramView extends Program { /** @@ -37,6 +38,13 @@ public interface TraceProgramView extends Program { */ long getSnap(); + /** + * Get the viewport this view is using for forked queries + * + * @return the viewport + */ + TraceTimeViewport getViewport(); + /** * Get the latest snap * diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/symbol/TraceReferenceOperations.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/symbol/TraceReferenceOperations.java index 40daa08781..7d924bd172 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/symbol/TraceReferenceOperations.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/symbol/TraceReferenceOperations.java @@ -66,7 +66,7 @@ public interface TraceReferenceOperations { TraceReference getPrimaryReferenceFrom(long snap, Address fromAddress, int operandIndex); - Collection getFlowRefrencesFrom(long snap, Address fromAddress); + Collection getFlowReferencesFrom(long snap, Address fromAddress); void clearReferencesFrom(Range span, AddressRange range); @@ -94,7 +94,7 @@ public interface TraceReferenceOperations { } default boolean hasFlowReferencesFrom(long snap, Address fromAddress) { - return !getFlowRefrencesFrom(snap, fromAddress).isEmpty(); + return !getFlowReferencesFrom(snap, fromAddress).isEmpty(); } default boolean hasReferencesTo(long snap, Address toAddress) { diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/TraceSchedule.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/TraceSchedule.java new file mode 100644 index 0000000000..6128ec14b2 --- /dev/null +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/TraceSchedule.java @@ -0,0 +1,1051 @@ +/* ### + * 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.trace.model.time; + +import java.util.*; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.StringUtils; + +import ghidra.pcode.emu.PcodeMachine; +import ghidra.pcode.emu.PcodeThread; +import ghidra.trace.model.Trace; +import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.model.thread.TraceThreadManager; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +public class TraceSchedule implements Comparable { + public static final TraceSchedule ZERO = TraceSchedule.snap(0); + + public static final TraceSchedule snap(long snap) { + return new TraceSchedule(snap, new TickSequence(), new TickSequence()); + } + + /** + * The result of a rich comparison of two schedules (or parts thereof) + */ + public enum CompareResult { + UNREL_LT(-1, false), + REL_LT(-1, true), + EQUALS(0, true), + REL_GT(1, true), + UNREL_GT(1, false); + + /** + * Enrich the result of {@link Comparable#compareTo(Object)}, given that the two are related + * + * @param compareTo the return from {@code compareTo} + * @return the rich result + */ + public static CompareResult related(int compareTo) { + if (compareTo < 0) { + return REL_LT; + } + if (compareTo > 0) { + return REL_GT; + } + return EQUALS; + } + + /** + * Enrich the result of {@link Comparable#compareTo(Object)}, given that the two are not + * related + * + * @param compareTo the return from {@code compareTo} + * @return the rich result + */ + public static CompareResult unrelated(int compareTo) { + if (compareTo < 0) { + return UNREL_LT; + } + if (compareTo > 0) { + return UNREL_GT; + } + return EQUALS; + } + + /** + * Maintain sort order, but specify the two are not in fact related + * + * @param result the result of another (usually recursive) rich comparison + * @return the modified result + */ + public static CompareResult unrelated(CompareResult result) { + return unrelated(result.compareTo); + } + + public final int compareTo; + public final boolean related; + + CompareResult(int compareTo, boolean related) { + this.compareTo = compareTo; + this.related = related; + } + } + + /** + * A step of a given thread in a schedule, repeated some number of times + */ + public static class TickStep implements Comparable { + + /** + * Parse a step of the form "{@code 3}" or {@code "t1-3"} + * + *

+ * The first form steps the last thread the given number of times, e.g., 3. The second form + * steps the given thread, e.g., 1, the given number of times. + * + * @param stepSpec the string specification + * @return the parsed step + * @throws IllegalArgumentException if the specification is of the wrong form + */ + public static TickStep parse(String stepSpec) { + if ("".equals(stepSpec)) { + return new TickStep(-1, 0); + } + String[] parts = stepSpec.split("-"); + if (parts.length == 1) { + return new TickStep(-1, Long.parseLong(parts[0].trim())); + } + if (parts.length == 2) { + String tPart = parts[0].trim(); + if (tPart.startsWith("t")) { + return new TickStep(Long.parseLong(tPart.substring(1)), + Long.parseLong(parts[1].trim())); + } + } + throw new IllegalArgumentException("Cannot parse step: '" + stepSpec + "'"); + } + + protected final long threadKey; + protected long tickCount; + + /** + * Construct a step for the given thread with the given tick count + * + * @param threadKey the key of the thread in the trace, -1 for the "last thread" + * @param tickCount the number of times to step the thread + */ + public TickStep(long threadKey, long tickCount) { + if (tickCount < 0) { + throw new IllegalArgumentException("Cannot step a negative number"); + } + this.threadKey = threadKey; + this.tickCount = tickCount; + } + + @Override + public String toString() { + if (threadKey == -1) { + return Long.toString(tickCount); + } + return String.format("t%d-%d", threadKey, tickCount); + } + + @Override + public TickStep clone() { + return new TickStep(threadKey, tickCount); + } + + /** + * Add to the count of this step + * + * @param steps the count to add + */ + public void advance(long steps) { + if (steps < 0) { + throw new IllegalArgumentException("Cannot advance a negative number"); + } + long newCount = tickCount + steps; + if (newCount < 0) { + throw new IllegalArgumentException("Total step count exceeds LONG_MAX"); + } + this.tickCount = newCount; + } + + /** + * Subtract from the count of this step + * + *

+ * If this step has a count exceeding that given, then this method simply subtracts the + * given number from the {@code tickCount} and returns the (negative) difference. If this + * step has exactly the count given, this method sets the count to 0 and returns 0, + * indicating this step should be removed from the sequence. If the given count exceeds that + * of this step, this method sets the count to 0 and returns the (positive) difference, + * indicating this step should be removed from the sequence, and the remaining steps rewound + * from the preceding step. + * + * @param steps the count to rewind + * @return the number of steps remaining + */ + public long rewind(long steps) { + if (steps < 0) { + throw new IllegalArgumentException("Cannot rewind a negative number"); + } + long diff = this.tickCount - steps; + this.tickCount = Long.max(0, diff); + return -diff; + } + + /** + * Check if the given step can be combined with this one + * + *

+ * Two steps applied to the same thread can just be summed. If the given step applies to the + * "last thread" or to the same thread as this step, then it can be combined. + * + * @param step the second step + * @return true if combinable, false otherwise. + */ + public boolean canCombine(TickStep step) { + return this.threadKey == step.threadKey || step.threadKey == -1; + } + + @Override + public int hashCode() { + return Long.hashCode(threadKey) * 31 + Long.hashCode(tickCount); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof TickStep)) { + return false; + } + TickStep that = (TickStep) obj; + if (this.threadKey != that.threadKey) { + return false; + } + if (this.tickCount != that.tickCount) { + return false; + } + return true; + } + + @Override + public int compareTo(TickStep that) { + return compareStep(that).compareTo; + } + + /** + * Richly compare this step to another + * + * @param that the object of comparison (this being the subject) + * @return a result describing the relationship from subject to object + */ + public CompareResult compareStep(TickStep that) { + CompareResult result; + + result = CompareResult.unrelated(Long.compare(this.threadKey, that.threadKey)); + if (result != CompareResult.EQUALS) { + return result; + } + + result = CompareResult.related(Long.compare(this.tickCount, that.tickCount)); + if (result != CompareResult.EQUALS) { + return result; + } + + return CompareResult.EQUALS; + } + } + + /** + * A sequence of thread steps, each repeated some number of times + */ + public static class TickSequence implements Comparable { + + /** + * Parse (and normalize) a sequence of steps + * + *

+ * This takes a comma-separated list of steps in the form specified by + * {@link TickStep#parse(String)}. Each step may or may not specify a thread, but it's + * uncommon for any but the first step to omit the thread. The sequence is normalized as it + * is parsed, so any step after the first that omits a thread will be combined with the + * previous step. When the first step applies to the "last thread," it typically means the + * "event thread" of the source trace snapshot. + * + * @param seqSpec the string specification of the sequence + * @return the parsed sequence + * @throws IllegalArgumentException if the specification is of the wrong form + */ + public static TickSequence parse(String seqSpec) { + TickSequence result = new TickSequence(); + for (String stepSpec : seqSpec.split(",")) { + TickStep step = TickStep.parse(stepSpec); + result.advance(step); + } + return result; + } + + /** + * Construct (and normalize) a sequence of the specified steps + * + * @param steps the desired steps in order + * @return the resulting sequence + */ + public static TickSequence of(TickStep... steps) { + return of(Arrays.asList(steps)); + } + + /** + * Construct (and normalize) a sequence of the specified steps + * + * @param steps the desired steps in order + * @return the resulting sequence + */ + public static TickSequence of(List steps) { + TickSequence result = new TickSequence(); + for (TickStep step : steps) { + result.advance(step); + } + return result; + } + + /** + * Construct (and normalize) a sequence formed by the steps in a followed by the steps in b + * + * @param a the first sequence + * @param b the second (appended) sequence + * @return the resulting sequence + */ + public static TickSequence catenate(TickSequence a, TickSequence b) { + TickSequence result = new TickSequence(); + result.advance(a); + result.advance(b); + return result; + } + + private final List steps; + + protected TickSequence() { + this(new ArrayList<>()); + } + + protected TickSequence(List steps) { + this.steps = steps; + } + + @Override + public String toString() { + return StringUtils.join(steps, ','); + } + + /** + * Append the given step to this sequence + * + * @param step the step to append + */ + public void advance(TickStep step) { + if (step.tickCount == 0) { + return; + } + if (steps.isEmpty()) { + steps.add(step); + return; + } + TickStep last = steps.get(steps.size() - 1); + if (!last.canCombine(step)) { + steps.add(step.clone()); + return; + } + last.advance(step.tickCount); + } + + /** + * Append the given sequence to this one + * + * @param seq the sequence to append + */ + public void advance(TickSequence seq) { + int size = seq.steps.size(); + // Clone early in case seq == this + // I should store copies of subsequent steps, anyway + List clone = seq.steps.stream() + .map(TickStep::clone) + .collect(Collectors.toList()); + if (size < 1) { + return; + } + // intervening -1 could resolve and be combined with following + advance(clone.get(0)); + if (size < 2) { + return; + } + advance(clone.get(1)); + steps.addAll(clone.subList(2, size)); + } + + /** + * Rewind this sequence the given step count + * + *

+ * This modifies the sequence in place, removing the given count from the end of the + * sequence. Any step whose count is reduced to 0 as a result of rewinding is removed + * entirely from the sequence. + * + * @param count the step count to rewind + * @return if count exceeds the steps of this sequence, the (positive) difference remaining + */ + public long rewind(long count) { + if (count < 0) { + throw new IllegalArgumentException("Cannot rewind a negative number"); + } + while (!steps.isEmpty()) { + int lastIndex = steps.size() - 1; + count = steps.get(lastIndex).rewind(count); + if (count >= 0) { + steps.remove(lastIndex); + } + if (count <= 0) { + break; + } + } + return Long.max(0, count); + } + + @Override + public TickSequence clone() { + return new TickSequence( + steps.stream().map(TickStep::clone).collect(Collectors.toList())); + } + + /** + * Obtain a clone of the steps + * + *

+ * Modifications to the returned steps have no effect on this sequence. + * + * @return the cloned steps + */ + public List getSteps() { + return steps.stream().map(TickStep::clone).collect(Collectors.toUnmodifiableList()); + } + + /** + * Check if this sequence represents any actions + * + * @return true if the sequence is empty, false if not + */ + public boolean isNop() { + return steps.isEmpty(); + } + + @Override + public int hashCode() { + return steps.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof TickSequence)) { + return false; + } + TickSequence that = (TickSequence) obj; + return Objects.equals(this.steps, that.steps); + } + + /** + * Richly compare to sequences + * + *

+ * The result indicates not only which is "less" or "greater" than the other, but also + * indicates whether the two are "related." Two sequences are considered related if one is + * the prefix to the other. More precisely, they are related if it's possible to transform + * one into the other solely by truncation (rewind) or solely by concatenation (advance). + * When related, the prefix is considered "less than" the other. Equal sequences are + * trivially related. + * + *

+ * Examples: + *

    + *
  • {@code ""} is related to and less than {@code "10"}
  • + *
  • {@code "10"} is related and equal to {@code "10"}
  • + *
  • {@code "10"} is related to and less than {@code "11"}
  • + *
  • {@code "t1-5"} is related to and less than {@code "t1-5,t2-4"}
  • + *
  • {@code "t1-5"} is un-related to and less than {@code "t1-4,t2-4"}
  • + *
+ * + *

+ * The {@link #compareTo(TickSequence)} implementation defers to this method. Thus, in a + * sorted set of tick sequences, the floor of a given sequence is will be the longest prefix + * in that set to the given sequence, assuming such a prefix is present. + * + * @param that the object of comparison (this being the subject) + * @return a result describing the relationship from subject to object + */ + public CompareResult compareSeq(TickSequence that) { + int min = Math.min(this.steps.size(), that.steps.size()); + CompareResult result; + for (int i = 0; i < min; i++) { + TickStep s1 = this.steps.get(i); + TickStep s2 = that.steps.get(i); + result = s1.compareStep(s2); + switch (result) { + case UNREL_LT: + case UNREL_GT: + return result; + case REL_LT: + if (i + 1 == this.steps.size()) { + return CompareResult.REL_LT; + } + else { + return CompareResult.UNREL_LT; + } + case REL_GT: + if (i + 1 == that.steps.size()) { + return CompareResult.REL_GT; + } + else { + return CompareResult.UNREL_GT; + } + default: // EQUALS, next step + } + } + if (that.steps.size() > min) { + return CompareResult.REL_LT; + } + if (this.steps.size() > min) { + return CompareResult.REL_GT; + } + return CompareResult.EQUALS; + } + + @Override + public int compareTo(TickSequence that) { + return compareSeq(that).compareTo; + } + + /** + * Compute the sequence which concatenated to the given prefix would result in this sequence + * + *

+ * The returned tick sequence should not be manipulated, since it may just be this sequence. + * + * @see #compareSeq(TickSequence) + * @param prefix the prefix + * @return the relative sequence from prefix to this + * @throws IllegalArgumentException if prefix is not a prefix of this sequence + */ + public TickSequence relativize(TickSequence prefix) { + if (prefix.isNop()) { + return this; + } + CompareResult comp = compareSeq(prefix); + TickSequence result = new TickSequence(); + if (comp == CompareResult.EQUALS) { + return result; + } + if (comp != CompareResult.REL_GT) { + throw new IllegalArgumentException(String.format( + "The given prefix (%s) is not actually a prefix of this (%s).", prefix, this)); + } + + int lastStepIndex = prefix.steps.size() - 1; + TickStep ancestorLast = prefix.steps.get(lastStepIndex); + TickStep continuation = this.steps.get(lastStepIndex); + long toFinish = continuation.tickCount - ancestorLast.tickCount; + if (toFinish > 0) { + result.advance(new TickStep(ancestorLast.threadKey, toFinish)); + } + result.steps.addAll(steps.subList(prefix.steps.size(), steps.size())); + return result; + } + + /** + * Compute to total number of steps specified + * + * @return the total + */ + public long totalTickCount() { + long count = 0; + for (TickStep step : steps) { + count += step.tickCount; + } + return count; + } + + /** + * Execute this sequence upon the given machine + * + *

+ * Threads are retrieved from the database by key, then created in the machine (if not + * already present) named by {@link TraceThread#getPath()}. The caller should ensure the + * machine's state is bound to the given trace. + * + * @param trace the trace to which the machine is bound + * @param eventThread the thread for the first step, if it applies to the "last thread" + * @param machine the machine to step + * @param action the action to step each thread + * @param monitor a monitor for cancellation and progress reports + * @return the last trace thread stepped during execution + * @throws CancelledException if execution is cancelled + */ + public TraceThread execute(Trace trace, TraceThread eventThread, PcodeMachine machine, + Consumer> action, TaskMonitor monitor) throws CancelledException { + TraceThreadManager tm = trace.getThreadManager(); + TraceThread thread = eventThread; + for (TickStep step : steps) { + thread = step.threadKey == -1 ? eventThread : tm.getThread(step.threadKey); + if (thread == null) { + if (step.threadKey == -1) { + throw new IllegalArgumentException( + "Thread key -1 can only be used if last/event thread is given"); + } + throw new IllegalArgumentException( + "Thread with key " + step.threadKey + " does not exist in given trace"); + } + + PcodeThread emuThread = machine.getThread(thread.getPath(), true); + for (int i = 0; i < step.tickCount; i++) { + monitor.incrementProgress(1); + monitor.checkCanceled(); + action.accept(emuThread); + } + } + return thread; + } + + /** + * Get the key of the last thread stepped + * + * @return the key, or -1 if no step in the sequence specifies a thread + */ + public long getLastThreadKey() { + if (steps.isEmpty()) { + return -1; + } + return steps.get(steps.size() - 1).threadKey; + } + } + + private static final String PARSE_ERR_MSG = + "Time specification must have form 'snap[:ticks[.pTicks]]'"; + + /** + * Parse schedule in the form "{@code snap[:ticks[.pTicks]]}" + * + *

+ * A schedule consists of a snap, a optional sequence of thread instruction-level steps (ticks), + * and optional p-code-level steps ({@code pTicks}). The form of {@code ticks} and + * {@code pTicks} is specified by {@link TickSequence#parse(String)}. + * + * @param spec the string specification + * @return the parsed schedule + */ + public static TraceSchedule parse(String spec) { + String[] parts = spec.split(":"); + if (parts.length > 2) { + throw new IllegalArgumentException(PARSE_ERR_MSG); + } + final long snap; + final TickSequence ticks; + final TickSequence pTicks; + try { + snap = Long.decode(parts[0]); + } + catch (NumberFormatException e) { + throw new IllegalArgumentException(PARSE_ERR_MSG, e); + } + if (parts.length > 1) { + String[] subs = parts[1].split("\\."); + try { + ticks = TickSequence.parse(subs[0]); + } + catch (IllegalArgumentException e) { + throw new IllegalArgumentException(PARSE_ERR_MSG, e); + } + if (subs.length == 1) { + pTicks = new TickSequence(); + } + else if (subs.length == 2) { + try { + pTicks = TickSequence.parse(subs[1]); + } + catch (IllegalArgumentException e) { + throw new IllegalArgumentException(PARSE_ERR_MSG, e); + } + } + else { + throw new IllegalArgumentException(PARSE_ERR_MSG); + } + } + else { + ticks = new TickSequence(); + pTicks = new TickSequence(); + } + return new TraceSchedule(snap, ticks, pTicks); + } + + private final long snap; + private final TickSequence ticks; + private final TickSequence pTicks; + + /** + * Construct the given schedule + * + * @param snap the initial trace snapshot + * @param ticks the tick sequence + * @param pTicks the of p-code tick sequence + */ + public TraceSchedule(long snap, TickSequence ticks, TickSequence pTicks) { + this.snap = snap; + this.ticks = ticks; + this.pTicks = pTicks; + } + + @Override + public String toString() { + if (pTicks.isNop()) { + if (ticks.isNop()) { + return Long.toString(snap); + } + return String.format("%d:%s", snap, ticks); + } + return String.format("%d:%s.%s", snap, ticks, pTicks); + } + + /** + * Richly compare two schedules + * + *

+ * Schedules starting at different snapshots are never related, because there is no + * emulator/simulator stepping action which advances to the next snapshot. Though p-code steps + * may comprise a partial step, we do not consider a partial step to be a prefix of a full step, + * since we cannot know a priori how many p-code steps comprise a full instruction + * step. Consider, e.g., the user may specify 100 p-code steps, which could effect 20 + * instruction steps. + * + * @param that the object of comparison (this being the subject) + * @return a result describing the relationship from subject to object + */ + public CompareResult compareSchedule(TraceSchedule that) { + CompareResult result; + + result = CompareResult.unrelated(Long.compare(this.snap, that.snap)); + if (result != CompareResult.EQUALS) { + return result; + } + + result = this.ticks.compareSeq(that.ticks); + switch (result) { + case UNREL_LT: + case UNREL_GT: + return result; + case REL_LT: + if (this.pTicks.isNop()) { + return CompareResult.REL_LT; + } + else { + return CompareResult.UNREL_LT; + } + case REL_GT: + if (that.pTicks.isNop()) { + return CompareResult.REL_GT; + } + else { + return CompareResult.UNREL_GT; + } + default: // EQUALS, compare pTicks + } + + result = this.pTicks.compareSeq(that.pTicks); + if (result != CompareResult.EQUALS) { + return result; + } + + return CompareResult.EQUALS; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof TraceSchedule)) { + return false; + } + TraceSchedule that = (TraceSchedule) obj; + if (this.snap != that.snap) { + return false; + } + if (!Objects.equals(this.ticks, that.ticks)) { + return false; + } + if (!Objects.equals(this.pTicks, that.pTicks)) { + return false; + } + return true; + } + + @Override + public int hashCode() { + return Objects.hash(snap, ticks, pTicks); + } + + @Override + public int compareTo(TraceSchedule o) { + return compareSchedule(o).compareTo; + } + + /** + * Check if this schedule requires any stepping + * + * @return true if no stepping is required, i.e., the resulting state can be realized simply by + * loading a snapshot + */ + public boolean isSnapOnly() { + return ticks.isNop() && pTicks.isNop(); + } + + /** + * Get the source snapshot + * + * @return + */ + public long getSnap() { + return snap; + } + + /** + * Get the last thread key stepped by this schedule + * + * @return + */ + public long getLastThreadKey() { + long last = pTicks.getLastThreadKey(); + if (last != -1) { + return last; + } + return ticks.getLastThreadKey(); + } + + /** + * Get the event thread for this schedule in the context of the given trace + * + *

+ * This is the thread stepped when no thread is specified for the first step of the sequence. + * + * @param trace the trace containing the source snapshot and threads + * @return the thread to use as "last thread" for the sequence + */ + public TraceThread getEventThread(Trace trace) { + TraceSnapshot snapshot = trace.getTimeManager().getSnapshot(snap, false); + return snapshot == null ? null : snapshot.getEventThread(); + } + + /** + * Get the last thread stepped by this schedule in the context of the given trace + * + * @param trace the trace containing the source snapshot and threads + * @return the thread last stepped, or the "event thread" when no steps are taken + */ + public TraceThread getLastThread(Trace trace) { + long lastKey = getLastThreadKey(); + if (lastKey == -1) { + return getEventThread(trace); + } + return trace.getThreadManager().getThread(lastKey); + } + + /** + * Compute the total number of ticks taken, including the p-code ticks + * + *

+ * This is suitable for use with {@link TaskMonitor#initialize(long)}, where that monitor will + * be passed to {@link #execute(Trace, PcodeMachine, TaskMonitor)} or similar. + * + * @return the number of ticks + */ + public long totalTickCount() { + return ticks.totalTickCount() + pTicks.totalTickCount(); + } + + /** + * Compute the number of ticks taken, excluding p-code ticks + * + * @return the number of ticks + */ + public long tickCount() { + return ticks.totalTickCount(); + } + + /** + * Compute the number of p-code ticks taken + * + * @return the number of ticks + */ + public long pTickCount() { + return pTicks.totalTickCount(); + } + + /** + * Realize the machine state for this schedule using the given trace and machine + * + *

+ * This method executes this schedule and trailing p-code steps on the given machine, assuming + * that machine is already "positioned" at the initial snapshot. Assuming successful execution, + * that machine is now said to be "positioned" at this schedule, and its state is the result of + * said execution. + * + * @param trace the trace containing the source snapshot and threads + * @param machine a machine bound to the trace whose current state reflects the initial snapshot + * @param monitor a monitor for cancellation and progress reporting + * @throws CancelledException if the execution is cancelled + */ + public void execute(Trace trace, PcodeMachine machine, TaskMonitor monitor) + throws CancelledException { + TraceThread lastThread = getEventThread(trace); + lastThread = + ticks.execute(trace, lastThread, machine, PcodeThread::stepInstruction, monitor); + lastThread = + pTicks.execute(trace, lastThread, machine, PcodeThread::stepPcodeOp, monitor); + } + + /** + * Realize the machine state for this schedule using the given trace and pre-positioned machine + * + *

+ * This method executes the remaining steps of this schedule and trailing p-code steps on the + * given machine, assuming that machine is already "positioned" at another given schedule. + * Assuming successful execution, that machine is now said to be "positioned" at this schedule, + * and its state is the result of said execution. + * + * @param trace the trace containing the source snapshot and threads + * @param position the current schedule of the given machine + * @param machine a machine bound to the trace whose current state reflects the given position + * @param monitor a monitor for cancellation and progress reporting + * @throws CancelledException if the execution is cancelled + * @throws IllegalArgumentException if the given position is not a prefix of this schedule + */ + public void finish(Trace trace, TraceSchedule position, PcodeMachine machine, + TaskMonitor monitor) throws CancelledException { + TraceThread lastThread = position.getLastThread(trace); + TickSequence remains = ticks.relativize(position.ticks); + if (remains.isNop()) { + TickSequence pRemains = this.pTicks.relativize(position.pTicks); + lastThread = + pRemains.execute(trace, lastThread, machine, PcodeThread::stepPcodeOp, monitor); + } + else { + lastThread = + remains.execute(trace, lastThread, machine, PcodeThread::stepInstruction, monitor); + lastThread = + pTicks.execute(trace, lastThread, machine, PcodeThread::stepPcodeOp, monitor); + } + } + + /** + * Returns the equivalent of executing the schedule (ignoring p-code steps) followed by stepping + * the given thread count more instructions + * + *

+ * This schedule is left unmodified. If it had any p-code steps, those steps are dropped in the + * resulting schedule. + * + * @param thread the thread to step + * @param tickCount the number of ticks to take the thread forward + * @return the resulting schedule + */ + public TraceSchedule steppedForward(TraceThread thread, long tickCount) { + TickSequence ticks = this.ticks.clone(); + ticks.advance(new TickStep(thread.getKey(), tickCount)); + return new TraceSchedule(snap, ticks, new TickSequence()); + } + + protected TraceSchedule doSteppedBackward(Trace trace, long tickCount, Set visited) { + if (!visited.add(snap)) { + return null; + } + long excess = tickCount - totalTickCount(); + if (excess > 0) { + if (trace == null) { + return null; + } + TraceSnapshot source = trace.getTimeManager().getSnapshot(snap, false); + if (source == null) { + return null; + } + TraceSchedule rec = source.getSchedule(); + if (rec == null) { + return null; + } + return rec.doSteppedBackward(trace, excess, visited); + } + TickSequence ticks = this.ticks.clone(); + ticks.rewind(tickCount); + return new TraceSchedule(snap, ticks, new TickSequence()); + } + + /** + * Returns the equivalent of executing count instructions (and all p-code operations) less than + * this schedule + * + *

+ * This schedule is left unmodified. If it had any p-code steps, those steps are dropped in the + * resulting schedule. If count exceeds this schedule's steps, it will try (recursively) to step + * the source snapshot's schedule backward, if known. + * + * @param trace the trace of this schedule, for context + * @param tickCount the number of ticks to take backward + * @return the resulting schedule or null if it cannot be computed + */ + public TraceSchedule steppedBackward(Trace trace, long tickCount) { + return doSteppedBackward(trace, tickCount, new HashSet<>()); + } + + /** + * Returns the equivalent of executing the schedule followed by stepping the given thread + * {@code pTickCount} more p-code operations + * + * @param thread the thread to step + * @param pTickCount the number of p-code ticks to take the thread forward + * @return the resulting schedule + */ + public TraceSchedule steppedPcodeForward(TraceThread thread, int pTickCount) { + TickSequence pTicks = this.pTicks.clone(); + pTicks.advance(new TickStep(thread.getKey(), pTickCount)); + return new TraceSchedule(snap, ticks, pTicks); + } + + /** + * Returns the equivalent of executing count p-code operations less than this schedule + * + *

+ * If {@code pTickCount} exceeds the p-code ticks of this schedule, null is returned, since we + * cannot know a priori how many p-code steps would be required to complete the + * preceding instruction step. + * + * @param pTickCount the number of p-code ticks to take backward + * @return the resulting schedule or null if it cannot be computed + */ + public TraceSchedule steppedPcodeBackward(int pTickCount) { + if (pTickCount > pTicks.totalTickCount()) { + return null; + } + TickSequence pTicks = this.pTicks.clone(); + pTicks.rewind(pTickCount); + return new TraceSchedule(snap, ticks, pTicks); + } +} diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/TraceSnapshot.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/TraceSnapshot.java index 957ff37c22..d3dec4e322 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/TraceSnapshot.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/TraceSnapshot.java @@ -82,20 +82,37 @@ public interface TraceSnapshot { void setEventThread(TraceThread thread); /** - * Get the number of ticks, if known, between this snapshot and the next + * Get the schedule, if applicable and known, relating this snapshot to a previous one * - * @return the (unsigned) number of ticks, or 0 for unspecified. + *

+ * This information is not always known, or even applicable. If recording a single step, + * ideally, this is simply the previous snap plus one step of the event thread, e.g., for snap + * 6, the schedule would be "5:1". For an emulated machine cached in scratch space, this should + * be the schedule that would recover the same machine state. + * + *

+ * The object managers in the trace pay no heed to this schedule. In particular, when retrieving + * the "most-recent" information from a snapshot with a known schedule, the "previous snap" part + * of that schedule is not taken into account. In other words, the managers still + * interpret time linearly, even though this schedule field might imply built-in forking. + * + * @return the (possibly null) schedule */ - long getTicks(); + TraceSchedule getSchedule(); /** - * Set the number of ticks between this snapshot and the next + * Get the string representation of the schedule * - * Conventionally, 0 indicates unknown or unspecified - * - * @param ticks the number of ticks + * @return the (possibly empty) string representation of the schedule */ - void setTicks(long ticks); + String getScheduleString(); + + /** + * Set the schedule from some previous snapshot to this one + * + * @param schedule the schedule + */ + void setSchedule(TraceSchedule schedule); /** * Delete this snapshot diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/TraceTimeManager.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/TraceTimeManager.java index 0ea7d28cb2..fe525fc2e4 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/TraceTimeManager.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/TraceTimeManager.java @@ -34,6 +34,26 @@ public interface TraceTimeManager { */ TraceSnapshot getSnapshot(long snap, boolean createIfAbsent); + /** + * Get the most recent snapshot since a given key + * + * @param snap the snapshot key + * @return the snapshot or {@code null} + */ + TraceSnapshot getMostRecentSnapshot(long snap); + + /** + * Get all snapshots with the given schedule + * + *

+ * Ideally, the snapshot schedules should be managed such that the returned collection contains + * at most one snapshot. + * + * @param schedule the schedule to find + * @return the snapshot, or {@code null} if no such snapshot exists + */ + Collection getSnapshotsWithSchedule(TraceSchedule schedule); + /** * List all snapshots in the trace * @@ -41,6 +61,18 @@ public interface TraceTimeManager { */ Collection getAllSnapshots(); + /** + * List all snapshots between two given snaps in the trace + * + * @param fromSnap the starting snap + * @param fromInclusive whether to include the from snap + * @param toSnap the ending snap + * @param toInclusive when to include the to snap + * @return the set of snapshots + */ + Collection getSnapshots(long fromSnap, boolean fromInclusive, + long toSnap, boolean toInclusive); + /** * Get maximum snapshot key that has ever existed, usually that of the latest snapshot * diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/DataAdapterFromDataType.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/DataAdapterFromDataType.java index 6266c4d48f..0835dbea15 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/DataAdapterFromDataType.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/DataAdapterFromDataType.java @@ -21,6 +21,23 @@ import ghidra.program.model.listing.Data; import ghidra.program.model.scalar.Scalar; public interface DataAdapterFromDataType extends Data { + + default String doToString() { + StringBuilder builder = new StringBuilder(); + builder.append(getMnemonicString()); + String valueRepresentation = getDefaultValueRepresentation(); + if (valueRepresentation != null) { + builder.append(' '); + builder.append(valueRepresentation); + } + return builder.toString(); + } + + @Override + default String getMnemonicString() { + return getDataType().getMnemonic(this); + } + @Override default Address getAddress(int opIndex) { if (opIndex != 0) { diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/DataAdapterFromSettings.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/DataAdapterFromSettings.java new file mode 100644 index 0000000000..3f6bf417d8 --- /dev/null +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/DataAdapterFromSettings.java @@ -0,0 +1,54 @@ +/* ### + * 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.trace.util; + +import ghidra.docking.settings.SettingsDefinition; +import ghidra.program.model.data.DataType; +import ghidra.program.model.data.MutabilitySettingsDefinition; +import ghidra.program.model.listing.Data; + +public interface DataAdapterFromSettings extends Data { + + default T getSettingsDefinition( + Class settingsDefinitionClass) { + DataType dt = getBaseDataType(); + for (SettingsDefinition def : dt.getSettingsDefinitions()) { + if (settingsDefinitionClass.isAssignableFrom(def.getClass())) { + return settingsDefinitionClass.cast(def); + } + } + return null; + } + + default boolean hasMutability(int mutabilityType) { + MutabilitySettingsDefinition def = + getSettingsDefinition(MutabilitySettingsDefinition.class); + if (def != null) { + return def.getChoice(this) == mutabilityType; + } + return false; + } + + @Override + default boolean isConstant() { + return hasMutability(MutabilitySettingsDefinition.CONSTANT); + } + + @Override + default boolean isVolatile() { + return hasMutability(MutabilitySettingsDefinition.VOLATILE); + } +} diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/DataAdapterMinimal.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/DataAdapterMinimal.java new file mode 100644 index 0000000000..a96fd8d2ae --- /dev/null +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/DataAdapterMinimal.java @@ -0,0 +1,40 @@ +/* ### + * 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.trace.util; + +import ghidra.program.model.listing.Data; +import ghidra.program.model.symbol.Reference; + +public interface DataAdapterMinimal extends Data { + /** Operand index for data. Will always be zero */ + int DATA_OP_INDEX = 0; + int[] EMPTY_INT_ARRAY = new int[0]; + + default String getPrimarySymbolOrDynamicName() { + /** TODO: Use primary symbol or dynamic name as in {@link DataDB#getPathName()} */ + return "DAT_" + getAddressString(false, false); + } + + @Override + default int getNumOperands() { + return 1; + } + + @Override + default Reference[] getValueReferences() { + return getOperandReferences(DATA_OP_INDEX); + } +} diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/DefaultTraceTimeViewport.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/DefaultTraceTimeViewport.java new file mode 100644 index 0000000000..614d688e9d --- /dev/null +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/DefaultTraceTimeViewport.java @@ -0,0 +1,277 @@ +/* ### + * 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.trace.util; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +import com.google.common.collect.*; + +import ghidra.program.model.address.*; +import ghidra.trace.model.Trace; +import ghidra.trace.model.Trace.TraceSnapshotChangeType; +import ghidra.trace.model.TraceDomainObjectListener; +import ghidra.trace.model.program.TraceProgramView; +import ghidra.trace.model.time.*; +import ghidra.util.*; +import ghidra.util.datastruct.ListenerSet; + +/** + * Computes and tracks the "viewport" resulting from forking patterns encoded in snapshot schedules + * + *

+ * This is used primarily by the {@link TraceProgramView} implementation to resolve most-recent + * objects according to a layering or forking structure given in snapshot schedules. This listens on + * the given trace for changes in snapshot schedules and keeps an up-to-date set of visible (or + * potentially-visible) ranges from the given snap. + * + *

+ * TODO: Because complicated forking structures are not anticipated, some minimal effort is given to + * cull meaningless changes, but in general, changes cause a complete re-computation of the + * viewport. If complex, deep forking structures prove to be desirable, then this is an area for + * optimization. + */ +public class DefaultTraceTimeViewport implements TraceTimeViewport { + protected class ForSnapshotsListener extends TraceDomainObjectListener { + { + listenFor(TraceSnapshotChangeType.ADDED, this::snapshotAdded); + listenFor(TraceSnapshotChangeType.CHANGED, this::snapshotChanged); + listenFor(TraceSnapshotChangeType.DELETED, this::snapshotDeleted); + } + + private void snapshotAdded(TraceSnapshot snapshot) { + if (snapshot.getSchedule() == null) { + return; + } + if (spanSet.contains(snapshot.getKey())) { + recomputeSnapRanges(); + return; + } + } + + private void snapshotChanged(TraceSnapshot snapshot) { + if (isLower(snapshot.getKey())) { + recomputeSnapRanges(); + return; + } + if (spanSet.contains(snapshot.getKey()) && snapshot.getSchedule() != null) { + recomputeSnapRanges(); + return; + } + } + + private void snapshotDeleted(TraceSnapshot snapshot) { + if (isLower(snapshot.getKey())) { + recomputeSnapRanges(); + return; + } + } + } + + protected final TraceTimeManager timeManager; + protected final List> ordered = new ArrayList<>(); + protected final RangeSet spanSet = TreeRangeSet.create(); + protected final ForSnapshotsListener listener = new ForSnapshotsListener(); + protected final ListenerSet changeListeners = new ListenerSet<>(Runnable.class); + + protected long snap; + + public DefaultTraceTimeViewport(Trace trace) { + this.timeManager = trace.getTimeManager(); + trace.addListener(listener); + } + + @Override + public void addChangeListener(Runnable l) { + changeListeners.add(l); + } + + @Override + public void removeChangeListener(Runnable l) { + changeListeners.remove(l); + } + + @Override + public boolean containsAnyUpper(Range range) { + // NB. This should only ever visit the first range intersecting that given + for (Range intersecting : spanSet.subRangeSet(range).asRanges()) { + if (range.contains(intersecting.upperEndpoint())) { + return true; + } + } + return false; + } + + @Override + public boolean isCompletelyVisible(AddressRange range, Range lifespan, T object, + Occlusion occlusion) { + for (Range rng : ordered) { + if (lifespan.contains(rng.upperEndpoint())) { + return true; + } + if (occlusion.occluded(object, range, rng)) { + return false; + } + } + return false; + } + + @Override + public AddressSet computeVisibleParts(AddressSetView set, Range lifespan, T object, + Occlusion occlusion) { + if (!containsAnyUpper(lifespan)) { + return new AddressSet(); + } + AddressSet remains = new AddressSet(set); + for (Range rng : ordered) { + if (lifespan.contains(rng.upperEndpoint())) { + return remains; + } + occlusion.remove(object, remains, rng); + if (remains.isEmpty()) { + return remains; + } + } + // This condition should have been detected by !containsAnyUpper + throw new AssertionError(); + } + + protected boolean isLower(long lower) { + Range range = spanSet.rangeContaining(lower); + if (range == null) { + return false; + } + return range.lowerEndpoint().longValue() == lower; + } + + protected boolean addSnapRange(long lower, long upper) { + if (spanSet.contains(lower)) { + return false; + } + Range range = Range.closed(lower, upper); + spanSet.add(range); + ordered.add(range); + return true; + } + + protected TraceSnapshot locateMostRecentFork(long from) { + while (true) { + TraceSnapshot prev = timeManager.getMostRecentSnapshot(from); + if (prev == null) { + return null; + } + TraceSchedule prevSched = prev.getSchedule(); + long prevKey = prev.getKey(); + if (prevSched == null) { + if (prevKey == Long.MIN_VALUE) { + return null; + } + from = prevKey - 1; + continue; + } + long forkedSnap = prevSched.getSnap(); + if (forkedSnap == prevKey - 1) { + // Schedule is notational without forking + from--; + continue; + } + return prev; + } + } + + protected void traverseAndAddForkRanges(long curSnap) { + while (true) { + TraceSnapshot fork = locateMostRecentFork(curSnap); + long prevSnap = fork == null ? Long.MIN_VALUE : fork.getKey(); + if (!addSnapRange(prevSnap, curSnap)) { + return; + } + if (fork == null) { + return; + } + curSnap = fork.getSchedule().getSnap(); + } + } + + protected void recomputeSnapRanges() { + spanSet.clear(); + ordered.clear(); + traverseAndAddForkRanges(snap); + assert !ordered.isEmpty(); + changeListeners.fire.run(); + } + + public void setSnap(long snap) { + this.snap = snap; + recomputeSnapRanges(); + } + + @Override + public boolean isForked() { + return ordered.size() > 1; + } + + @Override + public List getOrderedSnaps() { + return ordered + .stream() + .map(Range::upperEndpoint) + .collect(Collectors.toList()); + } + + @Override + public List getReversedSnaps() { + return Lists.reverse(ordered) + .stream() + .map(Range::upperEndpoint) + .collect(Collectors.toList()); + } + + @Override + public T getTop(Function func) { + for (Range rng : ordered) { + T t = func.apply(rng.upperEndpoint()); + if (t != null) { + return t; + } + } + return null; + } + + @Override + public Iterator mergedIterator(Function> iterFunc, + Comparator comparator) { + if (!isForked()) { + return iterFunc.apply(snap); + } + List> iters = ordered.stream() + .map(rng -> iterFunc.apply(rng.upperEndpoint())) + .collect(Collectors.toList()); + return new UniqIterator<>(new MergeSortingIterator<>(iters, comparator)); + } + + @Override + public AddressSetView unionedAddresses(Function viewFunc) { + if (!isForked()) { + return viewFunc.apply(snap); + } + List views = ordered.stream() + .map(rng -> viewFunc.apply(rng.upperEndpoint())) + .collect(Collectors.toList()); + return new UnionAddressSetView(views); + } +} diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/MemoryAdapter.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/MemoryAdapter.java new file mode 100644 index 0000000000..0da4f49227 --- /dev/null +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/MemoryAdapter.java @@ -0,0 +1,189 @@ +/* ### + * 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.trace.util; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import ghidra.program.model.address.Address; +import ghidra.program.model.mem.Memory; +import ghidra.program.model.mem.MemoryAccessException; + +public interface MemoryAdapter extends Memory { + + default ByteBuffer mustRead(Address addr, int length, boolean bigEndian) + throws MemoryAccessException { + ByteBuffer buf = ByteBuffer.allocate(length); + if (getBytes(addr, buf.array()) != length) { + throw new MemoryAccessException(); + } + buf.order(bigEndian ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN); + return buf; + } + + @Override + default int getBytes(Address addr, byte[] dest) throws MemoryAccessException { + return getBytes(addr, dest, 0, dest.length); + } + + @Override + default byte getByte(Address addr) throws MemoryAccessException { + return mustRead(addr, Byte.BYTES, true).get(0); + } + + @Override + default short getShort(Address addr) throws MemoryAccessException { + return mustRead(addr, Short.BYTES, true).getShort(0); + } + + @Override + default short getShort(Address addr, boolean bigEndian) throws MemoryAccessException { + return mustRead(addr, Short.BYTES, bigEndian).getShort(0); + } + + @Override + default int getShorts(Address addr, short[] dest) throws MemoryAccessException { + return getShorts(addr, dest, 0, dest.length, true); + } + + @Override + default int getShorts(Address addr, short[] dest, int dIndex, int nElem) + throws MemoryAccessException { + return getShorts(addr, dest, dIndex, nElem, true); + } + + @Override + default int getShorts(Address addr, short[] dest, int dIndex, int nElem, boolean bigEndian) + throws MemoryAccessException { + ByteBuffer buf = ByteBuffer.allocate(Short.BYTES * nElem); + int got = getBytes(addr, buf.array()) / Short.BYTES; + buf.order(bigEndian ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN); + buf.asShortBuffer().get(dest, dIndex, got); + return got; + } + + @Override + default int getInt(Address addr) throws MemoryAccessException { + return mustRead(addr, Integer.BYTES, true).getInt(0); + } + + @Override + default int getInt(Address addr, boolean bigEndian) throws MemoryAccessException { + return mustRead(addr, Integer.BYTES, bigEndian).getInt(0); + } + + @Override + default int getInts(Address addr, int[] dest) throws MemoryAccessException { + return getInts(addr, dest, 0, dest.length, true); + } + + @Override + default int getInts(Address addr, int[] dest, int dIndex, int nElem) + throws MemoryAccessException { + return getInts(addr, dest, dIndex, nElem, true); + } + + @Override + default int getInts(Address addr, int[] dest, int dIndex, int nElem, boolean bigEndian) + throws MemoryAccessException { + ByteBuffer buf = ByteBuffer.allocate(Integer.BYTES * nElem); + int got = getBytes(addr, buf.array()) / Integer.BYTES; + buf.order(bigEndian ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN); + buf.asIntBuffer().get(dest, dIndex, got); + return got; + } + + @Override + default long getLong(Address addr) throws MemoryAccessException { + return mustRead(addr, Long.BYTES, true).getLong(0); + } + + @Override + default long getLong(Address addr, boolean bigEndian) throws MemoryAccessException { + return mustRead(addr, Long.BYTES, bigEndian).getLong(0); + } + + @Override + default int getLongs(Address addr, long[] dest) throws MemoryAccessException { + return getLongs(addr, dest, 0, dest.length, true); + } + + @Override + default int getLongs(Address addr, long[] dest, int dIndex, int nElem) + throws MemoryAccessException { + return getLongs(addr, dest, dIndex, nElem, true); + } + + @Override + default int getLongs(Address addr, long[] dest, int dIndex, int nElem, boolean bigEndian) + throws MemoryAccessException { + ByteBuffer buf = ByteBuffer.allocate(Long.BYTES * nElem); + int got = getBytes(addr, buf.array()) / Long.BYTES; + buf.order(bigEndian ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN); + buf.asLongBuffer().get(dest, dIndex, got); + return got; + } + + @Override + default void setBytes(Address addr, byte[] source) throws MemoryAccessException { + setBytes(addr, source, 0, source.length); + } + + @Override + default void setByte(Address addr, byte value) throws MemoryAccessException { + setBytes(addr, new byte[] { value }); + } + + @Override + default void setShort(Address addr, short value) throws MemoryAccessException { + setShort(addr, value, true); + } + + @Override + default void setShort(Address addr, short value, boolean bigEndian) + throws MemoryAccessException { + ByteBuffer buf = ByteBuffer.allocate(Short.BYTES); + buf.order(bigEndian ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN); + buf.putShort(value); + setBytes(addr, buf.array()); + } + + @Override + default void setInt(Address addr, int value) throws MemoryAccessException { + setInt(addr, value, true); + } + + @Override + default void setInt(Address addr, int value, boolean bigEndian) throws MemoryAccessException { + ByteBuffer buf = ByteBuffer.allocate(Integer.BYTES); + buf.order(bigEndian ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN); + buf.putInt(value); + setBytes(addr, buf.array()); + } + + @Override + default void setLong(Address addr, long value) throws MemoryAccessException { + setLong(addr, value, true); + } + + @Override + default void setLong(Address addr, long value, boolean bigEndian) throws MemoryAccessException { + ByteBuffer buf = ByteBuffer.allocate(Long.BYTES); + buf.order(bigEndian ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN); + buf.putLong(value); + setBytes(addr, buf.array()); + } +} diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/TraceRegisterUtils.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/TraceRegisterUtils.java index 23130dc56e..43f4f6f30b 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/TraceRegisterUtils.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/TraceRegisterUtils.java @@ -17,6 +17,7 @@ package ghidra.trace.util; import java.nio.ByteBuffer; import java.util.Arrays; +import java.util.function.BiConsumer; import org.apache.commons.lang3.ArrayUtils; @@ -160,4 +161,22 @@ public enum TraceRegisterUtils { } return regs.getValue(snap, reg.getBaseRegister()).combineValues(rv); } + + public static RegisterValue getRegisterValue(Register register, + BiConsumer readAction) { + int byteLength = TraceRegisterUtils.byteLengthOf(register); + byte[] mask = register.getBaseMask(); + ByteBuffer buf = ByteBuffer.allocate(mask.length * 2); + buf.put(mask); + int maskOffset = TraceRegisterUtils.computeMaskOffset(mask); + int startVal = buf.position() + maskOffset; + buf.position(startVal); + buf.limit(buf.position() + byteLength); + readAction.accept(register.getAddress(), buf); + byte[] arr = buf.array(); + if (!register.isBigEndian()) { + ArrayUtils.reverse(arr, startVal, startVal + byteLength); + } + return new RegisterValue(register, arr); + } } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/TraceTimeViewport.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/TraceTimeViewport.java new file mode 100644 index 0000000000..5121d99725 --- /dev/null +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/TraceTimeViewport.java @@ -0,0 +1,211 @@ +/* ### + * 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.trace.util; + +import java.util.*; +import java.util.function.Function; + +import com.google.common.collect.Range; + +import ghidra.program.model.address.*; + +/** + * A convenience for tracking the time structure of a trace and querying the trace accordingly. + */ +public interface TraceTimeViewport { + + public interface Occlusion { + boolean occluded(T object, AddressRange range, Range span); + + void remove(T object, AddressSet remains, Range span); + } + + public interface QueryOcclusion extends Occlusion { + @Override + default boolean occluded(T object, AddressRange range, Range span) { + for (T found : query(range, span)) { + if (found == object) { + continue; + } + if (itemOccludes(range, found)) { + return true; + } + } + return false; + } + + @Override + default void remove(T object, AddressSet remains, Range span) { + // TODO: Split query by parts of remains? Probably not worth it. + for (T found : query( + new AddressRangeImpl(remains.getMinAddress(), remains.getMaxAddress()), span)) { + if (found == object) { + continue; + } + removeItem(remains, found); + if (remains.isEmpty()) { + return; + } + } + } + + Iterable query(AddressRange range, Range span); + + boolean itemOccludes(AddressRange range, T t); + + void removeItem(AddressSet remains, T t); + } + + public interface RangeQueryOcclusion extends QueryOcclusion { + @Override + default boolean itemOccludes(AddressRange range, T t) { + return range(t).intersects(range); + } + + @Override + default void removeItem(AddressSet remains, T t) { + remains.delete(range(t)); + } + + AddressRange range(T t); + } + + public interface SetQueryOcclusion extends QueryOcclusion { + @Override + default boolean itemOccludes(AddressRange range, T t) { + return set(t).intersects(range.getMinAddress(), range.getMaxAddress()); + } + + @Override + default void removeItem(AddressSet remains, T t) { + for (AddressRange range : set(t)) { + remains.delete(range); + if (remains.isEmpty()) { + return; + } + } + } + + AddressSetView set(T t); + } + + void addChangeListener(Runnable l); + + void removeChangeListener(Runnable l); + + /** + * Check if this view is forked + * + *

+ * The view is considered forked if any snap previous to this has a schedule with an initial + * snap other than the immediately-preceding one. Such forks "break" the linearity of the + * trace's usual time line. + * + * @return true if forked, false otherwise + */ + boolean isForked(); + + /** + * Check if the given lifespan contains any upper snap among the involved spans + * + * @param lifespan the lifespan to consider + * @return true if it contains any upper snap, false otherwise. + */ + boolean containsAnyUpper(Range lifespan); + + /** + * Check if any part of the given object is occluded by more-recent objects + * + * @param the type of the object + * @param range the address range of the object + * @param lifespan the lifespan of the object + * @param object optionally, the object to examine. Used to avoid "self occlusion" + * @param occlusion a mechanism for querying other like objects and checking for occlusion + * @return true if completely visible, false if even partially occluded + */ + boolean isCompletelyVisible(AddressRange range, Range lifespan, T object, + Occlusion occlusion); + + /** + * Compute the parts of a given object that are visible past more-recent objects + * + * @param the type of the object + * @param set the addresses comprising the object + * @param lifespan the lifespan of the object + * @param object the object to examine + * @param occlusion a mechanism for query other like objects and removing occluded parts + * @return the set of visible addresses + */ + AddressSet computeVisibleParts(AddressSetView set, Range lifespan, T object, + Occlusion occlusion); + + /** + * Get the snaps involved in the view in most-recent-first order + * + *

+ * The first is always this view's snap. Following are the source snaps of each previous + * snapshot's schedule where applicable. + * + * @return the list of snaps + */ + List getOrderedSnaps(); + + /** + * Get the snaps involved in the view in least-recent-first order + * + * @return the list of snaps + */ + List getReversedSnaps(); + + /** + * Get the first non-null result of the function, applied to the most-recent snaps first + * + *

+ * Typically, func both retrieves an object and tests for its suitability. + * + * @param the type of object to retrieve + * @param func the function on a snap to retrieve an object + * @return the first non-null result + */ + T getTop(Function func); + + /** + * Merge iterators from each involved snap into a single iterator + * + *

+ * Typically, the resulting iterator is passed through a filter to test each objects + * suitability. + * + * @param the type of objects in each iterator + * @param iterFunc a function on a snap to retrieve each iterator + * @param comparator the comparator for merging, which must yield the same order as each + * iterator + * @return the merged iterator + */ + Iterator mergedIterator(Function> iterFunc, + Comparator comparator); + + /** + * Union address sets from each involved snap + * + *

+ * The returned union is computed lazily. + * + * @param setFunc a function on a snap to retrieve the address set + * @return the union + */ + AddressSetView unionedAddresses(Function setFunc); +} diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/TraceViewportSpanIterator.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/TraceViewportSpanIterator.java new file mode 100644 index 0000000000..1d6f77f218 --- /dev/null +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/TraceViewportSpanIterator.java @@ -0,0 +1,82 @@ +/* ### + * 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.trace.util; + +import com.google.common.collect.*; + +import ghidra.trace.model.Trace; +import ghidra.trace.model.time.*; +import ghidra.util.AbstractPeekableIterator; + +public class TraceViewportSpanIterator extends AbstractPeekableIterator> { + private final TraceTimeManager timeManager; + private final RangeSet set = TreeRangeSet.create(); + private long snap; + private boolean done = false; + + public TraceViewportSpanIterator(Trace trace, long snap) { + this.timeManager = trace.getTimeManager(); + this.snap = snap; + } + + protected TraceSnapshot locateMostRecentFork(long from) { + while (true) { + TraceSnapshot prev = timeManager.getMostRecentSnapshot(from); + if (prev == null) { + return null; + } + TraceSchedule prevSched = prev.getSchedule(); + long prevKey = prev.getKey(); + if (prevSched == null) { + if (prevKey == Long.MIN_VALUE) { + return null; + } + from = prevKey - 1; + continue; + } + long forkedSnap = prevSched.getSnap(); + if (forkedSnap == prevKey - 1) { + // Schedule is notational without forking + from--; + continue; + } + return prev; + } + } + + @Override + protected Range seekNext() { + if (done) { + return null; + } + long curSnap = snap; + TraceSnapshot fork = locateMostRecentFork(snap); + long prevSnap = fork == null ? Long.MIN_VALUE : fork.getKey(); + if (fork == null) { + done = true; + } + else if (set.contains(prevSnap)) { + done = true; + return null; + } + else { + snap = fork.getSchedule().getSnap(); + } + Range range = Range.closed(prevSnap, curSnap); + set.add(range); + return range; + } +} diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/WrappingCodeUnitIterator.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/WrappingCodeUnitIterator.java new file mode 100644 index 0000000000..1f5dcb90bf --- /dev/null +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/WrappingCodeUnitIterator.java @@ -0,0 +1,44 @@ +/* ### + * 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.trace.util; + +import java.util.Iterator; + +import ghidra.program.model.listing.CodeUnit; +import ghidra.program.model.listing.CodeUnitIterator; + +public class WrappingCodeUnitIterator implements CodeUnitIterator { + protected final Iterator it; + + public WrappingCodeUnitIterator(Iterator it) { + this.it = it; + } + + @Override + public Iterator iterator() { + return this; + } + + @Override + public boolean hasNext() { + return it.hasNext(); + } + + @Override + public CodeUnit next() { + return it.next(); + } +} diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/WrappingDataIterator.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/WrappingDataIterator.java new file mode 100644 index 0000000000..e68de109a5 --- /dev/null +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/WrappingDataIterator.java @@ -0,0 +1,44 @@ +/* ### + * 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.trace.util; + +import java.util.Iterator; + +import ghidra.program.model.listing.Data; +import ghidra.program.model.listing.DataIterator; + +public class WrappingDataIterator implements DataIterator { + protected final Iterator it; + + public WrappingDataIterator(Iterator it) { + this.it = it; + } + + @Override + public Iterator iterator() { + return this; + } + + @Override + public boolean hasNext() { + return it.hasNext(); + } + + @Override + public Data next() { + return it.next(); + } +} diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/WrappingFunctionIterator.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/WrappingFunctionIterator.java new file mode 100644 index 0000000000..19981b7124 --- /dev/null +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/WrappingFunctionIterator.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.trace.util; + +import java.util.Iterator; + +import com.google.common.base.Predicate; +import com.google.common.collect.Iterators; + +import ghidra.program.model.listing.Function; +import ghidra.program.model.listing.FunctionIterator; + +public class WrappingFunctionIterator implements FunctionIterator { + private Iterator it; + + public WrappingFunctionIterator(Iterator it) { + this.it = it; + } + + public WrappingFunctionIterator(Iterator it, + Predicate filter) { + this.it = Iterators.filter(it, filter); + } + + @Override + public boolean hasNext() { + return it.hasNext(); + } + + @Override + public Function next() { + return it.next(); + } + + @Override + public Iterator iterator() { + return this; + } +} diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/WrappingInstructionIterator.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/WrappingInstructionIterator.java new file mode 100644 index 0000000000..b632c8034d --- /dev/null +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/WrappingInstructionIterator.java @@ -0,0 +1,44 @@ +/* ### + * 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.trace.util; + +import java.util.Iterator; + +import ghidra.program.model.listing.Instruction; +import ghidra.program.model.listing.InstructionIterator; + +public class WrappingInstructionIterator implements InstructionIterator { + protected final Iterator it; + + public WrappingInstructionIterator(Iterator it) { + this.it = it; + } + + @Override + public Iterator iterator() { + return this; + } + + @Override + public boolean hasNext() { + return it.hasNext(); + } + + @Override + public Instruction next() { + return it.next(); + } +} diff --git a/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/pcode/exec/trace/TracePcodeEmulatorTest.java b/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/pcode/exec/trace/TracePcodeEmulatorTest.java new file mode 100644 index 0000000000..eaae7cb2f0 --- /dev/null +++ b/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/pcode/exec/trace/TracePcodeEmulatorTest.java @@ -0,0 +1,641 @@ +/* ### + * 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 static org.junit.Assert.*; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodHandles.Lookup; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.util.List; + +import org.junit.Test; + +import com.google.common.collect.Range; + +import ghidra.app.plugin.assembler.Assembler; +import ghidra.app.plugin.assembler.Assemblers; +import ghidra.app.plugin.assembler.sleigh.sem.AssemblyPatternBlock; +import ghidra.app.plugin.processors.sleigh.SleighLanguage; +import ghidra.pcode.emu.PcodeThread; +import ghidra.pcode.exec.*; +import ghidra.program.model.lang.*; +import ghidra.test.AbstractGhidraHeadlessIntegrationTest; +import ghidra.trace.database.ToyDBTraceBuilder; +import ghidra.trace.model.memory.TraceMemoryFlag; +import ghidra.trace.model.memory.TraceMemoryManager; +import ghidra.trace.model.thread.TraceThread; +import ghidra.util.Msg; +import ghidra.util.NumericUtilities; +import ghidra.util.database.UndoableTransaction; + +public class TracePcodeEmulatorTest extends AbstractGhidraHeadlessIntegrationTest { + + /** + * Build a trace with a program ready for emulation + * + *

+ * This creates a relatively bare-bones trace with initial state for testing trace + * emulation/interpolation. It adds ".text" and "stack" regions, creates a thread, assembles + * given instructions, and then executes the given SLEIGH source (in the context of the new + * thread) to finish initializing the trace. Note, though given first, the SLEIGH is executed + * after assembly. Thus, it can be used to modify the resulting machine code by modifying the + * memory where it was assembled. + * + * @param tb the trace builder + * @param stateInit SLEIGH source lines to execute to initialize the trace state before + * emulation. Each line must end with ";" + * @param assembly lines of assembly to place starting at {@code 0x00400000} + * @return a new trace thread, whose register state is initialized as specified + * @throws Throwable if anything goes wrong + */ + public TraceThread initTrace(ToyDBTraceBuilder tb, List stateInit, + List assembly) throws Throwable { + TraceMemoryManager mm = tb.trace.getMemoryManager(); + TraceThread thread; + try (UndoableTransaction tid = tb.startTransaction()) { + thread = tb.getOrAddThread("Thread1", 0); + mm.addRegion("Regions[bin:.text]", + Range.atLeast(0L), tb.range(0x00400000, 0x0040ffff), + TraceMemoryFlag.READ, TraceMemoryFlag.EXECUTE); + mm.addRegion("Regions[stack1]", + Range.atLeast(0L), tb.range(0x00100000, 0x0010ffff), + TraceMemoryFlag.READ, TraceMemoryFlag.WRITE); + Assembler asm = Assemblers.getAssembler(tb.trace.getFixedProgramView(0)); + InstructionBlock block = + asm.assemble(tb.addr(0x00400000), assembly.toArray(String[]::new)); + Msg.info(this, "Assembly ended at: " + block.getMaxAddress()); + PcodeExecutor exec = + TraceSleighUtils.buildByteExecutor(tb.trace, 0, thread, 0); + PcodeProgram initProg = SleighProgramCompiler.compileProgram( + (SleighLanguage) tb.language, "test", stateInit, + SleighUseropLibrary.nil()); + exec.execute(initProg, SleighUseropLibrary.nil()); + } + return thread; + } + + /** + * Test a single instruction + * + *

+ * This tests that the internal p-code execution is working, that intermediate writes do not + * affect the trace, and that the write-down method works. That written state is also verified + * against the expected instruction behavior. + */ + @Test + public void testSinglePUSH() throws Throwable { + try (ToyDBTraceBuilder tb = new ToyDBTraceBuilder("Test", "x86:LE:64:default")) { + TraceThread thread = initTrace(tb, + List.of( + "RIP = 0x00400000;", + "RSP = 0x00110000;"), + List.of( + "PUSH 0xdeadbeef")); + + TracePcodeEmulator emu = new TracePcodeEmulator(tb.trace, 0); + PcodeThread emuThread = emu.newThread(thread.getPath()); + emuThread.overrideContextWithDefault(); + emuThread.stepInstruction(); + + // Verify no changes to trace + assertEquals(BigInteger.valueOf(0x00110000), + TraceSleighUtils.evaluate("RSP", tb.trace, 0, thread, 0)); + assertEquals(BigInteger.valueOf(0), + TraceSleighUtils.evaluate("*:4 0x0010fffc:8", tb.trace, 0, thread, 0)); + + try (UndoableTransaction tid = tb.startTransaction()) { + emu.writeDown(tb.trace, 1, 1, true); + } + + // 4, not 8 bytes pushed? + assertEquals(BigInteger.valueOf(0x0010fffc), + TraceSleighUtils.evaluate("RSP", tb.trace, 1, thread, 0)); + assertEquals(BigInteger.valueOf(0xdeadbeefL), + TraceSleighUtils.evaluate("*:4 RSP", tb.trace, 1, thread, 0)); + + assertEquals(tb.addr(0x00400006), + tb.trace.getStackManager() + .getStack(thread, 1, false) + .getFrame(0, false) + .getProgramCounter()); + } + } + + /** + * Test two consecutive instructions + * + *

+ * This tests both the fall-through case, and that the emulator is using the cached intermediate + * register state, rather than reading through to the trace, again. + */ + @Test + public void testDoublePUSH() throws Throwable { + try (ToyDBTraceBuilder tb = new ToyDBTraceBuilder("Test", "x86:LE:64:default")) { + TraceThread thread = initTrace(tb, + List.of( + "RIP = 0x00400000;", + "RSP = 0x00110000;"), + List.of( + "PUSH 0xdeadbeef", + "PUSH 0xbaadf00d")); + + TracePcodeEmulator emu = new TracePcodeEmulator(tb.trace, 0); + PcodeThread emuThread = emu.newThread(thread.getPath()); + emuThread.overrideContextWithDefault(); + emuThread.stepInstruction(); + emuThread.stepInstruction(); + + try (UndoableTransaction tid = tb.startTransaction()) { + emu.writeDown(tb.trace, 1, 1, false); + } + + assertEquals(BigInteger.valueOf(0x0010fff8), + TraceSleighUtils.evaluate("RSP", tb.trace, 1, thread, 0)); + assertEquals(BigInteger.valueOf(0xdeadbeefL), + TraceSleighUtils.evaluate("*:4 (RSP + 4)", tb.trace, 1, thread, 0)); + assertEquals(BigInteger.valueOf(0xbaadf00dL), + TraceSleighUtils.evaluate("*:4 RSP", tb.trace, 1, thread, 0)); + } + } + + /** + * Test the branch case + * + *

+ * This tests that branch instructions function. Both the emulator's counter and the PC of the + * machine state are verified after the JMP. + */ + @Test + public void testJMP() throws Throwable { + try (ToyDBTraceBuilder tb = new ToyDBTraceBuilder("Test", "x86:LE:64:default")) { + Register pc = tb.language.getProgramCounter(); + + TraceThread thread = initTrace(tb, + List.of( + "RIP = 0x00400000;", + "RSP = 0x00110000;", + "RAX = 0x12345678;"), + List.of( + "JMP 0x00400007", // 2 bytes + "MOV EAX,0xdeadbeef", // 5 bytes + "MOV ECX,0xbaadf00d")); // 5 bytes + + TracePcodeEmulator emu = new TracePcodeEmulator(tb.trace, 0); + PcodeThread emuThread = emu.newThread(thread.getPath()); + emuThread.overrideContextWithDefault(); + emuThread.stepInstruction(); + + assertEquals(tb.addr(0x00400007), emuThread.getCounter()); + assertArrayEquals(tb.arr(0x07, 0, 0x40, 0, 0, 0, 0, 0), + emuThread.getState().getVar(pc)); + + emuThread.stepInstruction(); + + try (UndoableTransaction tid = tb.startTransaction()) { + emu.writeDown(tb.trace, 1, 1, false); + } + + assertEquals(BigInteger.valueOf(0x00110000), + TraceSleighUtils.evaluate("RSP", tb.trace, 1, thread, 0)); + assertEquals(BigInteger.valueOf(0x0040000c), + TraceSleighUtils.evaluate("RIP", tb.trace, 1, thread, 0)); + assertEquals(BigInteger.valueOf(0x12345678), + TraceSleighUtils.evaluate("RAX", tb.trace, 1, thread, 0)); + assertEquals(BigInteger.valueOf(0xbaadf00dL), + TraceSleighUtils.evaluate("RCX", tb.trace, 1, thread, 0)); + } + } + + /** + * Test branch with flow + * + *

+ * This will test both context flow and some language-specific state modifiers, since ARM needs + * to truncate the last bit when jumping into THUMB mode. + */ + @Test + public void testBX() throws Throwable { + try (ToyDBTraceBuilder tb = new ToyDBTraceBuilder("Test", "ARM:LE:32:v8")) { + Register pc = tb.language.getProgramCounter(); + Register ctxreg = tb.language.getContextBaseRegister(); + Register tmode = tb.language.getRegister("TMode"); + + Assembler asm = Assemblers.getAssembler(tb.trace.getFixedProgramView(0)); + RegisterValue thumbCtx = + new RegisterValue(ctxreg, BigInteger.ZERO).assign(tmode, BigInteger.ONE); + AssemblyPatternBlock thumbPat = AssemblyPatternBlock.fromRegisterValue(thumbCtx); + + // NOTE: Assemble the thumb section separately + TraceThread thread = initTrace(tb, + List.of( + "pc = 0x00400000;", + "sp = 0x00110000;", + "*:4 0x00400008:4 = 0x00401001;"), // immediately after bx + List.of( + "ldr r6, [pc,#0]!", // 4 bytes, pc+4 should be 00400008 + "bx r6")); // 4 bytes + + byte[] mov = asm.assembleLine(tb.addr(0x00401000), + "mov r0, #123", thumbPat); // #123 is decimal + try (UndoableTransaction tid = tb.startTransaction()) { + asm.patchProgram(mov, tb.addr(0x00401000)); + } + + TracePcodeEmulator emu = new TracePcodeEmulator(tb.trace, 0); + PcodeThread emuThread = emu.newThread(thread.getPath()); + emuThread.overrideContextWithDefault(); + emuThread.stepInstruction(); + emuThread.stepInstruction(); + + assertEquals(tb.addr(0x00401000), emuThread.getCounter()); + assertArrayEquals(tb.arr(0, 0x10, 0x40, 0), + emuThread.getState().getVar(pc)); + assertEquals(new RegisterValue(ctxreg, BigInteger.valueOf(0x8000_0000_0000_0000L)), + emuThread.getContext()); + assertArrayEquals(tb.arr(0, 0, 0, 0, 0, 0, 0, 0x80), + emuThread.getState().getVar(ctxreg)); + + emuThread.stepInstruction(); + + try (UndoableTransaction tid = tb.startTransaction()) { + emu.writeDown(tb.trace, 1, 1, false); + } + + assertEquals(BigInteger.valueOf(0x00110000), + TraceSleighUtils.evaluate("sp", tb.trace, 1, thread, 0)); + assertEquals(BigInteger.valueOf(0x00401002), + TraceSleighUtils.evaluate("pc", tb.trace, 1, thread, 0)); + assertEquals(BigInteger.valueOf(123), + TraceSleighUtils.evaluate("r0", tb.trace, 1, thread, 0)); + } + } + + /** + * This tests an language without a contextreg + */ + @Test + public void testIMM() throws Throwable { + try (ToyDBTraceBuilder tb = new ToyDBTraceBuilder("Test", "Toy:BE:64:default")) { + assertNull(tb.language.getContextBaseRegister()); + + TraceThread thread = initTrace(tb, + List.of( + "pc = 0x00400000;", + "sp = 0x00110000;"), + List.of( + "imm r0, #1234")); // decimal + + TracePcodeEmulator emu = new TracePcodeEmulator(tb.trace, 0); + PcodeThread emuThread = emu.newThread(thread.getPath()); + emuThread.overrideContextWithDefault(); + emuThread.stepInstruction(); + + try (UndoableTransaction tid = tb.startTransaction()) { + emu.writeDown(tb.trace, 1, 1, false); + } + + assertEquals(BigInteger.valueOf(0x00110000), + TraceSleighUtils.evaluate("sp", tb.trace, 1, thread, 0)); + assertEquals(BigInteger.valueOf(0x00400002), + TraceSleighUtils.evaluate("pc", tb.trace, 1, thread, 0)); + assertEquals(BigInteger.valueOf(1234), + TraceSleighUtils.evaluate("r0", tb.trace, 1, thread, 0)); + } + } + + /** + * This tests the delay-slot semantics of the emulator + */ + @Test + public void testBRDS() throws Throwable { + try (ToyDBTraceBuilder tb = new ToyDBTraceBuilder("Test", "Toy:BE:64:default")) { + // TODO: Seems traces do not take delay-slotted instructions well... + // Assemble to the side and just write bytes in until that's fixed + Assembler asm = Assemblers.getAssembler(tb.trace.getFixedProgramView(0)); + TraceThread thread = initTrace(tb, + List.of( + "pc = 0x00400000;", + "sp = 0x00110000;"), + List.of()); + + try (UndoableTransaction tid = tb.startTransaction()) { + tb.trace.getMemoryManager() + .putBytes(0, tb.addr(0x00400000), ByteBuffer.wrap( + asm.assembleLine(tb.addr(0x00400000), "brds 0x00400006"))); + tb.trace.getMemoryManager() + .putBytes(0, tb.addr(0x00400002), ByteBuffer.wrap( + asm.assembleLine(tb.addr(0x00400002), "imm r0, #1234"))); // decimal + tb.trace.getMemoryManager() + .putBytes(0, tb.addr(0x00400004), ByteBuffer.wrap( + asm.assembleLine(tb.addr(0x00400004), "imm r0, #2020"))); + tb.trace.getMemoryManager() + .putBytes(0, tb.addr(0x00400006), ByteBuffer.wrap( + asm.assembleLine(tb.addr(0x00400006), "imm r1, #2021"))); + } + + TracePcodeEmulator emu = new TracePcodeEmulator(tb.trace, 0); + PcodeThread emuThread = emu.newThread(thread.getPath()); + emuThread.overrideContextWithDefault(); + emuThread.stepInstruction(); // brds and 1st imm executed + emuThread.stepInstruction(); // 3rd imm executed + + try (UndoableTransaction tid = tb.startTransaction()) { + emu.writeDown(tb.trace, 1, 1, false); + } + + assertEquals(BigInteger.valueOf(0x00400008), + TraceSleighUtils.evaluate("pc", tb.trace, 1, thread, 0)); + assertEquals(BigInteger.valueOf(1234), + TraceSleighUtils.evaluate("r0", tb.trace, 1, thread, 0)); + assertEquals(BigInteger.valueOf(2021), + TraceSleighUtils.evaluate("r1", tb.trace, 1, thread, 0)); + } + } + + /** + * Test the instruction decoder considers the cached state + * + *

+ * This may not reflect the semantics of an actual processor in these situations, since they may + * have instruction caching. Emulating such semantics is TODO, if at all. NB. This also tests + * that PC-relative addressing works, since internally the emulator advances the counter after + * execution of each instruction. Addressing is computed by the SLEIGH instruction parser and + * encoded as a constant deref in the p-code. + */ + @Test + public void testSelfModifyingX86() throws Throwable { + try (ToyDBTraceBuilder tb = new ToyDBTraceBuilder("Test", "x86:LE:64:default")) { + TraceThread thread = initTrace(tb, + List.of( + "RIP = 0x00400000;", + "RSP = 0x00110000;", + "RAX = 0x12345678;", + // NB. Assembly actually happens first, so this is modifying + "*:1 0x00400007:8 = *0x00400007:8 ^ 0xcc;"), + List.of( + // First instruction undoes the modification above + "XOR byte ptr [0x00400007], 0xcc", // 7 bytes + "MOV EAX,0xdeadbeef")); // 5 bytes + + TracePcodeEmulator emu = new TracePcodeEmulator(tb.trace, 0); + PcodeThread emuThread = emu.newThread(thread.getPath()); + emuThread.overrideContextWithDefault(); + + emuThread.stepInstruction(); + emuThread.stepInstruction(); + + try (UndoableTransaction tid = tb.startTransaction()) { + emu.writeDown(tb.trace, 1, 1, false); + } + + assertEquals(BigInteger.valueOf(0x00110000), + TraceSleighUtils.evaluate("RSP", tb.trace, 1, thread, 0)); + assertEquals(BigInteger.valueOf(0x0040000c), + TraceSleighUtils.evaluate("RIP", tb.trace, 1, thread, 0)); + assertEquals(BigInteger.valueOf(0xdeadbeefL), + TraceSleighUtils.evaluate("RAX", tb.trace, 1, thread, 0)); + } + } + + /** + * Test a two-instruction sample with p-code stepping + * + *

+ * Two instructions are used here to ensure that stepping will proceed to the next instruction. + * This will also serve as an evaluation of the API. + */ + @Test + public void testDoublePUSH_pCode() throws Throwable { + try (ToyDBTraceBuilder tb = new ToyDBTraceBuilder("Test", "x86:LE:64:default")) { + TraceThread thread = initTrace(tb, + List.of( + "RIP = 0x00400000;", + "RSP = 0x00110000;"), + List.of( + "PUSH 0xdeadbeef", + "PUSH 0xbaadf00d")); + + TracePcodeEmulator emu = new TracePcodeEmulator(tb.trace, 0); + PcodeThread emuThread = emu.newThread(thread.getPath()); + emuThread.overrideContextWithDefault(); + assertNull(emuThread.getFrame()); + + emuThread.stepPcodeOp(); + for (int i = 0; !emuThread.getFrame().isFinished(); i++) { + assertEquals(i, emuThread.getFrame().index()); + emuThread.stepPcodeOp(); + } + assertTrue(emuThread.getFrame().isFallThrough()); + assertEquals(tb.addr(0x00400000), emuThread.getCounter()); + + emuThread.stepPcodeOp(); + assertNull(emuThread.getFrame()); + assertEquals(tb.addr(0x00400006), emuThread.getCounter()); + + emuThread.stepPcodeOp(); + assertEquals(0, emuThread.getFrame().index()); + + emuThread.finishInstruction(); + assertNull(emuThread.getFrame()); + + try (UndoableTransaction tid = tb.startTransaction()) { + emu.writeDown(tb.trace, 1, 1, false); + } + + assertEquals(BigInteger.valueOf(0x0040000c), + TraceSleighUtils.evaluate("RIP", tb.trace, 1, thread, 0)); + assertEquals(BigInteger.valueOf(0x0010fff8), + TraceSleighUtils.evaluate("RSP", tb.trace, 1, thread, 0)); + assertEquals(BigInteger.valueOf(0xdeadbeefL), + TraceSleighUtils.evaluate("*:4 (RSP + 4)", tb.trace, 1, thread, 0)); + assertEquals(BigInteger.valueOf(0xbaadf00dL), + TraceSleighUtils.evaluate("*:4 RSP", tb.trace, 1, thread, 0)); + } + } + + /** + * Test the inject method + * + *

+ * This tests that injects work, and that they can invoke a userop from a client-provided + * library + */ + @Test + public void testInject() throws Throwable { + try (ToyDBTraceBuilder tb = new ToyDBTraceBuilder("Test", "x86:LE:64:default")) { + final StringBuilder dumped = new StringBuilder(); + SleighUseropLibrary library = new AnnotatedSleighUseropLibrary() { + @Override + protected Lookup getMethodLookup() { + return MethodHandles.lookup(); + } + + @SleighUserop + public void hexdump(byte[] in) { + dumped.append(NumericUtilities.convertBytesToString(in)); + } + }; + TraceThread thread = initTrace(tb, + List.of( + "RIP = 0x00400000;", + "RSP = 0x00110000;"), + List.of( + "PUSH 0xdeadbeef", + "PUSH 0xbaadf00d")); + + TracePcodeEmulator emu = new TracePcodeEmulator(tb.trace, 0, library); + emu.inject(tb.addr(0x00400006), List.of("hexdump(RSP);")); + PcodeThread emuThread = emu.newThread(thread.getPath()); + emuThread.overrideContextWithDefault(); + + emuThread.stepInstruction(); + assertEquals("", dumped.toString()); + + emuThread.stepInstruction(); + assertEquals("fcff100000000000", dumped.toString()); // LE + } + } + + /** + * Test that injects and interrupts work + * + *

+ * We'll put the interrupt within a more involved inject, so we can single-step the remainder of + * the inject, too. This will check the semantics of stepping over the interrupt. + */ + @Test + public void testInjectedInterrupt() throws Throwable { + try (ToyDBTraceBuilder tb = new ToyDBTraceBuilder("Test", "x86:LE:64:default")) { + final StringBuilder dumped = new StringBuilder(); + SleighUseropLibrary library = new AnnotatedSleighUseropLibrary() { + @Override + protected Lookup getMethodLookup() { + return MethodHandles.lookup(); + } + + @SleighUserop + public void hexdump(byte[] in) { + dumped.append(NumericUtilities.convertBytesToString(in)); + } + }; + TraceThread thread = initTrace(tb, + List.of( + "RIP = 0x00400000;", + "RSP = 0x00110000;"), + List.of( + "PUSH 0xdeadbeef", + "PUSH 0xbaadf00d")); + + TracePcodeEmulator emu = new TracePcodeEmulator(tb.trace, 0, library); + emu.inject(tb.addr(0x00400006), List.of( + "hexdump(RSP);", + "emu_swi();", + "hexdump(RIP);", + "emu_exec_decoded();", + "hexdump(RIP);")); + PcodeThread emuThread = emu.newThread(thread.getPath()); + emuThread.overrideContextWithDefault(); + + try { + emuThread.run(); + } + catch (InterruptPcodeExecutionException e) { + assertEquals(e.getFrame(), emuThread.getFrame()); + } + assertEquals("fcff100000000000", dumped.toString()); // LE + dumped.delete(0, dumped.length()); + + emuThread.stepPcodeOp(); + assertEquals("0600400000000000", dumped.toString()); + dumped.delete(0, dumped.length()); + + emuThread.finishInstruction(); + assertEquals("0c00400000000000", dumped.toString()); + dumped.delete(0, dumped.length()); + + try (UndoableTransaction tid = tb.startTransaction()) { + emu.writeDown(tb.trace, 1, 1, false); + } + + assertEquals(BigInteger.valueOf(0xbaadf00dL), + TraceSleighUtils.evaluate("*:4 RSP", tb.trace, 1, thread, 0)); + } + } + + /** + * Test that conditional breakpoints work + */ + @Test + public void testBreakpoints() throws Throwable { + try (ToyDBTraceBuilder tb = new ToyDBTraceBuilder("Test", "x86:LE:64:default")) { + TraceThread thread = initTrace(tb, + List.of( + "RIP = 0x00400000;", + "RSP = 0x00110000;", + "RAX = 0;"), + List.of( + "PUSH 0xdeadbeef", + "PUSH 0xbaadf00d")); + + TracePcodeEmulator emu = new TracePcodeEmulator(tb.trace, 0); + emu.addBreakpoint(tb.addr(0x00400000), "RAX == 1"); + emu.addBreakpoint(tb.addr(0x00400006), "RAX == 0"); + PcodeThread emuThread = emu.newThread(thread.getPath()); + emuThread.overrideContextWithDefault(); + + try { + emuThread.run(); + } + catch (InterruptPcodeExecutionException e) { + assertEquals(e.getFrame(), emuThread.getFrame()); + } + assertEquals(tb.addr(0x00400006), emuThread.getCounter()); + } + } + + /** + * Test ARM's CLZ instruction + * + *

+ * This tests that the state modifiers are properly invoked on CALLOTHER. + */ + @Test + public void testCLZ() throws Throwable { + try (ToyDBTraceBuilder tb = new ToyDBTraceBuilder("Test", "ARM:LE:32:v8")) { + TraceThread thread = initTrace(tb, + List.of( + "pc = 0x00400000;", + "sp = 0x00110000;", + "r0 = 0x00008000;"), + List.of( + "clz r1, r0")); + + TracePcodeEmulator emu = new TracePcodeEmulator(tb.trace, 0); + PcodeThread emuThread = emu.newThread(thread.getPath()); + emuThread.overrideContextWithDefault(); + emuThread.stepInstruction(); + + try (UndoableTransaction tid = tb.startTransaction()) { + emu.writeDown(tb.trace, 1, 1, false); + } + + assertEquals(BigInteger.valueOf(16), + TraceSleighUtils.evaluate("r1", tb.trace, 1, thread, 0)); + } + } +} diff --git a/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/pcode/exec/trace/TraceSleighUtilsTest.java b/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/pcode/exec/trace/TraceSleighUtilsTest.java index 6686daa12e..adf651b136 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/pcode/exec/trace/TraceSleighUtilsTest.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/pcode/exec/trace/TraceSleighUtilsTest.java @@ -206,7 +206,7 @@ public class TraceSleighUtilsTest extends AbstractGhidraHeadlessIntegrationTest @Test public void testCompileSleighProgram() throws Exception { try (ToyDBTraceBuilder b = new ToyDBTraceBuilder("test", TOY_BE_64_HARVARD)) { - SleighProgram sp = SleighProgramCompiler.compileProgram((SleighLanguage) b.language, + PcodeProgram sp = SleighProgramCompiler.compileProgram((SleighLanguage) b.language, "test", List.of( "if (r0) goto ;", " r1 = 6;", diff --git a/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/database/symbol/DBTraceReferenceManagerTest.java b/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/database/symbol/DBTraceReferenceManagerTest.java index 793a7462c7..a1bf5eb119 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/database/symbol/DBTraceReferenceManagerTest.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/database/symbol/DBTraceReferenceManagerTest.java @@ -403,7 +403,7 @@ public class DBTraceReferenceManagerTest extends AbstractGhidraHeadlessIntegrati } assertEquals(Set.of(flowRef), - new HashSet<>(manager.getFlowRefrencesFrom(0, b.addr(0x4000)))); + new HashSet<>(manager.getFlowReferencesFrom(0, b.addr(0x4000)))); } @Test diff --git a/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/model/time/TraceScheduleTest.java b/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/model/time/TraceScheduleTest.java new file mode 100644 index 0000000000..bd93717fbd --- /dev/null +++ b/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/model/time/TraceScheduleTest.java @@ -0,0 +1,519 @@ +/* ### + * 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.trace.model.time; + +import static org.junit.Assert.*; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; + +import ghidra.pcode.emu.AbstractPcodeMachine; +import ghidra.pcode.emu.AbstractPcodeMachine.ThreadPcodeExecutorState; +import ghidra.pcode.emu.PcodeThread; +import ghidra.pcode.exec.*; +import ghidra.program.model.address.Address; +import ghidra.program.model.lang.RegisterValue; +import ghidra.test.AbstractGhidraHeadlessIntegrationTest; +import ghidra.trace.database.ToyDBTraceBuilder; +import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.model.time.TraceSchedule.*; +import ghidra.util.database.UndoableTransaction; +import ghidra.util.task.TaskMonitor; + +public class TraceScheduleTest extends AbstractGhidraHeadlessIntegrationTest { + + @Test + public void testParseZero() { + TraceSchedule time = TraceSchedule.parse("0:0"); + assertEquals(new TraceSchedule(0, TickSequence.of(), TickSequence.of()), time); + } + + @Test + public void testParseSimple() { + TraceSchedule time = TraceSchedule.parse("0:100"); + assertEquals( + new TraceSchedule(0, TickSequence.of(new TickStep(-1, 100)), TickSequence.of()), time); + } + + @Test + public void testToStringSimple() { + assertEquals("0:100", + new TraceSchedule(0, TickSequence.of(new TickStep(-1, 100)), TickSequence.of()) + .toString()); + } + + @Test + public void testParseWithPcodeSteps() { + TraceSchedule time = TraceSchedule.parse("0:100.5"); + assertEquals(new TraceSchedule(0, TickSequence.of(new TickStep(-1, 100)), + TickSequence.of(new TickStep(-1, 5))), time); + } + + @Test + public void testToStringWithPcodeSteps() { + assertEquals("0:100.5", new TraceSchedule(0, TickSequence.of(new TickStep(-1, 100)), + TickSequence.of(new TickStep(-1, 5))).toString()); + } + + @Test + public void testParseWithThread() { + TraceSchedule time = TraceSchedule.parse("1:t3-100"); + assertEquals(new TraceSchedule(1, TickSequence.of(new TickStep(3, 100)), TickSequence.of()), + time); + } + + @Test + public void testToStringWithThread() { + assertEquals("1:t3-100", + new TraceSchedule(1, TickSequence.of(new TickStep(3, 100)), TickSequence.of()) + .toString()); + } + + @Test + public void testParseMultipleSteps() { + TraceSchedule time = TraceSchedule.parse("1:50,t3-50"); + assertEquals(new TraceSchedule(1, + TickSequence.of(new TickStep(-1, 50), new TickStep(3, 50)), TickSequence.of()), time); + } + + @Test + public void testToStringMultipleSteps() { + assertEquals("1:50,t3-50", + new TraceSchedule(1, TickSequence.of(new TickStep(-1, 50), new TickStep(3, 50)), + TickSequence.of()).toString()); + } + + @Test(expected = IllegalArgumentException.class) + public void testParseNegativeStepErr() { + TraceSchedule.parse("0:-100"); + } + + @Test(expected = IllegalArgumentException.class) + public void testNegativeStepErr() { + new TickStep(0, -100); + } + + @Test(expected = IllegalArgumentException.class) + public void testParseBadStepForm3Parts() { + TraceSchedule.parse("0:t1-10-10"); + } + + @Test(expected = IllegalArgumentException.class) + public void testParseBadStepFormMissingT() { + TraceSchedule.parse("0:1-10"); + } + + @Test + public void testAdvance() { + TickSequence seq = new TickSequence(); + seq.advance(new TickStep(-1, 0)); + assertEquals(TickSequence.of(), seq); + + seq.advance(new TickStep(-1, 10)); + assertEquals(TickSequence.of(new TickStep(-1, 10)), seq); + + seq.advance(new TickStep(-1, 10)); + assertEquals(TickSequence.of(new TickStep(-1, 20)), seq); + + seq.advance(new TickStep(1, 10)); + assertEquals(TickSequence.of(new TickStep(-1, 20), new TickStep(1, 10)), seq); + + seq.advance(new TickStep(-1, 10)); + assertEquals(TickSequence.of(new TickStep(-1, 20), new TickStep(1, 20)), seq); + + seq.advance(seq); + assertEquals(TickSequence.of(new TickStep(-1, 20), new TickStep(1, 60)), seq); + } + + @Test(expected = IllegalArgumentException.class) + public void testAdvanceNegativeErr() { + new TickStep(-1, 10).advance(-10); + } + + @Test(expected = IllegalArgumentException.class) + public void testAdvanceOverflowErr() { + new TickStep(-1, Long.MAX_VALUE).advance(Long.MAX_VALUE); + } + + @Test + public void testRewind() { + TickSequence seq = TickSequence.parse("10,t1-20,t2-30"); + + assertEquals(0, seq.rewind(5)); + assertEquals("10,t1-20,t2-25", seq.toString()); + + assertEquals(0, seq.rewind(25)); + assertEquals("10,t1-20", seq.toString()); + + assertEquals(0, seq.rewind(27)); + assertEquals("3", seq.toString()); + + assertEquals(7, seq.rewind(10)); + assertEquals("", seq.toString()); + + assertEquals(10, seq.rewind(10)); + assertEquals("", seq.toString()); + } + + @Test(expected = IllegalArgumentException.class) + public void testRewindNegativeErr() { + TickSequence seq = TickSequence.parse("10,t1-20,t2-30"); + seq.rewind(-1); + } + + @Test + public void testEquals() { + TraceSchedule time = TraceSchedule.parse("0:10"); + assertTrue(time.equals(time)); + assertFalse(TraceSchedule.parse("0:10").equals(null)); + assertFalse(TraceSchedule.parse("0:10").equals("Hello")); + assertFalse(TraceSchedule.parse("0:t0-10").equals(TraceSchedule.parse("1:t0-10"))); + assertFalse(TraceSchedule.parse("0:t0-10").equals(TraceSchedule.parse("0:t1-10"))); + assertFalse(TraceSchedule.parse("0:t0-10").equals(TraceSchedule.parse("0:t0-11"))); + assertFalse(TraceSchedule.parse("0:t0-10").equals(TraceSchedule.parse("0:t0-10.1"))); + assertTrue(TraceSchedule.parse("0:t0-10").equals(TraceSchedule.parse("0:t0-10"))); + } + + protected void expectU(String specL, String specG) { + TraceSchedule timeL = TraceSchedule.parse(specL); + TraceSchedule timeG = TraceSchedule.parse(specG); + assertEquals(CompareResult.UNREL_LT, timeL.compareSchedule(timeG)); + assertEquals(CompareResult.UNREL_GT, timeG.compareSchedule(timeL)); + } + + protected void expectR(String specL, String specG) { + TraceSchedule timeL = TraceSchedule.parse(specL); + TraceSchedule timeG = TraceSchedule.parse(specG); + assertEquals(CompareResult.REL_LT, timeL.compareSchedule(timeG)); + assertEquals(CompareResult.REL_GT, timeG.compareSchedule(timeL)); + } + + protected void expectE(String specL, String specG) { + TraceSchedule timeL = TraceSchedule.parse(specL); + TraceSchedule timeG = TraceSchedule.parse(specG); + assertEquals(CompareResult.EQUALS, timeL.compareSchedule(timeG)); + assertEquals(CompareResult.EQUALS, timeG.compareSchedule(timeL)); + } + + @Test + public void testCompare() { + expectU("0:10", "1:10"); + expectU("0:t0-10", "0:t1-10"); + // We don't know how many p-code steps complete an instruction step + expectU("0:t0-10.1", "0:t0-11"); + expectU("0:t0-10,t1-5", "0:t0-11,t1-5"); + + expectR("0:t0-10", "0:t0-11"); + expectR("0:t0-10", "0:t0-10,t1-5"); + expectR("0:t0-10", "0:t0-11,t1-5"); + expectR("0:t0-10", "0:t0-10.1"); + expectR("0:t0-10", "0:t0-11.1"); + expectR("0:t0-10", "0:t0-10,t1-5.1"); + expectR("0:t0-10", "0:t0-11,t1-5.1"); + + expectE("0:t0-10", "0:t0-10"); + expectE("0:t0-10.1", "0:t0-10.1"); + } + + public String strRelativize(String fromSpec, String toSpec) { + TickSequence seq = TickSequence.parse(toSpec).relativize(TickSequence.parse(fromSpec)); + return seq == null ? null : seq.toString(); + } + + @Test + public void testRelativize() { + assertEquals("10", strRelativize("", "10")); + assertEquals("", strRelativize("10", "10")); + assertEquals("9", strRelativize("1", "10")); + assertEquals("t1-9", strRelativize("t1-1", "t1-10")); + assertEquals("t1-10", strRelativize("5", "5,t1-10")); + } + + @Test(expected = IllegalArgumentException.class) + public void testRelativizeNotPrefixErr() { + strRelativize("t1-5", "5"); + } + + @Test + public void testTotalStepCount() { + assertEquals(15, TraceSchedule.parse("0:4,t1-5.6").totalTickCount()); + } + + protected static class TestThread implements PcodeThread { + protected final String name; + protected final TestMachine machine; + + public TestThread(String name, TestMachine machine) { + this.name = name; + this.machine = machine; + } + + @Override + public String getName() { + return name; + } + + @Override + public TestMachine getMachine() { + return machine; + } + + @Override + public void setCounter(Address counter) { + } + + @Override + public Address getCounter() { + return null; + } + + @Override + public void overrideCounter(Address counter) { + } + + @Override + public void assignContext(RegisterValue context) { + } + + @Override + public RegisterValue getContext() { + return null; + } + + @Override + public void overrideContext(RegisterValue context) { + } + + @Override + public void overrideContextWithDefault() { + } + + @Override + public void reInitialize() { + } + + @Override + public void stepInstruction() { + machine.record.add("s:" + name); + } + + @Override + public void stepPcodeOp() { + machine.record.add("p:" + name); + } + + @Override + public PcodeFrame getFrame() { + return null; + } + + @Override + public void executeInstruction() { + } + + @Override + public void finishInstruction() { + } + + @Override + public void skipInstruction() { + } + + @Override + public void dropInstruction() { + } + + @Override + public void run() { + } + + @Override + public void setSuspended(boolean suspended) { + } + + @Override + public PcodeExecutor getExecutor() { + return null; + } + + @Override + public SleighUseropLibrary getUseropLibrary() { + return null; + } + + @Override + public ThreadPcodeExecutorState getState() { + return null; + } + + @Override + public void inject(Address address, List sleigh) { + } + + @Override + public void clearInject(Address address) { + } + + @Override + public void clearAllInjects() { + } + } + + protected static class TestMachine extends AbstractPcodeMachine { + protected final List record = new ArrayList<>(); + + public TestMachine() { + super(null, null, null); + } + + @Override + protected PcodeThread createThread(String name) { + return new TestThread(name, this); + } + + @Override + protected PcodeExecutorState createMemoryState() { + return null; + } + + @Override + protected PcodeExecutorState createRegisterState(PcodeThread thread) { + return null; + } + } + + @Test + public void testExecute() throws Exception { + TestMachine machine = new TestMachine(); + TraceSchedule time = TraceSchedule.parse("1:4,t0-3,t1-2.1"); + try (ToyDBTraceBuilder tb = new ToyDBTraceBuilder("test", "Toy:BE:64:default")) { + TraceThread t2; + try (UndoableTransaction tid = tb.startTransaction()) { + tb.trace.getThreadManager().createThread("Threads[0]", 0); + tb.trace.getThreadManager().createThread("Threads[1]", 0); + t2 = tb.trace.getThreadManager().createThread("Threads[2]", 0); + tb.trace.getTimeManager().getSnapshot(1, true).setEventThread(t2); + } + time.execute(tb.trace, machine, TaskMonitor.DUMMY); + } + + assertEquals(List.of( + "s:Threads[2]", + "s:Threads[2]", + "s:Threads[2]", + "s:Threads[2]", + "s:Threads[0]", + "s:Threads[0]", + "s:Threads[0]", + "s:Threads[1]", + "s:Threads[1]", + "p:Threads[1]"), + machine.record); + } + + @Test(expected = IllegalArgumentException.class) + public void testExecuteNoEventThreadErr() throws Exception { + TestMachine machine = new TestMachine(); + TraceSchedule time = TraceSchedule.parse("1:4,t0-3,t1-2.1"); + try (ToyDBTraceBuilder tb = new ToyDBTraceBuilder("test", "Toy:BE:64:default")) { + try (UndoableTransaction tid = tb.startTransaction()) { + tb.trace.getThreadManager().createThread("Threads[0]", 0); + tb.trace.getThreadManager().createThread("Threads[1]", 0); + tb.trace.getThreadManager().createThread("Threads[2]", 0); + tb.trace.getTimeManager().getSnapshot(1, true); + } + time.execute(tb.trace, machine, TaskMonitor.DUMMY); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testExecuteBadThreadKeyErr() throws Exception { + TestMachine machine = new TestMachine(); + TraceSchedule time = TraceSchedule.parse("1:4,t0-3,t5-2.1"); + try (ToyDBTraceBuilder tb = new ToyDBTraceBuilder("test", "Toy:BE:64:default")) { + TraceThread t2; + try (UndoableTransaction tid = tb.startTransaction()) { + tb.trace.getThreadManager().createThread("Threads[0]", 0); + tb.trace.getThreadManager().createThread("Threads[1]", 0); + t2 = tb.trace.getThreadManager().createThread("Threads[2]", 0); + tb.trace.getTimeManager().getSnapshot(1, true).setEventThread(t2); + } + time.execute(tb.trace, machine, TaskMonitor.DUMMY); + } + } + + @Test + public void testFinish() throws Exception { + TestMachine machine = new TestMachine(); + TraceSchedule time = TraceSchedule.parse("1:4,t0-3,t1-2.1"); + try (ToyDBTraceBuilder tb = new ToyDBTraceBuilder("test", "Toy:BE:64:default")) { + TraceThread t2; + try (UndoableTransaction tid = tb.startTransaction()) { + tb.trace.getThreadManager().createThread("Threads[0]", 0); + tb.trace.getThreadManager().createThread("Threads[1]", 0); + t2 = tb.trace.getThreadManager().createThread("Threads[2]", 0); + tb.trace.getTimeManager().getSnapshot(1, true).setEventThread(t2); + } + time.finish(tb.trace, TraceSchedule.parse("1:4,t0-2"), machine, TaskMonitor.DUMMY); + } + + assertEquals(List.of( + "s:Threads[0]", + "s:Threads[1]", + "s:Threads[1]", + "p:Threads[1]"), + machine.record); + } + + @Test + public void testFinishPcode() throws Exception { + TestMachine machine = new TestMachine(); + TraceSchedule time = TraceSchedule.parse("1:4,t0-3,t1-2.1"); + try (ToyDBTraceBuilder tb = new ToyDBTraceBuilder("test", "Toy:BE:64:default")) { + TraceThread t2; + try (UndoableTransaction tid = tb.startTransaction()) { + tb.trace.getThreadManager().createThread("Threads[0]", 0); + tb.trace.getThreadManager().createThread("Threads[1]", 0); + t2 = tb.trace.getThreadManager().createThread("Threads[2]", 0); + tb.trace.getTimeManager().getSnapshot(1, true).setEventThread(t2); + } + time.finish(tb.trace, TraceSchedule.parse("1:4,t0-3,t1-2"), machine, + TaskMonitor.DUMMY); + } + + assertEquals(List.of( + "p:Threads[1]"), + machine.record); + } + + @Test(expected = IllegalArgumentException.class) + public void testFinishUnrelatedErr() throws Exception { + TestMachine machine = new TestMachine(); + TraceSchedule time = TraceSchedule.parse("1:4,t0-3,t1-2.1"); + try (ToyDBTraceBuilder tb = new ToyDBTraceBuilder("test", "Toy:BE:64:default")) { + TraceThread t2; + try (UndoableTransaction tid = tb.startTransaction()) { + tb.trace.getThreadManager().createThread("Threads[0]", 0); + tb.trace.getThreadManager().createThread("Threads[1]", 0); + t2 = tb.trace.getThreadManager().createThread("Threads[2]", 0); + tb.trace.getTimeManager().getSnapshot(1, true).setEventThread(t2); + } + time.finish(tb.trace, TraceSchedule.parse("1:4,t0-4"), machine, TaskMonitor.DUMMY); + } + } +} diff --git a/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/util/DefaultTraceTimeViewportTest.java b/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/util/DefaultTraceTimeViewportTest.java new file mode 100644 index 0000000000..0f20c00b70 --- /dev/null +++ b/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/util/DefaultTraceTimeViewportTest.java @@ -0,0 +1,106 @@ +/* ### + * 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.trace.util; + +import static org.junit.Assert.assertEquals; + +import java.util.List; + +import org.junit.Test; + +import com.google.common.collect.*; + +import ghidra.test.AbstractGhidraHeadlessIntegrationTest; +import ghidra.trace.database.ToyDBTraceBuilder; +import ghidra.trace.database.time.DBTraceTimeManager; +import ghidra.trace.model.time.TraceSchedule; +import ghidra.util.database.UndoableTransaction; + +public class DefaultTraceTimeViewportTest extends AbstractGhidraHeadlessIntegrationTest { + public static > RangeSet rangeSetOf(List> ranges) { + RangeSet result = TreeRangeSet.create(); + ranges.forEach(result::add); + return result; + } + + @Test + public void testEmptyTime() throws Exception { + try (ToyDBTraceBuilder tb = new ToyDBTraceBuilder("test", "Toy:BE:64:default")) { + DefaultTraceTimeViewport viewport = new DefaultTraceTimeViewport(tb.trace); + viewport.setSnap(10); + assertEquals(rangeSetOf(List.of(Range.closed(Long.MIN_VALUE, 10L))), viewport.spanSet); + } + } + + @Test + public void testSelfScheduleSnapshot0RemovesScratch() throws Exception { + try (ToyDBTraceBuilder tb = new ToyDBTraceBuilder("test", "Toy:BE:64:default")) { + try (UndoableTransaction tid = tb.startTransaction()) { + tb.trace.getTimeManager().getSnapshot(0, true).setSchedule(TraceSchedule.snap(0)); + } + + DefaultTraceTimeViewport viewport = new DefaultTraceTimeViewport(tb.trace); + viewport.setSnap(10); + assertEquals(rangeSetOf(List.of(Range.closed(0L, 10L))), viewport.spanSet); + } + } + + @Test + public void testNotationalSchedulesDontFork() throws Exception { + try (ToyDBTraceBuilder tb = new ToyDBTraceBuilder("test", "Toy:BE:64:default")) { + try (UndoableTransaction tid = tb.startTransaction()) { + DBTraceTimeManager tm = tb.trace.getTimeManager(); + tm.getSnapshot(0, true).setSchedule(TraceSchedule.snap(0)); + tm.getSnapshot(5, true).setSchedule(TraceSchedule.parse("4:1")); + } + + DefaultTraceTimeViewport viewport = new DefaultTraceTimeViewport(tb.trace); + viewport.setSnap(10); + assertEquals(rangeSetOf(List.of(Range.closed(0L, 10L))), viewport.spanSet); + } + } + + @Test + public void testForkFromScratch() throws Exception { + try (ToyDBTraceBuilder tb = new ToyDBTraceBuilder("test", "Toy:BE:64:default")) { + try (UndoableTransaction tid = tb.startTransaction()) { + DBTraceTimeManager tm = tb.trace.getTimeManager(); + tm.getSnapshot(0, true).setSchedule(TraceSchedule.snap(0)); + tm.getSnapshot(Long.MIN_VALUE, true).setSchedule(TraceSchedule.parse("10:4")); + } + + DefaultTraceTimeViewport viewport = new DefaultTraceTimeViewport(tb.trace); + viewport.setSnap(Long.MIN_VALUE); + assertEquals( + rangeSetOf(List.of(Range.singleton(Long.MIN_VALUE), Range.closed(0L, 10L))), + viewport.spanSet); + } + } + + @Test + public void testCyclesIgnored() throws Exception { + try (ToyDBTraceBuilder tb = new ToyDBTraceBuilder("test", "Toy:BE:64:default")) { + try (UndoableTransaction tid = tb.startTransaction()) { + DBTraceTimeManager tm = tb.trace.getTimeManager(); + tm.getSnapshot(Long.MIN_VALUE, true).setSchedule(TraceSchedule.parse("10:4")); + } + + DefaultTraceTimeViewport viewport = new DefaultTraceTimeViewport(tb.trace); + viewport.setSnap(Long.MIN_VALUE); + assertEquals(rangeSetOf(List.of(Range.singleton(Long.MIN_VALUE))), viewport.spanSet); + } + } +} diff --git a/Ghidra/Debug/ProposedUtils/certification.manifest b/Ghidra/Debug/ProposedUtils/certification.manifest index 5c54cc06d6..fa66778128 100644 --- a/Ghidra/Debug/ProposedUtils/certification.manifest +++ b/Ghidra/Debug/ProposedUtils/certification.manifest @@ -2,3 +2,5 @@ .classpath||NONE||reviewed||END| .project||NONE||reviewed||END| Module.manifest||GHIDRA||||END| +build.gradle||GHIDRA||||END| +data/ExtensionPoint.manifest||GHIDRA||||END| diff --git a/Ghidra/Debug/ProposedUtils/data/ExtensionPoint.manifest b/Ghidra/Debug/ProposedUtils/data/ExtensionPoint.manifest new file mode 100644 index 0000000000..d0f301cfba --- /dev/null +++ b/Ghidra/Debug/ProposedUtils/data/ExtensionPoint.manifest @@ -0,0 +1,2 @@ +PcodeStateInitializer + diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/CustomToStringCellRenderer.java b/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/CustomToStringCellRenderer.java index 1a0c25816e..5063c2813e 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/CustomToStringCellRenderer.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/CustomToStringCellRenderer.java @@ -17,6 +17,7 @@ package docking.widgets.table; import java.awt.Component; import java.awt.Font; +import java.math.BigInteger; import java.util.function.BiFunction; import javax.swing.JTable; @@ -33,24 +34,32 @@ public class CustomToStringCellRenderer extends AbstractGColumnRenderer { public static final CustomToStringCellRenderer MONO_OBJECT = new CustomToStringCellRenderer<>(CustomFont.MONOSPACED, Object.class, - (v, s) -> v == null ? "" : v.toString()); + (v, s) -> v == null ? "" : v.toString(), false); + public static final CustomToStringCellRenderer MONO_HTML = + new CustomToStringCellRenderer(CustomFont.MONOSPACED, String.class, + (v, s) -> v == null ? "" : v, true); public static final CustomToStringCellRenderer MONO_LONG_HEX = new CustomToStringCellRenderer<>(CustomFont.MONOSPACED, Long.class, - (v, s) -> v == null ? "" : "0x" + Long.toString(v, 16)); + (v, s) -> v == null ? "" : "0x" + Long.toString(v, 16), false); public static final CustomToStringCellRenderer MONO_ULONG_HEX = new CustomToStringCellRenderer<>(CustomFont.MONOSPACED, Long.class, - (v, s) -> v == null ? "" : "0x" + Long.toUnsignedString(v, 16)); + (v, s) -> v == null ? "" : "0x" + Long.toUnsignedString(v, 16), false); + public static final CustomToStringCellRenderer MONO_BIG_HEX = + new CustomToStringCellRenderer<>(CustomFont.MONOSPACED, BigInteger.class, + (v, s) -> v == null ? "" : "0x" + v.toString(16), false); private final CustomFont customFont; private final Class cls; private final BiFunction toString; - public CustomToStringCellRenderer(Class cls, BiFunction toString) { - this(null, cls, toString); + public CustomToStringCellRenderer(Class cls, BiFunction toString, + boolean enableHtml) { + this(null, cls, toString, enableHtml); } public CustomToStringCellRenderer(CustomFont font, Class cls, - BiFunction toString) { + BiFunction toString, boolean enableHtml) { + this.setHTMLRenderingEnabled(enableHtml); this.customFont = font; this.cls = cls; this.toString = toString; diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/AbstractPcodeEmulator.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/AbstractPcodeEmulator.java new file mode 100644 index 0000000000..61c485a967 --- /dev/null +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/AbstractPcodeEmulator.java @@ -0,0 +1,35 @@ +/* ### + * 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.emu; + +import ghidra.app.plugin.processors.sleigh.SleighLanguage; +import ghidra.pcode.exec.BytesPcodeArithmetic; +import ghidra.pcode.exec.SleighUseropLibrary; + +/** + * A p-code machine which executes on concrete bytes and incorporates per-architecture state + * modifiers + */ +public abstract class AbstractPcodeEmulator extends AbstractPcodeMachine { + public AbstractPcodeEmulator(SleighLanguage language, SleighUseropLibrary library) { + super(language, BytesPcodeArithmetic.forLanguage(language), library); + } + + @Override + protected BytesPcodeThread createThread(String name) { + return new BytesPcodeThread(name, this, library); + } +} diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/AbstractPcodeMachine.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/AbstractPcodeMachine.java new file mode 100644 index 0000000000..beb09b53c4 --- /dev/null +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/AbstractPcodeMachine.java @@ -0,0 +1,227 @@ +/* ### + * 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.emu; + +import java.util.*; + +import ghidra.app.plugin.processors.sleigh.SleighLanguage; +import ghidra.pcode.exec.*; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressSpace; +import ghidra.program.model.lang.Language; +import ghidra.program.model.mem.MemBuffer; +import ghidra.util.classfinder.ClassSearcher; + +/** + * An abstract implementation of {@link PcodeMachine} suitable as a base for most implementations + */ +public abstract class AbstractPcodeMachine implements PcodeMachine { + public static class ThreadPcodeExecutorState implements PcodeExecutorState { + protected final PcodeExecutorState memoryState; + protected final PcodeExecutorState registerState; + + public ThreadPcodeExecutorState(PcodeExecutorState memoryState, + PcodeExecutorState registerState) { + this.memoryState = memoryState; + this.registerState = registerState; + } + + @Override + public T longToOffset(AddressSpace space, long l) { + if (space.isRegisterSpace()) { + return registerState.longToOffset(space, l); + } + else { + return memoryState.longToOffset(space, l); + } + } + + @Override + public void setVar(AddressSpace space, T offset, int size, boolean truncateAddressableUnit, + T val) { + if (space.isRegisterSpace()) { + registerState.setVar(space, offset, size, truncateAddressableUnit, val); + } + else { + memoryState.setVar(space, offset, size, truncateAddressableUnit, val); + } + } + + @Override + public T getVar(AddressSpace space, T offset, int size, boolean truncateAddressableUnit) { + if (space.isRegisterSpace()) { + return registerState.getVar(space, offset, size, truncateAddressableUnit); + } + else { + return memoryState.getVar(space, offset, size, truncateAddressableUnit); + } + } + + @Override + public MemBuffer getConcreteBuffer(Address address) { + assert !address.getAddressSpace().isRegisterSpace(); + return memoryState.getConcreteBuffer(address); + } + + public PcodeExecutorState getMemoryState() { + return memoryState; + } + + public PcodeExecutorState getRegisterState() { + return registerState; + } + } + + protected final SleighLanguage language; + protected final PcodeArithmetic arithmetic; + protected final SleighUseropLibrary library; + + protected final SleighUseropLibrary stubLibrary; + + /* for abstract thread access */ PcodeStateInitializer initializer; + private PcodeExecutorState memoryState; + protected final Map> threads = new LinkedHashMap<>(); + + protected final Map injects = new HashMap<>(); + + public AbstractPcodeMachine(SleighLanguage language, PcodeArithmetic arithmetic, + SleighUseropLibrary library) { + this.language = language; + this.arithmetic = arithmetic; + this.library = library; + + this.stubLibrary = createThreadStubLibrary().compose(library); + + /** + * NOTE: Do not initialize memoryState here, since createMemoryState may depend on fields + * initialized in a sub-constructor + */ + + this.initializer = getPluggableInitializer(language); + } + + protected abstract PcodeExecutorState createMemoryState(); + + protected abstract PcodeExecutorState createRegisterState(PcodeThread thread); + + protected SleighUseropLibrary createThreadStubLibrary() { + return new DefaultPcodeThread.SleighEmulationLibrary(null); + } + + /** + * Extension point to override construction of this machine's threads + * + * @param name the name of the new thread + * @return the new thread + */ + protected PcodeThread createThread(String name) { + return new DefaultPcodeThread<>(name, this, library); + } + + protected static PcodeStateInitializer getPluggableInitializer(Language language) { + for (PcodeStateInitializer init : ClassSearcher.getInstances(PcodeStateInitializer.class)) { + if (init.isApplicable(language)) { + return init; + } + } + return null; + } + + protected void doPluggableInitialization() { + if (initializer != null) { + initializer.initializeMachine(this); + } + } + + @Override + public PcodeThread newThread() { + return createThread("Thread" + threads.size()); + } + + @Override + public PcodeThread newThread(String name) { + if (threads.containsKey(name)) { + throw new IllegalStateException("Thread with name '" + name + "' already exists"); + } + PcodeThread thread = createThread(name); + threads.put(name, thread); + return thread; + } + + @Override + public PcodeThread getThread(String name, boolean createIfAbsent) { + PcodeThread thread = threads.get(name); + if (thread == null && createIfAbsent) { + thread = newThread(name); + } + return thread; + } + + @Override + public PcodeExecutorState getMemoryState() { + if (memoryState == null) { + memoryState = createMemoryState(); + doPluggableInitialization(); + } + return memoryState; + } + + protected PcodeProgram getInject(Address address) { + return injects.get(address); + } + + @Override + public PcodeProgram compileSleigh(String sourceName, List lines) { + return SleighProgramCompiler.compileProgram(language, sourceName, lines, stubLibrary); + } + + @Override + public void inject(Address address, List sleigh) { + /** + * TODO: Can I compile the template and build as if the inject were a + * instruction:^instruction constructor? This would require me to delay that build until + * execution, or at least check for instruction modification, if I do want to cache the + * built p-code. + */ + PcodeProgram pcode = compileSleigh("machine_inject:" + address, sleigh); + injects.put(address, pcode); + } + + @Override + public void clearInject(Address address) { + injects.remove(address); + } + + @Override + public void clearAllInjects() { + injects.clear(); + } + + @Override + public void addBreakpoint(Address address, String sleighCondition) { + /** + * TODO: The template build idea is probably more pertinent here. If a user places a + * breakpoint with the purpose of single-stepping the p-code of that instruction, it won't + * work, because that p-code is occluded by emu_exec_decoded(). + */ + PcodeProgram pcode = compileSleigh("breakpoint:" + address, List.of( + "if (!(" + sleighCondition + ")) goto ;", + " emu_swi();", + "", + " emu_exec_decoded();")); + injects.put(address, pcode); + } +} diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/BytesPcodeThread.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/BytesPcodeThread.java new file mode 100644 index 0000000000..2178970329 --- /dev/null +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/BytesPcodeThread.java @@ -0,0 +1,183 @@ +/* ### + * 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.emu; + +import java.lang.reflect.Constructor; + +import ghidra.app.plugin.processors.sleigh.SleighLanguage; +import ghidra.pcode.emulate.*; +import ghidra.pcode.exec.*; +import ghidra.pcode.memstate.MemoryState; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressSpace; +import ghidra.program.model.lang.*; +import ghidra.program.model.pcode.PcodeOp; +import ghidra.util.Msg; + +/** + * A p-code thread which executes on concrete bytes and incorporates per-architecture state + * modifiers + */ +public class BytesPcodeThread extends DefaultPcodeThread { + + protected class GlueEmulate extends Emulate { + public GlueEmulate(SleighLanguage lang, MemoryState s, BreakTable b) { + super(lang, s, b); + } + + @Override + public Language getLanguage() { + return language; + } + + @Override + public void setExecuteAddress(Address addr) { + overrideCounter(addr); + } + + @Override + public Address getExecuteAddress() { + return getCounter(); + } + + @Override + public void setContextRegisterValue(RegisterValue regValue) { + overrideContext(regValue); + } + + @Override + public RegisterValue getContextRegisterValue() { + return getContext(); + } + } + + protected class GlueMemoryState extends MemoryState { + public GlueMemoryState(Language language) { + super(language); + } + + @Override + public int getChunk(byte[] res, AddressSpace spc, long off, int size, + boolean stopOnUnintialized) { + byte[] var = state.getVar(spc, off, size, true); + System.arraycopy(var, 0, res, 0, var.length); + return var.length; + } + + @Override + public void setChunk(byte[] val, AddressSpace spc, long off, int size) { + state.setVar(spc, off, size, true, val); + } + + @Override + public void setInitialized(boolean initialized, AddressSpace spc, long off, int size) { + // Do nothing + } + } + + protected class GluePcodeThreadExecutor extends PcodeThreadExecutor { + public GluePcodeThreadExecutor(Language language, PcodeArithmetic arithmetic, + PcodeExecutorStatePiece state) { + super(language, arithmetic, state); + } + + @Override + public void executeCallother(PcodeOp op, PcodeFrame frame, + SleighUseropLibrary library) { + // Prefer one in the library. Fall-back to state modifier's impl + try { + super.executeCallother(op, frame, library); + } + catch (SleighLinkException e) { + if (modifier == null || !modifier.executeCallOther(op)) { + throw e; + } + } + } + } + + // Part of the glue that makes existing state modifiers work in new emulation framework + protected final EmulateInstructionStateModifier modifier; + protected final Emulate emulate; + + protected Address savedCounter; + + public BytesPcodeThread(String name, AbstractPcodeMachine machine, + SleighUseropLibrary library) { + super(name, machine, library); + + /** + * These two exist as a way to integrate the language-specific injects that are already + * written for the established concrete emulator. + */ + emulate = new GlueEmulate(language, new GlueMemoryState(language), + new BreakTableCallBack(language)); + modifier = createModifier(); + } + + /** + * Construct a modifier for the given language + * + * @return the state modifier + */ + protected EmulateInstructionStateModifier createModifier() { + String classname = language + .getProperty(GhidraLanguagePropertyKeys.EMULATE_INSTRUCTION_STATE_MODIFIER_CLASS); + if (classname == null) { + return null; + } + try { + Class c = Class.forName(classname); + if (!EmulateInstructionStateModifier.class.isAssignableFrom(c)) { + Msg.error(this, + "Language " + language.getLanguageID() + " does not specify a valid " + + GhidraLanguagePropertyKeys.EMULATE_INSTRUCTION_STATE_MODIFIER_CLASS); + throw new RuntimeException(classname + " does not implement interface " + + EmulateInstructionStateModifier.class.getName()); + } + Constructor constructor = c.getConstructor(Emulate.class); + return (EmulateInstructionStateModifier) constructor.newInstance(emulate); + } + catch (Exception e) { + Msg.error(this, "Language " + language.getLanguageID() + " does not specify a valid " + + GhidraLanguagePropertyKeys.EMULATE_INSTRUCTION_STATE_MODIFIER_CLASS); + throw new RuntimeException( + "Failed to instantiate " + classname + " for language " + language.getLanguageID(), + e); + } + } + + @Override + protected PcodeThreadExecutor createExecutor() { + return new GluePcodeThreadExecutor(language, arithmetic, state); + } + + @Override + protected void preExecuteInstruction() { + if (modifier != null) { + savedCounter = getCounter(); + modifier.initialExecuteCallback(emulate, savedCounter, getContext()); + } + } + + @Override + protected void postExecuteInstruction() { + if (modifier != null) { + modifier.postExecuteCallback(emulate, savedCounter, frame.copyCode(), + frame.getBranched(), getCounter()); + } + } +} diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/DefaultPcodeThread.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/DefaultPcodeThread.java new file mode 100644 index 0000000000..e95a38d6d7 --- /dev/null +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/DefaultPcodeThread.java @@ -0,0 +1,402 @@ +/* ### + * 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.emu; + +import java.math.BigInteger; +import java.util.*; + +import ghidra.app.plugin.processors.sleigh.SleighLanguage; +import ghidra.pcode.emu.AbstractPcodeMachine.ThreadPcodeExecutorState; +import ghidra.pcode.exec.*; +import ghidra.program.model.address.Address; +import ghidra.program.model.lang.*; +import ghidra.program.model.listing.Instruction; +import ghidra.program.model.pcode.PcodeOp; +import ghidra.program.util.ProgramContextImpl; +import ghidra.util.Msg; + +/** + * The default implementation of {@link PcodeThread} suitable for most applications + */ +public class DefaultPcodeThread implements PcodeThread { + protected static class SleighEmulationLibrary extends AnnotatedSleighUseropLibrary { + private final DefaultPcodeThread thread; + + public SleighEmulationLibrary(DefaultPcodeThread thread) { + this.thread = thread; + } + + @SleighUserop + public void emu_exec_decoded() { + /** + * TODO: This idea of "pushing" a frame could be formalized, and the full stack made + * accessible to the client. This would permit "stepping into", and provide continuation + * after an interrupt. The caveat however, is whatever Java code invoked the inner frame + * cannot be continued/resumed. Such code could provide nothing more than glue. + */ + PcodeFrame saved = thread.frame; + thread.dropInstruction(); + thread.executeInstruction(); + thread.frame = saved; + } + + @SleighUserop + public void emu_skip_decoded() { + PcodeFrame saved = thread.frame; + thread.dropInstruction(); + thread.skipInstruction(); + thread.frame = saved; + } + + @SleighUserop + public void emu_swi() { + throw new InterruptPcodeExecutionException(null, null); + } + } + + protected class PcodeThreadExecutor extends PcodeExecutor { + volatile boolean suspended = false; + + public PcodeThreadExecutor(Language language, PcodeArithmetic arithmetic, + PcodeExecutorStatePiece state) { + super(language, arithmetic, state); + } + + @Override + public void stepOp(PcodeOp op, PcodeFrame frame, SleighUseropLibrary library) { + if (suspended) { + throw new SuspendedPcodeExecutionException(frame, null); + } + super.stepOp(op, frame, library); + } + + @Override + protected void branchToAddress(Address target) { + overrideCounter(target); + } + } + + private final String name; + private final AbstractPcodeMachine machine; + protected final SleighLanguage language; + protected final PcodeArithmetic arithmetic; + protected final ThreadPcodeExecutorState state; + protected final InstructionDecoder decoder; + protected final SleighUseropLibrary library; + + protected final PcodeThreadExecutor executor; + protected final Register pc; + protected final Register contextreg; + + private Address counter; + private RegisterValue context; + + protected Instruction instruction; + protected PcodeFrame frame; + + protected final ProgramContextImpl defaultContext; + protected final Map injects = new HashMap<>(); + + public DefaultPcodeThread(String name, AbstractPcodeMachine machine, + SleighUseropLibrary library) { + this.name = name; + this.machine = machine; + this.language = machine.language; + this.arithmetic = machine.arithmetic; + PcodeExecutorState memoryState = machine.getMemoryState(); + PcodeExecutorState registerState = machine.createRegisterState(this); + this.state = new ThreadPcodeExecutorState<>(memoryState, registerState); + this.decoder = new SleighInstructionDecoder(language, memoryState); + this.library = new SleighEmulationLibrary<>(this).compose(library); + + this.executor = createExecutor(); + this.pc = language.getProgramCounter(); + this.contextreg = language.getContextBaseRegister(); + + if (contextreg != null) { + defaultContext = new ProgramContextImpl(language); + language.applyContextSettings(defaultContext); + this.context = defaultContext.getDefaultDisassemblyContext(); + } + else { + defaultContext = null; + } + this.reInitialize(); + } + + protected PcodeThreadExecutor createExecutor() { + return new PcodeThreadExecutor(language, arithmetic, state); + } + + @Override + public String getName() { + return name; + } + + @Override + public AbstractPcodeMachine getMachine() { + return machine; + } + + @Override + public void setCounter(Address counter) { + this.counter = counter; + } + + @Override + public Address getCounter() { + return counter; + } + + @Override + public void overrideCounter(Address counter) { + setCounter(counter); + state.setVar(pc, arithmetic.fromConst(counter.getOffset(), pc.getMinimumByteSize())); + } + + @Override + public void assignContext(RegisterValue context) { + if (!context.getRegister().isProcessorContext()) { + throw new IllegalArgumentException("context must be the contextreg value"); + } + this.context = this.context.assign(context.getRegister(), context); + } + + @Override + public RegisterValue getContext() { + return context; + } + + @Override + public void overrideContext(RegisterValue context) { + assignContext(context); + state.setVar(contextreg, arithmetic.fromConst( + this.context.getUnsignedValueIgnoreMask(), + contextreg.getMinimumByteSize())); + } + + @Override + public void overrideContextWithDefault() { + if (contextreg != null) { + overrideContext(defaultContext.getDefaultValue(contextreg, counter)); + } + } + + protected void doPluggableInitialization() { + if (machine.initializer != null) { + machine.initializer.initializeThread(this); + } + } + + @Override + public void reInitialize() { + long offset = arithmetic.toConcrete(state.getVar(pc)).longValue(); + setCounter(language.getDefaultSpace().getAddress(offset)); + + if (contextreg != null) { + try { + BigInteger ctx = arithmetic.toConcrete(state.getVar(contextreg)); + assignContext(new RegisterValue(contextreg, ctx)); + } + catch (AccessPcodeExecutionException e) { + Msg.info(this, "contextreg not recorded in trace. This is pretty normal."); + } + } + + doPluggableInitialization(); + } + + @Override + public void stepInstruction() { + PcodeProgram inj = getInject(counter); + if (inj != null) { + instruction = null; + try { + executor.execute(inj, library); + } + catch (PcodeExecutionException e) { + frame = e.getFrame(); + throw e; + } + } + else { + executeInstruction(); + } + } + + @Override + public void stepPcodeOp() { + if (frame == null) { + beginInstructionOrInject(); + } + else if (!frame.isFinished()) { + executor.step(frame, library); + } + else { + advanceAfterFinished(); + } + } + + protected void beginInstructionOrInject() { + PcodeProgram inj = injects.get(counter); + if (inj != null) { + instruction = null; + frame = executor.begin(inj); + } + else { + instruction = decoder.decodeInstruction(counter, context); + PcodeProgram pcode = PcodeProgram.fromInstruction(instruction); + frame = executor.begin(pcode); + } + } + + protected void advanceAfterFinished() { + if (instruction == null) { // Frame resulted from an inject + frame = null; + return; + } + if (frame.isFallThrough()) { + overrideCounter(counter.addWrap(decoder.getLastLengthWithDelays())); + } + if (contextreg != null) { + overrideContext(instruction.getRegisterValue(contextreg)); + } + postExecuteInstruction(); + frame = null; + } + + @Override + public PcodeFrame getFrame() { + return frame; + } + + protected void assertCompletedInstruction() { + if (frame != null) { + throw new IllegalStateException("The current instruction or inject has not finished."); + } + } + + protected void assertMidInstruction() { + if (frame == null) { + throw new IllegalStateException("There is no current instruction to finish."); + } + } + + /** + * An extension point for hooking instruction execution before the fact + */ + protected void preExecuteInstruction() { + // Extension point + } + + /** + * An extension point for hooking instruction execution after the fact + */ + protected void postExecuteInstruction() { + // Extension point + } + + @Override + public void executeInstruction() { + assertCompletedInstruction(); + instruction = decoder.decodeInstruction(counter, context); + PcodeProgram insProg = PcodeProgram.fromInstruction(instruction); + preExecuteInstruction(); + try { + frame = executor.execute(insProg, library); + } + catch (PcodeExecutionException e) { + frame = e.getFrame(); + throw e; + } + advanceAfterFinished(); + } + + @Override + public void finishInstruction() { + assertMidInstruction(); + executor.finish(frame, library); + advanceAfterFinished(); + } + + @Override + public void skipInstruction() { + assertCompletedInstruction(); + instruction = decoder.decodeInstruction(counter, context); + overrideCounter(counter.addWrap(decoder.getLastLengthWithDelays())); + } + + @Override + public void dropInstruction() { + frame = null; + } + + @Override + public void run() { + executor.suspended = false; + if (frame != null) { + finishInstruction(); + } + while (true) { + stepInstruction(); + } + } + + @Override + public void setSuspended(boolean suspended) { + executor.suspended = suspended; + } + + @Override + public PcodeExecutor getExecutor() { + return executor; + } + + @Override + public SleighUseropLibrary getUseropLibrary() { + return library; + } + + @Override + public ThreadPcodeExecutorState getState() { + return state; + } + + protected PcodeProgram getInject(Address address) { + PcodeProgram inj = injects.get(address); + if (inj != null) { + return inj; + } + return machine.getInject(address); + } + + @Override + public void inject(Address address, List sleigh) { + PcodeProgram pcode = SleighProgramCompiler.compileProgram( + language, "thread_inject:" + address, sleigh, library); + injects.put(address, pcode); + } + + @Override + public void clearInject(Address address) { + injects.remove(address); + } + + @Override + public void clearAllInjects() { + injects.clear(); + } +} diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/InstructionDecoder.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/InstructionDecoder.java new file mode 100644 index 0000000000..d25bf85e3f --- /dev/null +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/InstructionDecoder.java @@ -0,0 +1,48 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.pcode.emu; + +import ghidra.program.model.address.Address; +import ghidra.program.model.lang.RegisterValue; +import ghidra.program.model.listing.Instruction; + +public interface InstructionDecoder { + /** + * Decode the instruction at the given address using the given context + * + *

+ * This method cannot return null. If a decode error occurs, it must throw an exception. + * + * @param address the address to start decoding + * @param context the disassembler/decode context + * @return the instruction + */ + Instruction decodeInstruction(Address address, RegisterValue context); + + /** + * Get the last instruction decoded + * + * @return the instruction + */ + Instruction getLastInstruction(); + + /** + * Get the length of the last decoded instruction, including delay slots + * + * @return the length + */ + int getLastLengthWithDelays(); +} diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/PcodeMachine.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/PcodeMachine.java new file mode 100644 index 0000000000..4e195beed5 --- /dev/null +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/PcodeMachine.java @@ -0,0 +1,125 @@ +/* ### + * 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.emu; + +import java.util.List; + +import ghidra.pcode.emu.DefaultPcodeThread.SleighEmulationLibrary; +import ghidra.pcode.exec.PcodeExecutorState; +import ghidra.pcode.exec.PcodeProgram; +import ghidra.program.model.address.Address; + +/** + * A machine which execute p-code on state of an abstract type + * + * @param the type of objects in the machine's state + */ +public interface PcodeMachine { + + /** + * Create a new thread with a default name in this machine + * + * @return the new thread + */ + PcodeThread newThread(); + + /** + * Create a new thread with the given name in this machine + * + * @param name the name + * @return the new thread + */ + PcodeThread newThread(String name); + + /** + * Get the thread, if present, with the given name + * + * @param name the name + * @param createIfAbsent create a new thread if the thread does not already exist + * @return the thread, or {@code null} if absent and not created + */ + PcodeThread getThread(String name, boolean createIfAbsent); + + /** + * Get the machine's memory state + * + *

+ * The returned state will may throw {@link IllegalArgumentException} if the client requests + * register values of it. This state is shared among all threads in this machine. + * + * @return the memory state + */ + PcodeExecutorState getMemoryState(); + + /** + * Compile the given SLEIGH code for execution by a thread of this machine + * + *

+ * This links in the userop library given at construction time and those defining the emulation + * userops, e.g., {@code emu_swi}. + * + * @param sourceName a user-defined source name for the resulting "program" + * @param lines the lines of SLEIGH source code + * @return the compiled program + */ + PcodeProgram compileSleigh(String sourceName, List lines); + + /** + * Override the p-code at the given address with the given SLEIGH source + * + *

+ * This will attempt to compile the given source against this machine's userop library and then + * will inject it at the given address. The resulting p-code replaces that which would + * be executed by decoding the instruction at the given address. The means the machine will not + * decode, nor advance its counter, unless the SLEIGH causes it. In most cases, the SLEIGH will + * call {@link SleighEmulationLibrary#emu_exec_decoded()} to cause the machine to decode and + * execute the overridden instruction. + * + *

+ * Each address can have at most a single inject. If there is already one present, it is + * replaced and the old inject completely forgotten. The injector does not support chaining or + * double-wrapping, etc. + * + * @param address the address to inject at + * @param sleigh the SLEIGH source to compile and inject + */ + void inject(Address address, List sleigh); + + /** + * Remove the inject, if present, at the given address + * + * @param address the address to clear + */ + void clearInject(Address address); + + /** + * Remove all injects from this machine + */ + void clearAllInjects(); + + /** + * Add a (conditional) breakpoint at the given address + * + *

+ * Breakpoints are implemented at the p-code level using an inject, without modification to the + * emulated image. As such, it cannot coexist with another inject. A client needing to break + * during an inject must use {@link SleighEmulationLibrary#emu_swi()} in the injected SLEIGH. + * + * @param address the address at which to break + * @param sleighCondition a SLEIGH expression which controls the breakpoint + */ + void addBreakpoint(Address address, String sleighCondition); +} diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/PcodeStateInitializer.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/PcodeStateInitializer.java new file mode 100644 index 0000000000..6ed69b4f6d --- /dev/null +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/PcodeStateInitializer.java @@ -0,0 +1,69 @@ +/* ### + * 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.emu; + +import ghidra.program.model.lang.Language; +import ghidra.util.classfinder.ExtensionPoint; + +/** + * An extension for preparing execution state for sleigh emulation + * + *

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

+ * TODO: Implement annotation-based {@link #isApplicable(Language)}? + */ +public interface PcodeStateInitializer extends ExtensionPoint { + + /** + * Check if this initializer applies to the given language + * + * @param language the language to check + * @return true if it applies, false otherwise + */ + boolean isApplicable(Language language); + + /** + * The machine's memory state has just been initialized from a "real" target, and additional + * initialization is needed for SLEIGH execution + * + *

+ * There's probably not much preparation of memory + * + * @param the type of values in the machine state + * @param machine the newly-initialized machine + */ + default void initializeMachine(PcodeMachine machine) { + } + + /** + * The thread's register state has just been initialized from a "real" target, and additional + * initialization is needed for SLEIGH execution + * + *

+ * Initialization generally consists of setting "virtual" registers using data from the real + * ones. Virtual registers are those specified in the SLEIGH, but which don't actually exist on + * the target processor. Often, they exist to simplify static analysis, but unfortunately cause + * a minor headache for dynamic execution. + * + * @param the type of values in the machine state + * @param thread the newly-initialized thread + */ + default void initializeThread(PcodeThread thread) { + } +} diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/PcodeThread.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/PcodeThread.java new file mode 100644 index 0000000000..64cf785b97 --- /dev/null +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/PcodeThread.java @@ -0,0 +1,288 @@ +/* ### + * 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.emu; + +import java.util.List; + +import ghidra.pcode.emu.AbstractPcodeMachine.ThreadPcodeExecutorState; +import ghidra.pcode.emu.DefaultPcodeThread.SleighEmulationLibrary; +import ghidra.pcode.exec.*; +import ghidra.program.model.address.Address; +import ghidra.program.model.lang.RegisterValue; + +/** + * An emulated thread of execution + * + * @param the type of values in the emulated machine state + */ +public interface PcodeThread { + + /** + * Get the name of this thread + * + * @return the name + */ + String getName(); + + /** + * Get the machine within which this thread executes + * + * @return the containing machine + */ + PcodeMachine getMachine(); + + /** + * Set the emulator's counter without writing to its machine state + * + * @param counter the new target address + */ + void setCounter(Address counter); + + /** + * Get the value of the program counter of this thread + * + * @return the value + */ + Address getCounter(); + + /** + * Set the emulator's counter and write the PC of its machine state + * + * @param counter the new target address + */ + void overrideCounter(Address counter); + + /** + * Adjust the emulator's parsing context without writing to its machine state + * + * @param context the new context + */ + void assignContext(RegisterValue context); + + /** + * Adjust the emulator's parsing context without writing to its machine state + * + * @param context the new context void assignContext(RegisterValue context); + * + * /** Get the emulator's parsing context + * + * @return the context + */ + RegisterValue getContext(); + + /** + * Adjust the emulator's parsing context and write the contextreg of its machine state + * + * @param context the new context + */ + void overrideContext(RegisterValue context); + + /** + * Set the context at the current counter to the default given by the language + * + *

+ * This also writes the context to the machine's state. For languages without context, this call + * does nothing. + * + *

+ * TODO: Seems to me, since this method must be called upon creating any emulator thread, that's + * evidence the trace's context manager is not providing correct defaults. + */ + void overrideContextWithDefault(); + + /** + * Re-sync the decode context and counter address from the machine state + */ + void reInitialize(); + + /** + * Step emulation a single instruction + * + *

+ * Note because of the way Ghidra and Sleigh handle delay slots, the execution of an instruction + * with delay slots cannot be separated from the following instructions filling them. It and its + * slots are executed in a single "step." Stepping individual p-code ops which comprise the + * delay-slotted instruction is possible using {@link #stepPcodeOp(PcodeFrame)}. + */ + void stepInstruction(); + + /** + * Step emulation a single p-code operation + * + *

+ * Execution of the current instruction begins if there is no current frame: A new frame is + * constructed and its counter is initialized. If a frame is present, and it has not been + * completed, its next operation is executed and its counter is stepped. If the current frame is + * completed, the machine's program counter is advanced and the current frame is removed. + * + *

+ * In order to provide the most flexibility, there is no enforcement of various emulation state + * on this method. Expect strange behavior for strange call sequences. For example, the caller + * should ensure that the given frame was in fact generated from the emulators current + * instruction. Doing otherwise may cause the emulator to advance in strange ways. + * + *

+ * While this method heeds injects, such injects will obscure the p-code of the instruction + * itself. If the inject executes the instruction, the entire instruction will be executed when + * stepping the {@link SleighEmulationLibrary#emu_exec_decoded()} userop, since there is not + * (currently) any way to "step into" a userop. + */ + void stepPcodeOp(); + + /** + * Get the current frame, if present + * + *

+ * If the client only calls {@link #stepInstruction()} and execution completes normally, this + * method will always return {@code null}. If interrupted, the frame marks where execution of an + * instruction or inject should resume. Depending on the case, the frame may need to be stepped + * back in order to retry the failed p-code operation. If this frame is present, it means that + * the instruction has not been executed completed. Even if the frame + * {@link PcodeFrame#isFinished()}, + * + * @return the current frame + */ + PcodeFrame getFrame(); + + /** + * Execute the next instruction, ignoring injects + * + *

+ * This method should likely only be used internally. It steps the current instruction, but + * without any consideration for user injects, e.g., breakpoints. Most clients should call + * {@link #stepInstruction()} instead. + * + * @throws IllegalStateException if the emulator is still in the middle of an instruction. That + * can happen if the machine is interrupted, or if the client has called + * {@link #stepPcodeOp()}. + */ + void executeInstruction(); + + /** + * Finish execution of the current instruction or inject + * + *

+ * In general, this method is only used after an interrupt or fault in order to complete the + * p-code of the faulting instruction. Depending on the nature of the interrupt, this behavior + * may not be desired. + * + * @throws IllegalStateException if there is no current instruction, i.e., the emulator has not + * started executing the next instruction, yet. + */ + void finishInstruction(); + + /** + * Decode, but skip the next instruction + */ + void skipInstruction(); + + /** + * If there is a current instruction, drop its frame of execution + * + *

+ * This does not revert any state changes caused by a partially-executed instruction. It is up + * to the client to revert the underlying machine state if desired. Note the thread's program + * counter will not be advanced. Likely, the next call to {@link #stepInstruction()} will + * re-start the same instruction. If there is no current instruction, this method has no effect. + */ + void dropInstruction(); + + /** + * Emulate indefinitely + * + *

+ * This begins or resumes execution of the emulator. If there is a current instruction, that + * instruction is finished. By calling this method, you are "donating" the current Java thread + * to the emulator. This method will not likely return, but instead only terminates via + * exception, e.g., hitting a user breakpoint or becoming suspended. Depending on the use case, + * this method might be invoked from a dedicated Java thread. + */ + void run(); + + /** + * Set the suspension state of the thread's executor + * + *

+ * When {@link #run()} is invoked by a dedicated thread, suspending the pcode thread is the most + * reliable way to halt execution. Note the emulator will halt mid instruction. If this is not + * desired, then upon catching the exception, the dedicated thread should un-suspend the machine + * and call {@link #finishInstruction()}. + */ + void setSuspended(boolean suspended); + + /** + * Get the thread's p-code executor + * + *

+ * This can be used to execute inject p-code execution, e.g., as part of implementing a userop, + * or as part of testing, outside the emulator's usual control flow. Any new frame generated by + * the executor is ignored by the emulator. It retains the instruction frame, if any. Note that + * suspension is implemented by the executor, so if this p-code thread is suspended, the + * executor cannot execute any code. + * + * @return the executor + */ + PcodeExecutor getExecutor(); + + /** + * Get the userop library for controlling this thread's execution + * + * @return the library + */ + SleighUseropLibrary getUseropLibrary(); + + /** + * Get the thread's memory and register state + * + *

+ * The memory part of this state is shared among all threads in the same machine. See + * {@link PcodeMachine#getMemoryState()}. + * + */ + ThreadPcodeExecutorState getState(); + + /** + * Override the p-code at the given address with the given SLEIGH source for only this thread + * + * This works the same {@link PcodeMachine#inject(Address, List)} but on a per-thread basis. + * Where there is both a machine-level and thread-level inject the thread inject takes + * precedence. Furthermore, the machine-level inject cannot be accessed by the thread-level + * inject. + * + * @param address the address to inject at + * @param sleigh the SLEIGH source to compile and inject + */ + void inject(Address address, List sleigh); + + /** + * Remove the per-thread inject, if present, at the given address + * + *

+ * This has no affect on machine-level injects. If there is one present, it will still override + * this thread's p-code if execution reaches the address. + * + * @param address the address to clear + */ + void clearInject(Address address); + + /** + * Remove all per-thread injects from this thread + * + *

+ * All machine-level injects are still effective after this call. + */ + void clearAllInjects(); +} diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/SleighInstructionDecoder.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/SleighInstructionDecoder.java new file mode 100644 index 0000000000..516e5fc919 --- /dev/null +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/SleighInstructionDecoder.java @@ -0,0 +1,103 @@ +/* ### + * 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.emu; + +import ghidra.pcode.emulate.InstructionDecodeException; +import ghidra.pcode.exec.PcodeExecutorState; +import ghidra.program.disassemble.Disassembler; +import ghidra.program.disassemble.DisassemblerMessageListener; +import ghidra.program.model.address.*; +import ghidra.program.model.lang.*; +import ghidra.program.model.listing.Instruction; +import ghidra.util.Msg; +import ghidra.util.task.TaskMonitor; + +public class SleighInstructionDecoder implements InstructionDecoder { + // TODO: Some sort of instruction decode caching? + // Not as imported for stepping small distances + // Could become important when dealing with "full system emulation," if we get there. + + private static final String DEFAULT_ERROR = "Unknown disassembly error"; + + protected final PcodeExecutorState state; + protected final AddressFactory addrFactory; + protected final Disassembler disassembler; + + protected String lastMsg = DEFAULT_ERROR; + + protected InstructionBlock block; + protected int lengthWithDelays; + + private Instruction instruction; + + public SleighInstructionDecoder(Language language, PcodeExecutorState state) { + this.state = state; + addrFactory = language.getAddressFactory(); + DisassemblerMessageListener listener = msg -> { + Msg.warn(this, msg); + lastMsg = msg; + }; + disassembler = + Disassembler.getDisassembler(language, addrFactory, TaskMonitor.DUMMY, listener); + } + + @Override + public Instruction decodeInstruction(Address address, RegisterValue context) { + lastMsg = DEFAULT_ERROR; + // Always re-parse block in case bytes change + block = disassembler.pseudoDisassembleBlock(state.getConcreteBuffer(address), context, 1); + instruction = block == null ? null : block.getInstructionAt(address); + if (instruction == null) { + throw new InstructionDecodeException(lastMsg, address); + } + lengthWithDelays = computeLength(); + return instruction; + } + + protected int computeLength() { + int length = instruction.getLength(); + int slots = instruction.getDelaySlotDepth(); + Instruction ins = instruction; + for (int i = 0; i < slots; i++) { + try { + Address next = ins.getAddress().addNoWrap(ins.getLength()); + Instruction ni = block.getInstructionAt(next); + if (ni == null) { + throw new InstructionDecodeException("Failed to parse delay slot instruction", + next); + } + ins = ni; + length += ins.getLength(); + } + catch (AddressOverflowException e) { + throw new InstructionDecodeException("Delay slot would exceed address space", + ins.getAddress()); + } + } + return length; + } + + @Override + public int getLastLengthWithDelays() { + return lengthWithDelays; + + } + + @Override + public Instruction getLastInstruction() { + return instruction; + } +} diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/x86/X86PcodeStateInitializer.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/x86/X86PcodeStateInitializer.java new file mode 100644 index 0000000000..d21bc0dfb4 --- /dev/null +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/x86/X86PcodeStateInitializer.java @@ -0,0 +1,48 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.pcode.emu.x86; + +import java.util.List; + +import ghidra.pcode.emu.PcodeStateInitializer; +import ghidra.pcode.emu.PcodeThread; +import ghidra.pcode.exec.PcodeProgram; +import ghidra.program.model.lang.Language; +import ghidra.program.model.lang.LanguageID; +import ghidra.util.Msg; + +public class X86PcodeStateInitializer implements PcodeStateInitializer { + private static final List LANG_IDS = List.of( + new LanguageID("x86:LE:32:default"), + new LanguageID("x86:LE:64:default")); + private static final List SOURCE = List.of( + "FS_OFFSET = 0;", + "GS_OFFSET = 0;"); + + @Override + public boolean isApplicable(Language language) { + return false; + //return LANG_IDS.contains(language.getLanguageID()); + } + + @Override + public void initializeThread(PcodeThread thread) { + Msg.warn(this, "Segmentation is not emulated. Initializing FS_OFFSET and FS_OFFSET to 0."); + + PcodeProgram init = thread.getMachine().compileSleigh("initializer", SOURCE); + thread.getExecutor().execute(init, thread.getUseropLibrary()); + } +} diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/AccessPcodeExecutionException.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/AccessPcodeExecutionException.java new file mode 100644 index 0000000000..8a5b67c8d9 --- /dev/null +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/AccessPcodeExecutionException.java @@ -0,0 +1,33 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.pcode.exec; + +/** + * There was an issue accessing the executor's state, i.e., memory or register values + */ +public class AccessPcodeExecutionException extends PcodeExecutionException { + public AccessPcodeExecutionException(String message, PcodeFrame frame, Throwable cause) { + super(message, frame, cause); + } + + public AccessPcodeExecutionException(String message, Exception cause) { + super(message, cause); + } + + public AccessPcodeExecutionException(String message) { + super(message); + } +} diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/AddressOfPcodeArithmetic.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/AddressOfPcodeArithmetic.java index 2394441414..18ac5a2f4e 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/AddressOfPcodeArithmetic.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/AddressOfPcodeArithmetic.java @@ -15,6 +15,8 @@ */ package ghidra.pcode.exec; +import java.math.BigInteger; + import ghidra.pcode.opbehavior.BinaryOpBehavior; import ghidra.pcode.opbehavior.UnaryOpBehavior; import ghidra.program.model.address.Address; @@ -39,8 +41,18 @@ public enum AddressOfPcodeArithmetic implements PcodeArithmetic

{ return null; // TODO: Do we care about Constant space? } + @Override + public Address fromConst(BigInteger value, int size) { + return null; + } + @Override public boolean isTrue(Address cond) { - throw new AssertionError("Cannot decide branches using an address"); + throw new AssertionError("Cannot decide branches using 'address of'"); + } + + @Override + public BigInteger toConcrete(Address value) { + throw new AssertionError("Should not attempt to concretize 'address of'"); } } diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/AddressOfPcodeExecutorState.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/AddressOfPcodeExecutorState.java index b373a2f137..118b45072a 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/AddressOfPcodeExecutorState.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/AddressOfPcodeExecutorState.java @@ -21,6 +21,7 @@ import java.util.Map; import ghidra.pcode.utils.Utils; import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressSpace; +import ghidra.program.model.mem.MemBuffer; public class AddressOfPcodeExecutorState implements PcodeExecutorStatePiece { @@ -55,4 +56,9 @@ public class AddressOfPcodeExecutorState } return unique.get(off); } + + @Override + public MemBuffer getConcreteBuffer(Address address) { + throw new AssertionError("Cannot make 'address of' concrete buffers"); + } } diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/AnnotatedSleighUseropLibrary.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/AnnotatedSleighUseropLibrary.java index 5c6ba82858..470eff8d9a 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/AnnotatedSleighUseropLibrary.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/AnnotatedSleighUseropLibrary.java @@ -16,10 +16,14 @@ package ghidra.pcode.exec; import java.lang.annotation.*; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodHandles.Lookup; +import java.lang.reflect.*; import java.util.*; +import org.apache.commons.lang3.reflect.TypeUtils; + import ghidra.program.model.pcode.Varnode; public abstract class AnnotatedSleighUseropLibrary implements SleighUseropLibrary { @@ -56,17 +60,26 @@ public abstract class AnnotatedSleighUseropLibrary implements SleighUseropLib } } - class AnnotatedSleighUseropDefinition implements SleighUseropDefinition { + static class AnnotatedSleighUseropDefinition implements SleighUseropDefinition { private final Method method; + private final MethodHandle handle; - public AnnotatedSleighUseropDefinition(Method method) { + public AnnotatedSleighUseropDefinition(AnnotatedSleighUseropLibrary library, + Class opType, Lookup lookup, Method method) { this.method = method; + try { + this.handle = lookup.unreflect(method).bindTo(library); + } + catch (IllegalAccessException e) { + throw new AssertionError("Cannot access " + method + " having @" + + SleighUserop.class.getSimpleName() + " annotation. Override getMethodLookup()"); + } for (Class ptype : method.getParameterTypes()) { if (Varnode.class.isAssignableFrom(ptype)) { continue; } - if (getOperandType().isAssignableFrom(ptype)) { + if (opType.isAssignableFrom(ptype)) { continue; } throw new IllegalArgumentException( @@ -85,45 +98,66 @@ public abstract class AnnotatedSleighUseropLibrary implements SleighUseropLib } @Override - public void execute(PcodeExecutorStatePiece state, Varnode outVar, List inVars) { + public void execute(PcodeExecutorStatePiece state, Varnode outVar, + List inVars) { // outVar is ignored - Object[] args = new Object[inVars.size()]; + List args = Arrays.asList(new Object[inVars.size()]); Class[] ptypes = method.getParameterTypes(); - for (int i = 0; i < args.length; i++) { + for (int i = 0; i < args.size(); i++) { if (Varnode.class.isAssignableFrom(ptypes[i])) { - args[i] = inVars.get(i); + args.set(i, inVars.get(i)); } else { - args[i] = state.getVar(inVars.get(i)); + args.set(i, state.getVar(inVars.get(i))); } } try { - method.invoke(AnnotatedSleighUseropLibrary.this, args); + handle.invokeWithArguments(args); } - catch (IllegalAccessException | IllegalArgumentException - | InvocationTargetException e) { - throw new AssertionError(e); + catch (PcodeExecutionException e) { + throw e; + } + catch (Throwable e) { + throw new PcodeExecutionException("Error executing userop", null, e); } } } @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) - @interface SleighUserop { + public @interface SleighUserop { } Map> ops = new HashMap<>(); public AnnotatedSleighUseropLibrary() { + Lookup lookup = getMethodLookup(); + Class opType = getOperandType(); @SuppressWarnings({ "unchecked", "rawtypes" }) Class> cls = (Class) this.getClass(); Set methods = CACHE_BY_CLASS.computeIfAbsent(cls, __ -> collectDefinitions(cls)); for (Method m : methods) { - ops.put(m.getName(), new AnnotatedSleighUseropDefinition(m)); + ops.put(m.getName(), new AnnotatedSleighUseropDefinition<>(this, opType, lookup, m)); } } - protected abstract Class getOperandType(); + @SuppressWarnings({ "unchecked", "rawtypes" }) + protected Class getOperandType() { + Map, Type> args = + TypeUtils.getTypeArguments(getClass(), AnnotatedSleighUseropLibrary.class); + if (args == null) { + return (Class) Object.class; + } + Type type = args.get(AnnotatedSleighUseropLibrary.class.getTypeParameters()[0]); + if (!(type instanceof Class)) { + return (Class) Object.class; + } + return (Class) type; + } + + protected Lookup getMethodLookup() { + return MethodHandles.lookup(); + } @Override public Map> getUserops() { diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/BigIntegerPcodeArithmetic.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/BigIntegerPcodeArithmetic.java index 1008a15b49..1bb18b9ab5 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/BigIntegerPcodeArithmetic.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/BigIntegerPcodeArithmetic.java @@ -39,8 +39,18 @@ public enum BigIntegerPcodeArithmetic implements PcodeArithmetic { return BigInteger.valueOf(value); } + @Override + public BigInteger fromConst(BigInteger value, int size) { + return value; + } + @Override public boolean isTrue(BigInteger cond) { return !cond.equals(BigInteger.ZERO); } + + @Override + public BigInteger toConcrete(BigInteger value) { + return value; + } } diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/BytesPcodeArithmetic.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/BytesPcodeArithmetic.java index 1e6a556c7f..b4ecfaf86b 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/BytesPcodeArithmetic.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/BytesPcodeArithmetic.java @@ -74,6 +74,11 @@ public enum BytesPcodeArithmetic implements PcodeArithmetic { return Utils.longToBytes(value, size, isBigEndian); } + @Override + public byte[] fromConst(BigInteger value, int size) { + return Utils.bigIntegerToBytes(value, size, isBigEndian); + } + @Override public boolean isTrue(byte[] cond) { for (byte b : cond) { @@ -83,4 +88,9 @@ public enum BytesPcodeArithmetic implements PcodeArithmetic { } return false; } + + @Override + public BigInteger toConcrete(byte[] value) { + return Utils.bytesToBigInteger(value, value.length, isBigEndian, false); + } } diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/ComposedSleighUseropLibrary.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/ComposedSleighUseropLibrary.java new file mode 100644 index 0000000000..970237ede1 --- /dev/null +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/ComposedSleighUseropLibrary.java @@ -0,0 +1,46 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.pcode.exec; + +import java.util.*; + +public class ComposedSleighUseropLibrary implements SleighUseropLibrary { + public static Map> composeUserops( + Collection> libraries) { + Map> userops = new HashMap<>(); + for (SleighUseropLibrary lib : libraries) { + for (SleighUseropDefinition def : lib.getUserops().values()) { + if (userops.put(def.getName(), def) != null) { + throw new IllegalArgumentException( + "Cannot compose libraries with conflicting definitions on " + + def.getName()); + } + } + } + return userops; + } + + private final Map> userops; + + public ComposedSleighUseropLibrary(Collection> libraries) { + this.userops = composeUserops(libraries); + } + + @Override + public Map> getUserops() { + return userops; + } +} diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/InterruptPcodeExecutionException.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/InterruptPcodeExecutionException.java new file mode 100644 index 0000000000..dc4b13f317 --- /dev/null +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/InterruptPcodeExecutionException.java @@ -0,0 +1,22 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.pcode.exec; + +public class InterruptPcodeExecutionException extends PcodeExecutionException { + public InterruptPcodeExecutionException(PcodeFrame frame, Throwable cause) { + super("Execution hit breakpoint", frame, cause); + } +} diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PairedPcodeArithmetic.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PairedPcodeArithmetic.java index 007858ab57..6b93a88509 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PairedPcodeArithmetic.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PairedPcodeArithmetic.java @@ -15,6 +15,7 @@ */ package ghidra.pcode.exec; +import java.math.BigInteger; import java.util.Map.Entry; import org.apache.commons.lang3.tuple.ImmutablePair; @@ -64,8 +65,19 @@ public class PairedPcodeArithmetic implements PcodeArithmetic> rightArith.fromConst(value, size)); } + @Override + public Pair fromConst(BigInteger value, int size) { + return new ImmutablePair<>(leftArith.fromConst(value, size), + rightArith.fromConst(value, size)); + } + @Override public boolean isTrue(Pair cond) { return leftArith.isTrue(cond.getLeft()); } + + @Override + public BigInteger toConcrete(Pair value) { + return leftArith.toConcrete(value.getLeft()); + } } 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 c2c6108c58..821bc2b7cb 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 @@ -18,8 +18,24 @@ package ghidra.pcode.exec; import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.Pair; +import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressSpace; +import ghidra.program.model.mem.MemBuffer; +/** + * A paired executor state + * + *

+ * Where a response cannot be composed of both states, the paired state defers to the left. In this + * way, the left state controls the machine, while the right is computed in tandem. The right never + * directly controls the machine; however, by overriding + * {@link #getVar(AddressSpace, Object, int, boolean)} and/or + * {@link #setVar(AddressSpace, Object, int, boolean, Object)}, the right can affect the left and + * indirectly control the machine. + * + * @param the type of values for the "left" state + * @param the type of values for the "right" state + */ public class PairedPcodeExecutorState extends AbstractOffsetTransformedPcodeExecutorState, L, Pair> implements PcodeExecutorState> { @@ -41,4 +57,9 @@ public class PairedPcodeExecutorState protected L transformOffset(Pair offset) { return offset.getLeft(); } + + @Override + public MemBuffer getConcreteBuffer(Address address) { + return left.getConcreteBuffer(address); + } } diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PairedPcodeExecutorStatePiece.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PairedPcodeExecutorStatePiece.java index 70162eed82..8acb05916d 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PairedPcodeExecutorStatePiece.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PairedPcodeExecutorStatePiece.java @@ -18,8 +18,17 @@ package ghidra.pcode.exec; import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.Pair; +import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressSpace; +import ghidra.program.model.mem.MemBuffer; +/** + * A paired executor state piece + * + * @param the type of offset, usually the type of a controlling state + * @param the type of the "left" state + * @param the type of the "right" state + */ public class PairedPcodeExecutorStatePiece implements PcodeExecutorStatePiece> { @@ -51,4 +60,9 @@ public class PairedPcodeExecutorStatePiece left.getVar(space, offset, size, truncateAddressableUnit), right.getVar(space, offset, size, truncateAddressableUnit)); } + + @Override + public MemBuffer getConcreteBuffer(Address address) { + return left.getConcreteBuffer(address); + } } diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeArithmetic.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeArithmetic.java index c0d6f2fba4..add7bf045d 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeArithmetic.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeArithmetic.java @@ -31,5 +31,25 @@ public interface PcodeArithmetic { T fromConst(long value, int size); + T fromConst(BigInteger value, int size); + + /** + * Make concrete, if possible, the given abstract condition to a boolean value + * + * @param cond the abstract condition + * @return the boolean value + */ boolean isTrue(T cond); + + /** + * Make concrete, if possible, the given abstract value + * + *

+ * If the conversion is not possible, throw an exception. TODO: Decide on conventions of which + * exception to throw and/or establish a hierarchy of checked exceptions. + * + * @param value the abstract value + * @return the concrete value + */ + BigInteger toConcrete(T value); } diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeExecutionException.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeExecutionException.java new file mode 100644 index 0000000000..2dc30213ec --- /dev/null +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeExecutionException.java @@ -0,0 +1,42 @@ +/* ### + * 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; + +public class PcodeExecutionException extends RuntimeException { + + /*package*/ PcodeFrame frame; + + public PcodeExecutionException(String message, PcodeFrame frame, Throwable cause) { + super(message, cause); + this.frame = frame; + } + + public PcodeExecutionException(String message, PcodeFrame frame) { + this(message, frame, null); + } + + public PcodeExecutionException(String message, Throwable cause) { + this(message, null, cause); + } + + public PcodeExecutionException(String message) { + this(message, null, null); + } + + public PcodeFrame getFrame() { + return frame; + } +} diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeExecutor.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeExecutor.java index 8174c4a810..b0365f9e68 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeExecutor.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeExecutor.java @@ -47,25 +47,56 @@ public class PcodeExecutor { } public void executeLine(String line) { - SleighProgram program = SleighProgramCompiler.compileProgram((SleighLanguage) language, + PcodeProgram program = SleighProgramCompiler.compileProgram((SleighLanguage) language, "line", List.of(line + ";"), SleighUseropLibrary.NIL); execute(program, SleighUseropLibrary.nil()); } - public void execute(SleighProgram program, SleighUseropLibrary library) { - execute(program.code, program.useropNames, library); + public PcodeFrame begin(PcodeProgram program) { + return begin(program.code, program.useropNames); } - public void execute(List code, Map useropNames, + public PcodeFrame execute(PcodeProgram program, SleighUseropLibrary library) { + return execute(program.code, program.useropNames, library); + } + + public PcodeFrame begin(List code, Map useropNames) { + return new PcodeFrame(language, code, useropNames); + } + + public PcodeFrame execute(List code, Map useropNames, SleighUseropLibrary library) { - PcodeFrame frame = new PcodeFrame(code); - while (!frame.isFinished()) { - step(frame, useropNames, library); + PcodeFrame frame = begin(code, useropNames); + finish(frame, library); + return frame; + } + + /** + * Finish execution of a frame + * + *

+ * TODO: This is not really sufficient for continuation after a break, esp. if that break occurs + * within a nested call back into the executor. This would likely become common when using pCode + * injection. + * + * @param frame the incomplete frame + * @param library the library of userops to use + */ + public void finish(PcodeFrame frame, SleighUseropLibrary library) { + try { + while (!frame.isFinished()) { + step(frame, library); + } + } + catch (PcodeExecutionException e) { + if (e.frame == null) { + e.frame = frame; + } + throw e; } } - public void stepOp(PcodeOp op, PcodeFrame frame, Map useropNames, - SleighUseropLibrary library) { + public void stepOp(PcodeOp op, PcodeFrame frame, SleighUseropLibrary library) { OpBehavior b = OpBehaviorFactory.getOpBehavior(op.getOpcode()); if (b == null) { throw new LowlevelError("Unsupported pcode op" + op); @@ -113,7 +144,7 @@ public class PcodeExecutor { executeIndirectCall(op, frame); return; case PcodeOp.CALLOTHER: - executeCallother(op, useropNames, library); + executeCallother(op, frame, library); return; case PcodeOp.RETURN: executeReturn(op, frame); @@ -123,9 +154,17 @@ public class PcodeExecutor { } } - public void step(PcodeFrame frame, Map useropNames, - SleighUseropLibrary library) { - stepOp(frame.nextOp(), frame, useropNames, library); + public void step(PcodeFrame frame, SleighUseropLibrary library) { + try { + stepOp(frame.nextOp(), frame, library); + } + catch (PcodeExecutionException e) { + e.frame = frame; + throw e; + } + catch (Exception e) { + throw new PcodeExecutionException("Exception during pcode execution", frame, e); + } } protected int getIntConst(Varnode vn) { @@ -151,7 +190,19 @@ public class PcodeExecutor { state.setVar(space, offset, valVar.getSize(), true, val); } - protected void branchTo(T offset, PcodeFrame frame) { + /** + * Called when execution branches to a target address + * + *

+ * NOTE: This is not called for the fall-through case + * + * @param target the target address + */ + protected void branchToAddress(Address target) { + // Extension point + } + + protected void branchToOffset(T offset, PcodeFrame frame) { state.setVar(pc.getAddressSpace(), pc.getOffset(), (pc.getBitLength() + 7) / 8, false, offset); frame.finishAsBranch(); @@ -163,7 +214,8 @@ public class PcodeExecutor { frame.branch((int) target.getOffset()); } else { - branchTo(arithmetic.fromConst(target.getOffset(), pointerSize), frame); + branchToOffset(arithmetic.fromConst(target.getOffset(), pointerSize), frame); + branchToAddress(target); } } @@ -177,22 +229,33 @@ public class PcodeExecutor { public void executeIndirectBranch(PcodeOp op, PcodeFrame frame) { T offset = state.getVar(op.getInput(0)); - branchTo(offset, frame); + branchToOffset(offset, frame); + + long concrete = arithmetic.toConcrete(offset).longValue(); + Address target = op.getSeqnum().getTarget().getNewAddress(concrete); + branchToAddress(target); } public void executeCall(PcodeOp op, PcodeFrame frame) { Address target = op.getInput(0).getAddress(); - branchTo(arithmetic.fromConst(target.getOffset(), pointerSize), frame); + branchToOffset(arithmetic.fromConst(target.getOffset(), pointerSize), frame); + branchToAddress(target); } public void executeIndirectCall(PcodeOp op, PcodeFrame frame) { executeIndirectBranch(op, frame); } - public void executeCallother(PcodeOp op, Map useropNames, - SleighUseropLibrary library) { + public String getUseropName(int opNo, PcodeFrame frame) { + if (opNo < language.getNumberOfUserDefinedOpNames()) { + return language.getUserDefinedOpName(opNo); + } + return frame.getUseropName(opNo); + } + + public void executeCallother(PcodeOp op, PcodeFrame frame, SleighUseropLibrary library) { int opNo = getIntConst(op.getInput(0)); - String opName = useropNames.get(opNo); + String opName = getUseropName(opNo, frame); if (opName == null) { throw new AssertionError( "Pcode userop " + opNo + " is not defined"); @@ -200,7 +263,7 @@ public class PcodeExecutor { SleighUseropDefinition opDef = library.getUserops().get(opName); if (opDef == null) { throw new SleighLinkException( - "Sleigh userop " + opName + " is not in the library " + library); + "Sleigh userop '" + opName + "' is not in the library " + library); } opDef.execute(state, op.getOutput(), List.of(op.getInputs()).subList(1, op.getNumInputs())); } 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 55afd4c956..8489ea24ee 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 @@ -18,6 +18,7 @@ package ghidra.pcode.exec; import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressSpace; import ghidra.program.model.lang.Register; +import ghidra.program.model.mem.MemBuffer; import ghidra.program.model.pcode.Varnode; public interface PcodeExecutorStatePiece { @@ -57,4 +58,6 @@ public interface PcodeExecutorStatePiece { default T getVar(AddressSpace space, long offset, int size, boolean truncateAddressableUnit) { return getVar(space, longToOffset(space, offset), size, truncateAddressableUnit); } + + MemBuffer getConcreteBuffer(Address address); } diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeFrame.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeFrame.java index 00e02cdbf1..dfac2d22f7 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeFrame.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeFrame.java @@ -16,16 +16,66 @@ package ghidra.pcode.exec; import java.util.List; +import java.util.Map; import ghidra.pcode.error.LowlevelError; +import ghidra.program.model.lang.Language; import ghidra.program.model.pcode.PcodeOp; public class PcodeFrame { + private final Language language; private final List code; - private int index = 0; + private final Map useropNames; - public PcodeFrame(List code) { + private int index = 0; + private int branched = -1; + + /** + * Construct a frame of p-code execution + * + *

+ * The passed in code should be an immutable list. It is returned directly by + * {@link #getCode()}, which would otherwise allow mutation. The frame does not create its own + * immutable copy as a matter of efficiency. Instead, the provider of the code should create an + * immutable copy, probably once, e.g., when compiling a {@link PcodeProgram}. + * + * @param language the language to which the program applies + * @param code the program's p-code + * @param useropNames a map of additional sleigh/p-code userops linked to the program + */ + public PcodeFrame(Language language, List code, Map useropNames) { + this.language = language; this.code = code; + this.useropNames = useropNames; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(""); + } + else if (i == index) { + sb.append(" ->"); + } + else { + sb.append(" "); + } + PcodeOp op = code.get(i); + sb.append(op.getSeqnum() + ": " + PcodeProgram.opToString(language, op, false)); + } + if (index == code.size()) { + sb.append("\n *> fall-through"); + } + sb.append("\n}>"); + return sb.toString(); } public int index() { @@ -40,6 +90,14 @@ public class PcodeFrame { return index++; } + public int stepBack() { + return index--; + } + + public String getUseropName(int userop) { + return useropNames.get(userop); + } + public boolean isFallThrough() { return index == code.size(); } @@ -60,6 +118,29 @@ public class PcodeFrame { } public void finishAsBranch() { + branched = index - 1; // -1 because we already advanced index = -1; } + + public List getCode() { + return code; + } + + public PcodeOp[] copyCode() { + return code.toArray(PcodeOp[]::new); + } + + /** + * Get the index of the last (branch) op executed + * + *

+ * The behavior here is a bit strange for compatibility with the established concrete emulator. + * If the program (instruction) completed with fall-through, then this will return -1. If it + * completed on a branch, then this will return the index of that branch. + * + * @return + */ + public int getBranched() { + return branched; + } } diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeProgram.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeProgram.java new file mode 100644 index 0000000000..d6c95bd9a7 --- /dev/null +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeProgram.java @@ -0,0 +1,216 @@ +/* ### + * 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.util.*; + +import ghidra.app.plugin.processors.sleigh.SleighLanguage; +import ghidra.pcodeCPort.slghsymbol.UserOpSymbol; +import ghidra.program.model.address.AddressSpace; +import ghidra.program.model.lang.Language; +import ghidra.program.model.lang.Register; +import ghidra.program.model.listing.Instruction; +import ghidra.program.model.pcode.PcodeOp; +import ghidra.program.model.pcode.Varnode; +import ghidra.util.HTMLUtilities; + +public class PcodeProgram { + protected static String htmlSpan(String cls, String display) { + return String.format("%s", cls, + HTMLUtilities.escapeHTML(display)); + } + + public static String registerToString(Register reg, boolean markup) { + if (markup) { + return htmlSpan("register", reg.toString()); + } + else { + return reg.toString(); + } + } + + public static String constToString(Varnode cvn, boolean markup) { + String display = String.format("%d:%d", cvn.getOffset(), cvn.getSize()); + if (markup) { + return htmlSpan("constant", display); + } + else { + return display; + } + } + + public static String uniqueToString(Varnode uvn, boolean markup) { + String display = String.format("$U%s:%d", + uvn.getAddress().getOffsetAsBigInteger().toString(16), uvn.getSize()); + if (markup) { + return htmlSpan("unique", display); + } + else { + return display; + } + } + + public static String addressToString(Varnode avn, boolean markup) { + String display = String.format("%s:%d", avn.getAddress().toString(true), avn.getSize()); + if (markup) { + return htmlSpan("address", display); + } + else { + return display; + } + } + + public static String vnToString(Language language, Varnode vn, boolean markup) { + Register reg = + language.getRegister(vn.getAddress().getAddressSpace(), vn.getOffset(), vn.getSize()); + if (reg != null) { + return registerToString(reg, markup); + } + if (vn.isConstant()) { + return constToString(vn, markup); + } + if (vn.isUnique()) { + return uniqueToString(vn, markup); + } + return addressToString(vn, markup); + } + + public static String spaceToString(Language language, Varnode vn, boolean markup) { + if (!vn.isConstant()) { + throw new IllegalArgumentException("space id must be a constant varnode"); + } + AddressSpace space = language.getAddressFactory().getAddressSpace((int) vn.getOffset()); + String display = space == null ? "" : space.getName(); + if (markup) { + return htmlSpan("space", display); + } + else { + return display; + } + } + + public static String useropToString(Language language, Varnode vn, boolean markup) { + if (!vn.isConstant()) { + throw new IllegalArgumentException("userop index must be a constant varnode"); + } + String display = "\"" + language.getUserDefinedOpName((int) vn.getOffset()) + "\""; + if (markup) { + return htmlSpan("userop", display); + } + else { + return display; + } + } + + public static String opCodeToString(Language language, int op, boolean markup) { + if (markup) { + return htmlSpan("op", PcodeOp.getMnemonic(op)); + } + else { + return PcodeOp.getMnemonic(op); + } + } + + public static String opToString(Language language, PcodeOp op, boolean markup) { + StringBuilder sb = new StringBuilder(); + Varnode output = op.getOutput(); + if (output != null) { + sb.append(vnToString(language, output, markup)); + sb.append(" = "); + } + int opcode = op.getOpcode(); + sb.append(opCodeToString(language, opcode, markup)); + boolean isDeref = opcode == PcodeOp.LOAD || opcode == PcodeOp.STORE; + boolean isUserop = opcode == PcodeOp.CALLOTHER; + int i; + if (isDeref) { + sb.append(' '); + sb.append(spaceToString(language, op.getInput(0), markup)); + sb.append('('); + sb.append(vnToString(language, op.getInput(1), markup)); + sb.append(')'); + i = 2; + } + else if (isUserop) { + sb.append(' '); + sb.append(useropToString(language, op.getInput(0), markup)); + i = 1; + } + else { + i = 0; + } + for (; i < op.getNumInputs(); i++) { + if (i != 0) { + sb.append(','); + } + sb.append(' '); + sb.append(vnToString(language, op.getInput(i), markup)); + } + return sb.toString(); + } + + public static PcodeProgram fromInstruction(Instruction instruction) { + Language language = instruction.getPrototype().getLanguage(); + if (!(language instanceof SleighLanguage)) { + throw new IllegalArgumentException("Instruction must be parsed using Sleigh"); + } + PcodeOp[] pcode = instruction.getPcode(false); + return new PcodeProgram((SleighLanguage) language, List.of(pcode), + Map.of()); + } + + protected final SleighLanguage language; + protected final List code; + protected final Map useropNames = new HashMap<>(); + + protected PcodeProgram(SleighLanguage language, List code, + Map useropSymbols) { + this.language = language; + this.code = code; + int langOpCount = language.getNumberOfUserDefinedOpNames(); + for (Map.Entry ent : useropSymbols.entrySet()) { + int index = ent.getKey(); + if (index < langOpCount) { + useropNames.put(index, language.getUserDefinedOpName(index)); + } + else { + useropNames.put(index, ent.getValue().getName()); + } + } + } + + public SleighLanguage getLanguage() { + return language; + } + + public void execute(PcodeExecutor executor, SleighUseropLibrary library) { + executor.execute(this, library); + } + + protected String getHead() { + return getClass().getSimpleName(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("<" + getHead() + ":"); + for (PcodeOp op : code) { + sb.append("\n " + op.getSeqnum() + ": " + opToString(language, op, false)); + } + sb.append("\n>"); + return sb.toString(); + } +} diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/SleighExpression.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/SleighExpression.java index aaf1b4023b..fa022bddec 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/SleighExpression.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/SleighExpression.java @@ -22,7 +22,7 @@ import ghidra.app.plugin.processors.sleigh.SleighLanguage; import ghidra.pcodeCPort.slghsymbol.UserOpSymbol; import ghidra.program.model.pcode.PcodeOp; -public class SleighExpression extends SleighProgram { +public class SleighExpression extends PcodeProgram { public static final String RESULT_NAME = "___result"; protected static final SleighUseropLibrary CAPTURING = new ValueCapturingSleighUseropLibrary<>(); @@ -31,12 +31,6 @@ public class SleighExpression extends SleighProgram { extends AnnotatedSleighUseropLibrary { T result; - @Override - @SuppressWarnings({ "unchecked", "rawtypes" }) - protected Class getOperandType() { - return (Class) Object.class; - } - @SleighUserop public void ___result(T result) { this.result = result; diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/SleighProgram.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/SleighProgram.java deleted file mode 100644 index dc38bb8210..0000000000 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/SleighProgram.java +++ /dev/null @@ -1,66 +0,0 @@ -/* ### - * IP: GHIDRA - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package ghidra.pcode.exec; - -import java.util.*; - -import ghidra.app.plugin.processors.sleigh.SleighLanguage; -import ghidra.pcodeCPort.slghsymbol.UserOpSymbol; -import ghidra.program.model.pcode.PcodeOp; - -public class SleighProgram { - protected final SleighLanguage language; - protected final List code; - protected final Map useropNames = new HashMap<>(); - - protected SleighProgram(SleighLanguage language, List code, - Map useropSymbols) { - this.language = language; - this.code = code; - int langOpCount = language.getNumberOfUserDefinedOpNames(); - for (Map.Entry ent : useropSymbols.entrySet()) { - int index = ent.getKey(); - if (index < langOpCount) { - useropNames.put(index, language.getUserDefinedOpName(index)); - } - else { - useropNames.put(index, ent.getValue().getName()); - } - } - } - - public SleighLanguage getLanguage() { - return language; - } - - public void execute(PcodeExecutor executor, SleighUseropLibrary library) { - executor.execute(this, library); - } - - protected String getHead() { - return getClass().getSimpleName(); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder("<" + getHead() + ":"); - for (PcodeOp op : code) { - sb.append("\n " + op.getSeqnum() + ": " + op); - } - sb.append("\n>"); - return sb.toString(); - } -} diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/SleighProgramCompiler.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/SleighProgramCompiler.java index 3b90a40eae..bfc57a0610 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/SleighProgramCompiler.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/SleighProgramCompiler.java @@ -80,15 +80,22 @@ public class SleighProgramCompiler { return List.of(emit.getPcodeOp()); } + /** + * Add extra user-op symbols to the parser's table + * + *

+ * The map cannot contain symbols whose user-op indices are already defined by the language. + * + * @param parser the parser to modify + * @param symbols the map of extra symbols + */ protected static void addParserSymbols(PcodeParser parser, Map symbols) { for (UserOpSymbol sym : symbols.values()) { - if (sym != null) { - parser.addSymbol(sym); - } + parser.addSymbol(sym); } } - public static SleighProgram compileProgram(SleighLanguage language, String sourceName, + public static PcodeProgram compileProgram(SleighLanguage language, String sourceName, List lines, SleighUseropLibrary library) { PcodeParser parser = createParser(language); Map symbols = library.getSymbols(language); @@ -97,7 +104,7 @@ public class SleighProgramCompiler { ConstructTpl template = compileTemplate(language, parser, sourceName, StringUtils.join(lines, "\n")); try { - return new SleighProgram(language, buildOps(language, template), symbols); + return new PcodeProgram(language, buildOps(language, template), symbols); } catch (UnknownInstructionException | MemoryAccessException e) { throw new AssertionError(e); diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/SleighUseropLibrary.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/SleighUseropLibrary.java index e82c9b77b7..f6e56f2a88 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/SleighUseropLibrary.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/SleighUseropLibrary.java @@ -23,12 +23,14 @@ import ghidra.program.model.pcode.Varnode; import ghidra.sleigh.grammar.Location; public interface SleighUseropLibrary { - public static final SleighUseropLibrary NIL = new SleighUseropLibrary() { + final class EmptySleighUseropLibrary implements SleighUseropLibrary { @Override public Map> getUserops() { return Map.of(); } - }; + } + + SleighUseropLibrary NIL = new EmptySleighUseropLibrary(); @SuppressWarnings("unchecked") public static SleighUseropLibrary nil() { @@ -45,33 +47,41 @@ public interface SleighUseropLibrary { Map> getUserops(); + default SleighUseropLibrary compose(SleighUseropLibrary lib) { + if (lib == null) { + return this; + } + return new ComposedSleighUseropLibrary<>(List.of(this, lib)); + } + + /** + * Get named symbols defined by this library that are not already defined in the language + * + * @param language the language whose existing symbols to consider + * @return a map of new user-op indices to extra user-op symbols + */ default Map getSymbols(SleighLanguage language) { - Map langDefedOps = new HashMap<>(); + //Set langDefedNames = new HashSet<>(); Map symbols = new HashMap<>(); Set allNames = new HashSet<>(); int langOpCount = language.getNumberOfUserDefinedOpNames(); for (int i = 0; i < langOpCount; i++) { String name = language.getUserDefinedOpName(i); - langDefedOps.put(name, i); + allNames.add(name); } int nextOpNo = langOpCount; - for (SleighUseropDefinition uop : getUserops().values()) { + for (SleighUseropDefinition uop : new TreeMap<>(getUserops()).values()) { String opName = uop.getName(); if (!allNames.add(opName)) { - // Will emit warning at execute + // Real duplicates will cause a warning during execution continue; } - Integer langOpNo = langDefedOps.get(opName); - if (langOpNo != null) { - symbols.put(langOpNo, null); - } - else { - int opNo = nextOpNo++; - Location loc = new Location(getClass().getName() + ":" + opName, 0); - UserOpSymbol sym = new UserOpSymbol(loc, opName); - sym.setIndex(opNo); - symbols.put(opNo, sym); - } + + int opNo = nextOpNo++; + Location loc = new Location(getClass().getName() + ":" + opName, 0); + UserOpSymbol sym = new UserOpSymbol(loc, opName); + sym.setIndex(opNo); + symbols.put(opNo, sym); } return symbols; } diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/SuspendedPcodeExecutionException.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/SuspendedPcodeExecutionException.java new file mode 100644 index 0000000000..8e79424716 --- /dev/null +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/SuspendedPcodeExecutionException.java @@ -0,0 +1,22 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.pcode.exec; + +public class SuspendedPcodeExecutionException extends PcodeExecutionException { + public SuspendedPcodeExecutionException(PcodeFrame frame, Throwable cause) { + super("Execution suspended by user", frame, cause); + } +} diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/AbstractAddressSetView.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/AbstractAddressSetView.java index 03cde6e880..03c53d414a 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/AbstractAddressSetView.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/AbstractAddressSetView.java @@ -16,6 +16,7 @@ package ghidra.util; import java.util.Iterator; +import java.util.List; import ghidra.program.model.address.*; @@ -139,13 +140,16 @@ public abstract class AbstractAddressSetView implements AddressSetView { @Override public boolean intersects(AddressSetView addrSet) { AddressRangeIterator iit = - AddressRangeIterators.intersect(this.iterator(), addrSet.iterator(), true); + AddressRangeIterators.intersect(this.iterator(addrSet.getMinAddress(), true), + addrSet.iterator(this.getMinAddress(), true), true); return iit.hasNext(); } @Override public boolean intersects(Address start, Address end) { - return intersects(new AddressSet(start, end)); + AddressRangeIterator iit = AddressRangeIterators.intersect(this.iterator(start, true), + List.of((AddressRange) new AddressRangeImpl(start, end)).iterator(), true); + return iit.hasNext(); } @Override diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/MergeSortingIterator.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/MergeSortingIterator.java index 618305edb7..bcd421ff99 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/MergeSortingIterator.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/MergeSortingIterator.java @@ -25,6 +25,7 @@ import generic.util.PeekableIterator; /** * An iterator which merges sorted iterators according to a comparator * + *

* TODO: This may be replaceable with {@link Iterators#mergeSorted(Iterable, Comparator)}. I * hesitate, since I benefit from this implementation complying with {@link PeekableIterator}, while * Guava's does not -- though, they would use {@link PeekingIterator} instead. Currently, my @@ -97,6 +98,7 @@ public class MergeSortingIterator implements PeekableIterator { /** * Construct a merge-sorting iterator which generates labeled values * + *

* The map of iterators is a map of entries, each giving a label and an iterator to be merged. * Each iterator must return values as sorted by the given comparator. The entries returned by * the combined iterator give the values in sorted order, but each has a the key indicating diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/UniqIterator.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/UniqIterator.java new file mode 100644 index 0000000000..dad6ded804 --- /dev/null +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/UniqIterator.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.util; + +import java.util.Iterator; +import java.util.Objects; + +import generic.util.PeekableIterator; + +/** + * A filtering iterator which removes repeated objects + * + *

+ * This operates in style to the uniq command on UNIX, which only removes immediate repeats. To + * obtain a truly unique iteration, the wrapped iterator must visit elements in sorted order. + * + * @param the type of elements + */ +public class UniqIterator extends AbstractPeekableIterator { + protected boolean first; + protected T last; + protected final PeekableIterator wrapped; + + public UniqIterator(Iterator wrapped) { + this.wrapped = PeekableIterators.castOrWrap(wrapped); + } + + @Override + protected T seekNext() { + if (first) { + first = false; + return last = wrapped.hasNext() ? wrapped.peek() : null; + } + while (wrapped.hasNext() && Objects.equals(last, wrapped.peek())) { + wrapped.next(); + } + return last = wrapped.hasNext() ? wrapped.peek() : null; + } +}