diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/control/ControlMode.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/control/ControlMode.java index cf4e4d26f7..7bad8baae5 100644 --- a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/control/ControlMode.java +++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/control/ControlMode.java @@ -24,7 +24,6 @@ import javax.swing.Icon; import db.Transaction; import generic.theme.GIcon; -import ghidra.app.services.DebuggerEmulationService; import ghidra.app.services.DebuggerTraceManagerService; import ghidra.app.services.DebuggerTraceManagerService.ActivationCause; import ghidra.debug.api.target.Target; @@ -45,8 +44,6 @@ import ghidra.trace.model.time.schedule.PatchStep; import ghidra.trace.model.time.schedule.TraceSchedule; import ghidra.trace.model.time.schedule.TraceSchedule.ScheduleForm; import ghidra.trace.util.TraceRegisterUtils; -import ghidra.util.exception.CancelledException; -import ghidra.util.task.TaskMonitor; /** * The control / state editing modes @@ -332,22 +329,7 @@ public enum ControlMode { DebuggerTraceManagerService traceManager = Objects.requireNonNull(tool.getService(DebuggerTraceManagerService.class), "No trace manager service"); - Long found = traceManager.findSnapshot(withTime); - // Materialize it on the same thread (even if swing) - // It shouldn't take long, since we're only appending one step. - if (found == null) { - // TODO: Could still do it async on another thread, no? - // Not sure it buys anything, since program view will call .get on swing thread - DebuggerEmulationService emulationService = Objects.requireNonNull( - tool.getService(DebuggerEmulationService.class), "No emulation service"); - try { - emulationService.emulate(coordinates.getPlatform(), time, - TaskMonitor.DUMMY); - } - catch (CancelledException e) { - throw new AssertionError(e); - } - } + return traceManager.activateAndNotify(withTime, ActivationCause.EMU_STATE_EDIT); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/DebuggerEmulationServicePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/DebuggerEmulationServicePlugin.java index 81ec0d7532..196a53d905 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/DebuggerEmulationServicePlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/DebuggerEmulationServicePlugin.java @@ -687,9 +687,16 @@ public class DebuggerEmulationServicePlugin extends Plugin implements DebuggerEm TracePlatform platform = key.platform; TraceSchedule time = key.time; - Map.Entry ancestor = findNearestPrefix(key); - if (ancestor != null) { - CacheKey prevKey = ancestor.getKey(); + TraceSnapshot tracePrefix = trace.getTimeManager().findSnapshotWithNearestPrefix(time); + if (tracePrefix.getSchedule().isSnapOnly()) { + tracePrefix = null; + } + Map.Entry cachePrefix = findNearestPrefix(key); + if (cachePrefix != null && (tracePrefix == null || + cachePrefix.getKey().time.compareTo(tracePrefix.getSchedule()) >= 0)) { + CacheKey prevKey = cachePrefix.getKey(); + + Msg.debug(this, "Using cached emulator at %s".formatted(prevKey.time)); synchronized (cache) { cache.remove(prevKey); @@ -698,7 +705,7 @@ public class DebuggerEmulationServicePlugin extends Plugin implements DebuggerEm // TODO: Handle errors, and add to proper place in cache? // TODO: Finish partially-executed instructions? - try (BusyEmu be = new BusyEmu(ancestor.getValue())) { + try (BusyEmu be = new BusyEmu(cachePrefix.getValue())) { PcodeMachine emu = be.ce.emulator(); emu.clearAllInjects(); @@ -713,18 +720,28 @@ public class DebuggerEmulationServicePlugin extends Plugin implements DebuggerEm return be.dup(); } } + Target target = targetService == null ? null : targetService.getTarget(trace); - DefaultPcodeDebuggerAccess from = - new DefaultPcodeDebuggerAccess(tool, target, platform, time.getSnap()); + DefaultPcodeDebuggerAccess from = new DefaultPcodeDebuggerAccess(tool, target, platform, + tracePrefix != null ? tracePrefix.getKey() : time.getSnap(), time.getSnap()); Writer writer = DebuggerEmulationIntegration.bytesDelayedWriteTrace(from); PcodeMachine emu = emulatorFactory.create(from, writer); try (BusyEmu be = new BusyEmu(new CachedEmulator(key.trace, emu, writer))) { installBreakpoints(key.trace, key.time.getSnap(), be.ce.emulator()); - monitor.initialize(time.totalTickCount()); + monitor.initialize(time.totalTickCount() - + (tracePrefix != null ? tracePrefix.getSchedule().totalTickCount() : 0)); createRegisterSpaces(trace, time, monitor); monitor.setMessage("Emulating"); - time.execute(trace, emu, monitor); + if (tracePrefix != null) { + Msg.debug(this, "Using new emulator from scratch snapshot %s" + .formatted(tracePrefix.getScheduleString())); + time.finish(trace, tracePrefix.getSchedule(), emu, monitor); + } + else { + Msg.debug(this, "Using new emulator from snap %d".formatted(time.getSnap())); + time.execute(trace, emu, monitor); + } return be.dup(); } } @@ -753,6 +770,7 @@ public class DebuggerEmulationServicePlugin extends Plugin implements DebuggerEm ce.writer().writeDown(into); TraceThread lastThread = key.time.getLastThread(key.trace); destSnap.setEventThread(lastThread); + destSnap.setVersion(key.trace.getEmulatorCacheVersion()); } catch (Throwable e) { Msg.showError(this, null, "Emulate", 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 5ecd2c01a3..c5aa671421 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 @@ -16,8 +16,7 @@ package ghidra.trace.database.time; import java.io.IOException; -import java.util.Collection; -import java.util.Collections; +import java.util.*; import java.util.Map.Entry; import java.util.concurrent.locks.ReadWriteLock; @@ -31,7 +30,7 @@ import ghidra.trace.model.Lifespan; import ghidra.trace.model.target.TraceObjectValue; import ghidra.trace.model.time.TraceSnapshot; import ghidra.trace.model.time.TraceTimeManager; -import ghidra.trace.model.time.schedule.TraceSchedule; +import ghidra.trace.model.time.schedule.*; import ghidra.trace.model.time.schedule.TraceSchedule.TimeRadix; import ghidra.trace.util.TraceChangeRecord; import ghidra.trace.util.TraceEvents; @@ -157,6 +156,94 @@ public class DBTraceTimeManager implements TraceTimeManager, DBTraceManager { return snapshot; } + protected DBTraceSnapshot doGetValidSnapshotBySchedule(String key, long version) { + for (DBTraceSnapshot snapshot : snapshotsBySchedule.get(key)) { + if (snapshot.getVersion() >= version) { + return snapshot; + } + } + return null; + } + + protected DBTraceSnapshot doFindNearest(TraceSchedule schedule, long version) { + // Base case + if (schedule.isSnapOnly()) { + return snapshotStore.getObjectAt(schedule.getSnap()); // may be null + } + + // Inductive case + DBTraceSnapshot best = null; + TraceSchedule dropped = schedule.dropLastStep(); + String strDropped = dropped.toString(); + for (Iterator it = + snapshotsBySchedule.tail(strDropped, true).keys().iterator(); it.hasNext();) { + String key = it.next(); + if (!key.startsWith(strDropped)) { + break; + } + TraceSchedule candidateSchedule = TraceSchedule.parse(key); + // We're in a chunk of too-advanced (and probably unrelated) schedules + if (candidateSchedule.stepCount() > schedule.stepCount()) { + TraceSchedule candidateTrunc = + candidateSchedule.truncateToSteps(schedule.stepCount()); + Step candidateStep = candidateTrunc.lastStep().step(); + TraceSchedule candidateDropped = candidateTrunc.dropLastStep(); + // Hack the lexicographic indexing + String extra = switch (candidateStep) { + case PatchStep s -> "t%d-%c".formatted(s.getThreadKey(), '{' + 1); + default -> "%s%c".formatted(candidateStep, ';' + 1); + }; + String newTailKey = (candidateDropped.isSnapOnly() ? "%s:%s" : "%s;%s") + .formatted(candidateDropped, extra); + it = snapshotsBySchedule.tail(newTailKey, true).keys().iterator(); + continue; + } + + // We have a potential nearest. Must be related and less than, but better than best + CompareResult cmp = candidateSchedule.compareSchedule(schedule); + if (!cmp.related || cmp.compareTo > 0) { + continue; + } + if (best != null && best.getSchedule().compareTo(candidateSchedule) >= 0) { + continue; + } + // Checking validity requires loading the record. Do this filter last. + DBTraceSnapshot valid = doGetValidSnapshotBySchedule(key, version); + if (valid != null) { + best = valid; + } + } + + if (best != null) { + return best; + } + + return doFindNearest(dropped, version); + } + + /** + * {@inheritDoc} + * + * @implNote Because the index is lexicographic, we have to hack a bit. Consider that 20 would + * come before 3 in the index. That said, all the steps leading up to the last would + * have to be equal for it to be a prefix, so I don't think any weird lexicographic + * stuff comes into play except in the final step. + * @implNote Even if the index were numeric, we have to worry about non-related schedules + * appearing between related ones, e.g., {@code 0:t0-2, 0:t0-3;t1-1}, when searching + * for {@code 0:t0-4}. + */ + @Override + public TraceSnapshot findSnapshotWithNearestPrefix(TraceSchedule schedule) { + long version = trace.getEmulatorCacheVersion(); + Optional exists = getSnapshotsWithSchedule(schedule).stream() + .filter(s -> s.getVersion() >= version) + .findFirst(); + if (exists.isPresent()) { + return exists.get(); + } + return doFindNearest(schedule.dropPSteps(), version); + } + @Override public Collection getAllSnapshots() { return Collections.unmodifiableCollection(snapshotStore.asMap().values()); 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 85a3ad2a1a..2ccc77f3f9 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 @@ -17,6 +17,7 @@ package ghidra.trace.model.time; import java.util.Collection; +import ghidra.trace.model.Trace; import ghidra.trace.model.time.schedule.TraceSchedule; import ghidra.trace.model.time.schedule.TraceSchedule.TimeRadix; @@ -76,6 +77,23 @@ public interface TraceTimeManager { */ TraceSnapshot findScratchSnapshot(TraceSchedule schedule); + /** + * Find the nearest related snapshot whose schedule is a prefix of the given schedule + * + *

+ * This finds a snapshot that can be used as the initial state of an emulator to materialize the + * state at the given schedule. The one it returns is the one that would require the fewest + * instruction steps. Note that since an emulator cannot be initialized into the middle of an + * instruction, snapshots whose schedules contain p-code op steps are ignored. Additionally, + * this will ignore any snapshots whose version is less than the emulator cache version. + * + * @param schedule the desired schedule + * @param allowOpSteps whether to include snapshots with p-code op steps + * @return the found snapshot, or null + * @see Trace#getEmulatorCacheVersion() + */ + TraceSnapshot findSnapshotWithNearestPrefix(TraceSchedule schedule); + /** * List all snapshots in the trace * @@ -129,5 +147,4 @@ public interface TraceTimeManager { * @return radix the radix */ TimeRadix getTimeRadix(); - } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/Sequence.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/Sequence.java index 74f6c8b4b7..ae75c2680e 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/Sequence.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/Sequence.java @@ -200,6 +200,35 @@ public class Sequence implements Comparable { return Long.max(0, count); } + /** + * Drop the last step from this sequence + * + * @return the sequence with the last step removed + */ + public Sequence dropLast() { + if (steps.isEmpty()) { + throw new NoSuchElementException(); + } + return new Sequence(new ArrayList<>(steps.subList(0, steps.size() - 1))); + } + + /** + * {@return the last step} + */ + public Step last() { + return steps.getLast(); + } + + /** + * Truncate this sequence to the first count steps + * + * @param count the count + * @return the new sequence + */ + public Sequence truncate(int count) { + return new Sequence(new ArrayList<>(steps.subList(0, count))); + } + @Override public Sequence clone() { return new Sequence( @@ -280,24 +309,22 @@ public class Sequence implements Comparable { Step s2 = that.steps.get(i); result = s1.compareStep(s2); switch (result) { - case UNREL_LT: - case UNREL_GT: + case UNREL_LT, 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 + } + case REL_LT -> { + return i + 1 == this.steps.size() + ? CompareResult.REL_LT + : CompareResult.UNREL_LT; + } + case REL_GT -> { + return i + 1 == that.steps.size() + ? CompareResult.REL_GT + : CompareResult.UNREL_GT; + } + default -> { + // EQUALS, next step + } } } if (that.steps.size() > min) { @@ -360,6 +387,10 @@ public class Sequence implements Comparable { return count; } + public int count() { + return steps.size(); + } + public long totalSkipCount() { long count = 0; for (Step step : steps) { diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/StepKind.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/StepKind.java new file mode 100644 index 0000000000..046c2ef602 --- /dev/null +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/StepKind.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.trace.model.time.schedule; + +import ghidra.pcode.emu.PcodeThread; + +public enum StepKind implements Stepper { + INSTRUCTION { + @Override + public void tick(PcodeThread thread) { + thread.stepInstruction(); + } + + @Override + public void skip(PcodeThread thread) { + thread.skipInstruction(); + } + }, + PCODE { + @Override + public void tick(PcodeThread thread) { + thread.stepPcodeOp(); + } + + @Override + public void skip(PcodeThread thread) { + thread.skipPcodeOp(); + } + }; +} diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/Stepper.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/Stepper.java index 9445b40c0c..da4ef47a5d 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/Stepper.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/Stepper.java @@ -4,9 +4,9 @@ * 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. @@ -18,37 +18,12 @@ package ghidra.trace.model.time.schedule; import ghidra.pcode.emu.PcodeThread; public interface Stepper { - enum Enum implements Stepper { - INSTRUCTION { - @Override - public void tick(PcodeThread thread) { - thread.stepInstruction(); - } - - @Override - public void skip(PcodeThread thread) { - thread.skipInstruction(); - } - }, - PCODE { - @Override - public void tick(PcodeThread thread) { - thread.stepPcodeOp(); - } - - @Override - public void skip(PcodeThread thread) { - thread.skipPcodeOp(); - } - }; - } - static Stepper instruction() { - return Enum.INSTRUCTION; + return StepKind.INSTRUCTION; } static Stepper pcode() { - return Enum.PCODE; + return StepKind.PCODE; } void tick(PcodeThread thread); diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/TraceSchedule.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/TraceSchedule.java index 93898b0ab6..8c43a44d90 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/TraceSchedule.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/TraceSchedule.java @@ -486,6 +486,15 @@ public class TraceSchedule implements Comparable { return !steps.isNop(); } + /** + * Check if this schedule has p-code steps + * + * @return true if this indicates at least one instruction step + */ + public boolean hasPSteps() { + return !pSteps.isNop(); + } + /** * Get the source snapshot * @@ -580,6 +589,15 @@ public class TraceSchedule implements Comparable { return steps.totalTickCount(); } + /** + * Count the number of steps, excluding p-code steps + * + * @return the number of steps + */ + public int stepCount() { + return steps.count(); + } + /** * Compute the number of patches, excluding p-code patches * @@ -884,6 +902,51 @@ public class TraceSchedule implements Comparable { return new TraceSchedule(this.snap, this.steps, new Sequence()); } + /** + * Drop the last step + * + *

+ * If there are p-code steps, this drops the last step there. Otherwise, this drops the last + * step from the instruction steps. A step includes all ticks in the step, e.g., + * {@code 0:t0-20;t1-5} becomes {@code 0:t0-20}. To remove a specific number of ticks, see + * {@link TraceSchedule#steppedBackward(Trace, long)}. + * + * @return the schedule with the last step removed + * @throws NoSuchElementException If there are neither instruction nor p-code steps. + */ + public TraceSchedule dropLastStep() { + if (!this.pSteps.isNop()) { + return new TraceSchedule(this.snap, this.steps, this.pSteps.dropLast()); + } + return new TraceSchedule(this.snap, this.steps.dropLast(), new Sequence()); + } + + /** + * Indicates a step and which kind (instruction or p-code) + */ + public record StepAndKind(StepKind kind, Step step) {} + + /** + * {@return the last step of the schedule} + */ + public StepAndKind lastStep() { + if (!this.pSteps.isNop()) { + return new StepAndKind(StepKind.PCODE, pSteps.last()); + } + return new StepAndKind(StepKind.INSTRUCTION, steps.last()); + } + + /** + * Drop all p-code steps, if any, and enough instruction steps, such that {@link #stepCount()} + * returns the given count. + * + * @param count the desired step count + * @return the new schedule + */ + public TraceSchedule truncateToSteps(int count) { + return new TraceSchedule(this.snap, this.steps.truncate(count), new Sequence()); + } + /** * Get the threads involved in the schedule * diff --git a/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/database/time/DBTraceTimeManagerTest.java b/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/database/time/DBTraceTimeManagerTest.java new file mode 100644 index 0000000000..9a4f44f958 --- /dev/null +++ b/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/database/time/DBTraceTimeManagerTest.java @@ -0,0 +1,68 @@ +/* ### + * 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.database.time; + +import static org.junit.Assert.*; + +import org.junit.*; + +import db.Transaction; +import ghidra.test.AbstractGhidraHeadlessIntegrationTest; +import ghidra.trace.database.ToyDBTraceBuilder; +import ghidra.trace.model.time.schedule.TraceSchedule; + +public class DBTraceTimeManagerTest extends AbstractGhidraHeadlessIntegrationTest { + + ToyDBTraceBuilder b; + DBTraceTimeManager timeManager; + + @Before + public void setUpTimeManagerTest() throws Exception { + b = new ToyDBTraceBuilder("Testing", "Toy:BE:64:default"); + timeManager = b.trace.getTimeManager(); + } + + @After + public void tearDownTimeManagerTest() throws Exception { + b.close(); + } + + @Test + public void testFindSnapshotWithNearestPrefix() throws Exception { + try (Transaction tx = b.startTransaction()) { + assertNotNull(timeManager.findScratchSnapshot(TraceSchedule.parse("0:t0-2"))); + assertNotNull(timeManager.findScratchSnapshot(TraceSchedule.parse("0:t0-20;t1-9"))); + assertNotNull(timeManager.findScratchSnapshot(TraceSchedule.parse("0:t0-20;t1-10"))); + assertNotNull(timeManager.findScratchSnapshot(TraceSchedule.parse("0:t0-3;t1-10"))); + assertNotNull(timeManager.findScratchSnapshot(TraceSchedule.parse("0:t0-4"))); + } + + assertEquals("0:t0-20;t1-10", + timeManager.findSnapshotWithNearestPrefix(TraceSchedule.parse("0:t0-20;t1-10")) + .getScheduleString()); + assertEquals("0:t0-20;t1-10", + timeManager.findSnapshotWithNearestPrefix(TraceSchedule.parse("0:t0-20;t1-11")) + .getScheduleString()); + assertEquals("0:t0-2", + timeManager.findSnapshotWithNearestPrefix(TraceSchedule.parse("0:t0-3")) + .getScheduleString()); + assertEquals("0:t0-4", + timeManager.findSnapshotWithNearestPrefix(TraceSchedule.parse("0:t0-5")) + .getScheduleString()); + + assertNull(timeManager.findSnapshotWithNearestPrefix(TraceSchedule.parse("1:t0-1"))); + } +}