GP-6236: Emulate from nearest snapshot. Avoid UI hang in Registers Panel.

This commit is contained in:
Dan
2025-12-19 13:16:39 +00:00
parent fed6dd7864
commit cbe7d4743e
9 changed files with 361 additions and 77 deletions
@@ -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);
}
@@ -687,9 +687,16 @@ public class DebuggerEmulationServicePlugin extends Plugin implements DebuggerEm
TracePlatform platform = key.platform;
TraceSchedule time = key.time;
Map.Entry<CacheKey, CachedEmulator> ancestor = findNearestPrefix(key);
if (ancestor != null) {
CacheKey prevKey = ancestor.getKey();
TraceSnapshot tracePrefix = trace.getTimeManager().findSnapshotWithNearestPrefix(time);
if (tracePrefix.getSchedule().isSnapOnly()) {
tracePrefix = null;
}
Map.Entry<CacheKey, CachedEmulator> 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",
@@ -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<String> 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<? extends TraceSnapshot> exists = getSnapshotsWithSchedule(schedule).stream()
.filter(s -> s.getVersion() >= version)
.findFirst();
if (exists.isPresent()) {
return exists.get();
}
return doFindNearest(schedule.dropPSteps(), version);
}
@Override
public Collection<? extends DBTraceSnapshot> getAllSnapshots() {
return Collections.unmodifiableCollection(snapshotStore.asMap().values());
@@ -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
*
* <p>
* 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();
}
@@ -200,6 +200,35 @@ public class Sequence implements Comparable<Sequence> {
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<Sequence> {
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<Sequence> {
return count;
}
public int count() {
return steps.size();
}
public long totalSkipCount() {
long count = 0;
for (Step step : steps) {
@@ -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();
}
};
}
@@ -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);
@@ -486,6 +486,15 @@ public class TraceSchedule implements Comparable<TraceSchedule> {
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<TraceSchedule> {
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<TraceSchedule> {
return new TraceSchedule(this.snap, this.steps, new Sequence());
}
/**
* Drop the last step
*
* <p>
* 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
*
@@ -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")));
}
}