diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengProcessActivationTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengProcessActivationTest.java index 811cb3811d..bb96337e20 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengProcessActivationTest.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengProcessActivationTest.java @@ -78,6 +78,6 @@ public abstract class AbstractModelForDbgengProcessActivationTest .collect(Collectors.toList())).trim(); String procId = getIdFromCapture(line); assertEquals(expected.getPath(), - getProcessPattern().applyIndices(procId).getSingletonPath()); + getProcessPattern().applyKeys(procId).getSingletonPath()); } } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbFrameActivationTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbFrameActivationTest.java index e4268b80dc..421d1f6a32 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbFrameActivationTest.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbFrameActivationTest.java @@ -83,6 +83,6 @@ public abstract class AbstractModelForGdbFrameActivationTest assertFalse(line.contains("\n")); assertTrue(line.startsWith("#")); String frameLevel = line.substring(1).split("\\s+")[0]; - assertEquals(expected.getPath(), STACK_PATTERN.applyIndices(frameLevel).getSingletonPath()); + assertEquals(expected.getPath(), STACK_PATTERN.applyKeys(frameLevel).getSingletonPath()); } } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbInferiorActivationTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbInferiorActivationTest.java index a2222b338d..a1005a9744 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbInferiorActivationTest.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbInferiorActivationTest.java @@ -74,6 +74,6 @@ public abstract class AbstractModelForGdbInferiorActivationTest .filter(l -> l.trim().startsWith("*")) .collect(Collectors.toList())).trim(); String inferiorId = line.split("\\s+")[1]; - assertEquals(expected.getPath(), INF_PATTERN.applyIndices(inferiorId).getSingletonPath()); + assertEquals(expected.getPath(), INF_PATTERN.applyKeys(inferiorId).getSingletonPath()); } } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbThreadActivationTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbThreadActivationTest.java index 82e8ad3bd7..aa7cd6459a 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbThreadActivationTest.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbThreadActivationTest.java @@ -87,6 +87,6 @@ public abstract class AbstractModelForGdbThreadActivationTest .collect(Collectors.toList())); String threadGid = line.split("\\s+")[2]; assertEquals(expected.getPath(), - THREAD_PATTERN.applyIndices(threadGid, threadGid).getSingletonPath()); + THREAD_PATTERN.applyKeys(threadGid, threadGid).getSingletonPath()); } } diff --git a/Ghidra/Debug/Debugger-agent-lldb/src/test/java/agent/lldb/model/AbstractModelForLldbScenarioX64RegistersTest.java b/Ghidra/Debug/Debugger-agent-lldb/src/test/java/agent/lldb/model/AbstractModelForLldbScenarioX64RegistersTest.java index 7b5eaecd84..44f9c85510 100644 --- a/Ghidra/Debug/Debugger-agent-lldb/src/test/java/agent/lldb/model/AbstractModelForLldbScenarioX64RegistersTest.java +++ b/Ghidra/Debug/Debugger-agent-lldb/src/test/java/agent/lldb/model/AbstractModelForLldbScenarioX64RegistersTest.java @@ -60,7 +60,7 @@ public abstract class AbstractModelForLldbScenarioX64RegistersTest for (String name : toWrite.keySet()) { for (TargetRegisterBank bank : banks.values()) { Map, TargetRegister> regs = m.findAll(TargetRegister.class, - bank.getPath(), pred -> pred.applyIndices(name), false); + bank.getPath(), pred -> pred.applyKeys(name), false); for (TargetRegister reg : regs.values()) { waitOn(bank.writeRegister(reg, toWrite.get(name))); } diff --git a/Ghidra/Debug/Debugger-agent-lldb/src/test/java/agent/lldb/model/AbstractModelForLldbX64RegistersTest.java b/Ghidra/Debug/Debugger-agent-lldb/src/test/java/agent/lldb/model/AbstractModelForLldbX64RegistersTest.java index b122d17d00..c99156d863 100644 --- a/Ghidra/Debug/Debugger-agent-lldb/src/test/java/agent/lldb/model/AbstractModelForLldbX64RegistersTest.java +++ b/Ghidra/Debug/Debugger-agent-lldb/src/test/java/agent/lldb/model/AbstractModelForLldbX64RegistersTest.java @@ -81,7 +81,7 @@ public abstract class AbstractModelForLldbX64RegistersTest for (Entry ent : getRegisterWrites().entrySet()) { String regName = ent.getKey(); Map, TargetRegister> regs = m.findAll(TargetRegister.class, - path, pred -> pred.applyIndices(regName), false); + path, pred -> pred.applyKeys(regName), false); for (TargetRegister reg : regs.values()) { assertEquals(ent.getValue().length, (reg.getBitLength() + 7) / 8); } @@ -121,7 +121,7 @@ public abstract class AbstractModelForLldbX64RegistersTest for (TargetRegisterBank bank : banks.values()) { for (String name : exp.keySet()) { Map, TargetRegister> regs = m.findAll(TargetRegister.class, - bank.getPath(), pred -> pred.applyIndices(name), false); + bank.getPath(), pred -> pred.applyKeys(name), false); for (TargetRegister reg : regs.values()) { byte[] bytes = waitOn(bank.readRegister(reg)); read.put(name, bytes); @@ -149,7 +149,7 @@ public abstract class AbstractModelForLldbX64RegistersTest for (TargetRegisterBank bank : banks.values()) { for (String name : write.keySet()) { Map, TargetRegister> regs = m.findAll(TargetRegister.class, - bank.getPath(), pred -> pred.applyIndices(name), false); + bank.getPath(), pred -> pred.applyKeys(name), false); for (TargetRegister reg : regs.values()) { waitOn(bank.writeRegister(reg, write.get(name))); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/DebuggerReadsMemoryTrait.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/DebuggerReadsMemoryTrait.java index 61585af598..24d5722f54 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/DebuggerReadsMemoryTrait.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/DebuggerReadsMemoryTrait.java @@ -67,7 +67,7 @@ public abstract class DebuggerReadsMemoryTrait { Trace trace = current.getTrace(); TraceRecorder recorder = current.getRecorder(); BackgroundUtils.async(tool, trace, NAME, true, true, false, - (__, monitor) -> recorder.captureProcessMemory(selection, monitor, false)); + (__, monitor) -> recorder.readMemoryBlocks(selection, monitor, false)); } @Override @@ -78,7 +78,7 @@ public abstract class DebuggerReadsMemoryTrait { } TraceRecorder recorder = current.getRecorder(); // TODO: Either allow partial, or provide action to intersect with accessible - if (!recorder.getAccessibleProcessMemory().contains(selection)) { + if (!recorder.getAccessibleMemory().contains(selection)) { return false; } return true; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/PCLocationTrackingSpec.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/PCLocationTrackingSpec.java index 93b9ce0f71..0da99ed8b0 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/PCLocationTrackingSpec.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/PCLocationTrackingSpec.java @@ -73,7 +73,7 @@ public class PCLocationTrackingSpec implements RegisterLocationTrackingSpec { if (frame == null) { return null; } - return frame.getProgramCounter(); + return frame.getProgramCounter(snap); } @Override diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/VisibleAutoReadMemorySpec.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/VisibleAutoReadMemorySpec.java index 0211928e05..bc64214a63 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/VisibleAutoReadMemorySpec.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/VisibleAutoReadMemorySpec.java @@ -56,7 +56,7 @@ public class VisibleAutoReadMemorySpec implements AutoReadMemorySpec { } TraceRecorder recorder = coordinates.getRecorder(); AddressSet visibleAccessible = - recorder.getAccessibleProcessMemory().intersect(visible); + recorder.getAccessibleMemory().intersect(visible); TraceMemoryManager mm = coordinates.getTrace().getMemoryManager(); AddressSetView alreadyKnown = mm.getAddressesWithState(coordinates.getSnap(), visibleAccessible, @@ -67,6 +67,6 @@ public class VisibleAutoReadMemorySpec implements AutoReadMemorySpec { return AsyncUtils.NIL; } - return recorder.captureProcessMemory(toRead, TaskMonitor.DUMMY, false); + return recorder.readMemoryBlocks(toRead, TaskMonitor.DUMMY, false); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/VisibleROOnceAutoReadMemorySpec.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/VisibleROOnceAutoReadMemorySpec.java index fe5c37c547..eeadedc261 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/VisibleROOnceAutoReadMemorySpec.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/VisibleROOnceAutoReadMemorySpec.java @@ -58,7 +58,7 @@ public class VisibleROOnceAutoReadMemorySpec implements AutoReadMemorySpec { } TraceRecorder recorder = coordinates.getRecorder(); AddressSet visibleAccessible = - recorder.getAccessibleProcessMemory().intersect(visible); + recorder.getAccessibleMemory().intersect(visible); TraceMemoryManager mm = coordinates.getTrace().getMemoryManager(); AddressSetView alreadyKnown = mm.getAddressesWithState(coordinates.getSnap(), visibleAccessible, @@ -92,6 +92,6 @@ public class VisibleROOnceAutoReadMemorySpec implements AutoReadMemorySpec { return AsyncUtils.NIL; } - return recorder.captureProcessMemory(toRead, TaskMonitor.DUMMY, false); + return recorder.readMemoryBlocks(toRead, TaskMonitor.DUMMY, false); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/copying/DebuggerCopyIntoProgramDialog.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/copying/DebuggerCopyIntoProgramDialog.java index f4b30cb828..1f9bbf5d63 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/copying/DebuggerCopyIntoProgramDialog.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/copying/DebuggerCopyIntoProgramDialog.java @@ -810,7 +810,7 @@ public class DebuggerCopyIntoProgramDialog extends DialogComponentProvider { throws Exception { synchronized (this) { monitor.checkCanceled(); - this.captureTask = recorder.captureProcessMemory(new AddressSet(range), monitor, false); + this.captureTask = recorder.readMemoryBlocks(new AddressSet(range), monitor, false); } try { captureTask.get(); // Not a fan, but whatever. diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/DebuggerObjectsProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/DebuggerObjectsProvider.java index 54dc545f20..9871f7df36 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/DebuggerObjectsProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/DebuggerObjectsProvider.java @@ -854,6 +854,14 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter } + public boolean isRoot(ActionContext context) { + TargetObject object = this.getObjectFromContext(context); + if (object == null) { + return false; + } + return object.isRoot(); + } + public boolean isInstance(ActionContext context, Class clazz) { TargetObject object = this.getObjectFromContext(context); if (object == null) { @@ -1128,8 +1136,8 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter .popupMenuGroup(DebuggerResources.GROUP_TARGET, "T" + groupTargetIndex) .popupMenuIcon(AbstractRecordAction.ICON) .helpLocation(new HelpLocation(plugin.getName(), "record")) - .enabledWhen(ctx -> isInstance(ctx, TargetProcess.class)) - .popupWhen(ctx -> isInstance(ctx, TargetProcess.class)) + .enabledWhen(ctx -> isInstance(ctx, TargetProcess.class) || isRoot(ctx)) + .popupWhen(ctx -> isInstance(ctx, TargetProcess.class) || isRoot(ctx)) .onAction(ctx -> performStartRecording(ctx)) .enabled(true) .buildAndInstallLocal(this); @@ -1592,7 +1600,7 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter } } - public void startRecording(TargetProcess targetObject, boolean prompt) { + public void startRecording(TargetObject targetObject, boolean prompt) { TraceRecorder rec = modelService.getRecorder(targetObject); if (rec != null) { return; // Already being recorded @@ -1630,6 +1638,11 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter } public void performStartRecording(ActionContext context) { + TargetObject maybeRoot = getObjectFromContext(context); + if (maybeRoot.isRoot()) { + startRecording(maybeRoot, true); + return; + } performAction(context, false, TargetProcess.class, proc -> { TargetProcess valid = DebugModelConventions.liveProcessOrNull(proc); if (valid != null) { 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 c3ea5be414..09eb4c5791 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 @@ -410,6 +410,7 @@ public class DebuggerStackProvider extends ComponentProviderAdapter { return; } if (currentStack == stack) { + stackTableModel.fireTableDataChanged(); return; } currentStack = stack; 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 891deedd27..72b1533ef2 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 @@ -28,41 +28,50 @@ import ghidra.util.database.UndoableTransaction; public class StackFrameRow { public static class Synthetic extends StackFrameRow { + private Address pc; + public Synthetic(DebuggerStackProvider provider, Address pc) { - super(provider, pc); + super(provider); + this.pc = pc; } public void updateProgramCounter(Address pc) { this.pc = pc; } + + @Override + public Address getProgramCounter() { + return pc; + } } private final DebuggerStackProvider provider; final TraceStackFrame frame; private int level; - Address pc; public StackFrameRow(DebuggerStackProvider provider, TraceStackFrame frame) { this.provider = provider; this.frame = frame; this.level = frame.getLevel(); - this.pc = frame.getProgramCounter(); } - private StackFrameRow(DebuggerStackProvider provider, Address pc) { + private StackFrameRow(DebuggerStackProvider provider) { this.provider = provider; this.frame = null; this.level = 0; - this.pc = pc; } public int getFrameLevel() { return level; } + public long getSnap() { + return provider.current.getSnap(); + } + public Address getProgramCounter() { - return pc; + return frame.getProgramCounter(getSnap()); } public String getComment() { @@ -88,11 +97,12 @@ public class StackFrameRow { if (curThread == null) { return null; } + Address pc = getProgramCounter(); if (pc == null) { return null; } TraceLocation dloc = new DefaultTraceLocation(curThread.getTrace(), - curThread, Range.singleton(provider.current.getSnap()), pc); + curThread, Range.singleton(getSnap()), pc); ProgramLocation sloc = provider.mappingService.getOpenMappedLocation(dloc); if (sloc == null) { return null; @@ -103,6 +113,5 @@ public class StackFrameRow { protected void update() { assert frame != null; // Should never update a synthetic stack level = frame.getLevel(); - pc = frame.getProgramCounter(); } } 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 1a5c098afc..b55001c87c 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 @@ -578,11 +578,18 @@ public class DebuggerWatchesProvider extends ComponentProviderAdapter { } private ProgramLocation getDynamicLocation(ProgramLocation someLoc) { + if (someLoc == null) { + return null; + } TraceProgramView view = current.getView(); if (view == null) { return null; } - if (someLoc.getProgram() instanceof TraceProgramView) { + Program program = someLoc.getProgram(); + if (program == null) { + return null; + } + if (program instanceof TraceProgramView) { return someLoc; } return mappingService.getDynamicLocationFromStatic(view, someLoc); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/mapping/DebuggerMappingOpinion.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/mapping/DebuggerMappingOpinion.java index 2bbca2f590..05bafefc0a 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/mapping/DebuggerMappingOpinion.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/mapping/DebuggerMappingOpinion.java @@ -101,6 +101,7 @@ public interface DebuggerMappingOpinion extends ExtensionPoint { */ public default Set getOffers(TargetObject target, boolean includeOverrides) { + // TODO: Remove this check? if (!(target instanceof TargetProcess)) { return Set.of(); } @@ -117,13 +118,13 @@ public interface DebuggerMappingOpinion extends ExtensionPoint { } /** - * Produce this opinion's offers for the given environment and target process + * Produce this opinion's offers for the given environment and target * * @param env the environment associated with the target - * @param process the target process + * @param target the target (usually a process) * @param includeOverrides true to include override offers, i.e., those with negative confidence * @return the offers, possibly empty, but never null */ - Set offersForEnv(TargetEnvironment env, TargetProcess process, + Set offersForEnv(TargetEnvironment env, TargetObject target, boolean includeOverrides); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/mapping/DebuggerMemoryMapper.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/mapping/DebuggerMemoryMapper.java index 08a1a1ca34..10b895eb51 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/mapping/DebuggerMemoryMapper.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/mapping/DebuggerMemoryMapper.java @@ -15,8 +15,7 @@ */ package ghidra.app.plugin.core.debug.mapping; -import ghidra.program.model.address.Address; -import ghidra.program.model.address.AddressRange; +import ghidra.program.model.address.*; public interface DebuggerMemoryMapper { /** @@ -33,7 +32,10 @@ public interface DebuggerMemoryMapper { * @param traceRange the range in the view's address space * @return the "same range" in the target's address space */ - AddressRange traceToTarget(AddressRange traceRange); + default AddressRange traceToTarget(AddressRange traceRange) { + return new AddressRangeImpl(traceToTarget(traceRange.getMinAddress()), + traceToTarget(traceRange.getMaxAddress())); + } /** * Map the given address from the target process into the trace @@ -49,5 +51,8 @@ public interface DebuggerMemoryMapper { * @param targetRange the range in the target's address space * @return the "same range" in the trace's address space */ - AddressRange targetToTrace(AddressRange targetRange); + default AddressRange targetToTrace(AddressRange targetRange) { + return new AddressRangeImpl(targetToTrace(targetRange.getMinAddress()), + targetToTrace(targetRange.getMaxAddress())); + } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/mapping/DefaultDebuggerMemoryMapper.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/mapping/DefaultDebuggerMemoryMapper.java index 218fd0800f..3f5f58b950 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/mapping/DefaultDebuggerMemoryMapper.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/mapping/DefaultDebuggerMemoryMapper.java @@ -45,12 +45,6 @@ public class DefaultDebuggerMemoryMapper implements DebuggerMemoryMapper { public Address traceToTarget(Address traceAddr) { assert isInFactory(traceAddr, traceAddressFactory); return toSameNamedSpace(traceAddr, targetAddressFactory); - }; - - @Override - public AddressRange traceToTarget(AddressRange traceRange) { - return new AddressRangeImpl(traceToTarget(traceRange.getMinAddress()), - traceToTarget(traceRange.getMaxAddress())); } @Override @@ -65,10 +59,4 @@ public class DefaultDebuggerMemoryMapper implements DebuggerMemoryMapper { assert isInFactory(targetAddr, targetAddressFactory); return toSameNamedSpace(targetAddr, traceAddressFactory); } - - @Override - public AddressRange targetToTrace(AddressRange targetRange) { - return new AddressRangeImpl(targetToTrace(targetRange.getMinAddress()), - targetToTrace(targetRange.getMaxAddress())); - } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/mapping/DefaultDebuggerTargetTraceMapper.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/mapping/DefaultDebuggerTargetTraceMapper.java index cb1e46b2df..07231a1e32 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/mapping/DefaultDebuggerTargetTraceMapper.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/mapping/DefaultDebuggerTargetTraceMapper.java @@ -35,12 +35,12 @@ public class DefaultDebuggerTargetTraceMapper implements DebuggerTargetTraceMapp protected final Set extraRegNames; public DefaultDebuggerTargetTraceMapper(TargetObject target, LanguageID langID, - CompilerSpecID csId, Collection extraRegNames) + CompilerSpecID csID, Collection extraRegNames) throws LanguageNotFoundException, CompilerSpecNotFoundException { this.target = target; LanguageService langServ = DefaultLanguageService.getLanguageService(); this.language = langServ.getLanguage(langID); - this.cSpec = language.getCompilerSpecByID(csId); + this.cSpec = language.getCompilerSpecByID(csID); this.extraRegNames = Set.copyOf(extraRegNames); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/mapping/ObjectBasedDebuggerMappingOffer.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/mapping/ObjectBasedDebuggerMappingOffer.java new file mode 100644 index 0000000000..9f875e3d05 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/mapping/ObjectBasedDebuggerMappingOffer.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.app.plugin.core.debug.mapping; + +import java.util.Set; + +import ghidra.dbg.target.TargetObject; +import ghidra.program.model.lang.*; + +public class ObjectBasedDebuggerMappingOffer extends DefaultDebuggerMappingOffer { + private static final String DESCRIPTION = + "EXPERIMENTAL: Object-based recording, deferred mapping"; + private static final int CONFIDENCE = -100; // TODO: Increase this when it becomes preferred + protected static final LanguageID LANGID_DATA64 = new LanguageID("DATA:BE:64:default"); + protected static final CompilerSpecID CSID_PTR64 = new CompilerSpecID("pointer64"); + + public ObjectBasedDebuggerMappingOffer(TargetObject target) { + // TODO: Is extraRegNames relevant? + super(target, CONFIDENCE, DESCRIPTION, LANGID_DATA64, CSID_PTR64, Set.of()); + } + + @Override + protected DebuggerTargetTraceMapper createMapper() + throws LanguageNotFoundException, CompilerSpecNotFoundException { + return new ObjectBasedDebuggerTargetTraceMapper(target, langID, csID, extraRegNames); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/mapping/ObjectBasedDebuggerMappingOpinion.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/mapping/ObjectBasedDebuggerMappingOpinion.java new file mode 100644 index 0000000000..6e729e8eb7 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/mapping/ObjectBasedDebuggerMappingOpinion.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.app.plugin.core.debug.mapping; + +import java.util.Set; + +import ghidra.dbg.target.TargetEnvironment; +import ghidra.dbg.target.TargetObject; + +public class ObjectBasedDebuggerMappingOpinion implements DebuggerMappingOpinion { + + @Override + public Set getOffers(TargetObject target, boolean includeOverrides) { + // TODO: Remove this check + if (!includeOverrides) { + return Set.of(); + } + // TODO: Do I want to require it to record the whole model? + // If not, I need to figure out how to locate object dependencies and still record them. + if (!target.isRoot()) { + return Set.of(); + } + return Set.of(new ObjectBasedDebuggerMappingOffer(target)); + } + + @Override + public Set offersForEnv(TargetEnvironment env, TargetObject target, + boolean includeOverrides) { + throw new UnsupportedOperationException(); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/mapping/ObjectBasedDebuggerMemoryMapper.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/mapping/ObjectBasedDebuggerMemoryMapper.java new file mode 100644 index 0000000000..23f96de056 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/mapping/ObjectBasedDebuggerMemoryMapper.java @@ -0,0 +1,93 @@ +/* ### + * 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.mapping; + +import java.util.HashMap; +import java.util.Map; + +import ghidra.program.model.address.*; +import ghidra.trace.model.Trace; +import ghidra.util.database.UndoableTransaction; +import ghidra.util.exception.DuplicateNameException; + +public class ObjectBasedDebuggerMemoryMapper implements DebuggerMemoryMapper { + protected final Trace trace; + protected final AddressSpace base; + + protected final Map targetToTraceSpaces = new HashMap<>(); + protected final Map traceToTargetSpaces = new HashMap<>(); + + public ObjectBasedDebuggerMemoryMapper(Trace trace) { + this.trace = trace; + this.base = trace.getBaseAddressFactory().getDefaultAddressSpace(); + } + + @Override + public Address traceToTarget(Address traceAddr) { + AddressSpace traceSpace = traceAddr.getAddressSpace(); + int traceIdHash = System.identityHashCode(traceSpace); + AddressSpace targetSpace; + synchronized (traceToTargetSpaces) { + targetSpace = traceToTargetSpaces.get(traceIdHash); + } + /** + * Can only be null if space is the default space or some non-physical space. In that case, + * the target hasn't defined a space with that name, so no mapping. + */ + if (targetSpace == null) { + return null; + } + return targetSpace.getAddress(traceAddr.getOffset()); + } + + @Override + public Address targetToTrace(Address targetAddr) { + AddressSpace targetSpace = targetAddr.getAddressSpace(); + int targetIdHash = System.identityHashCode(targetSpace); + AddressSpace traceSpace; + synchronized (traceToTargetSpaces) { + traceSpace = targetToTraceSpaces.get(targetIdHash); + if (traceSpace == null) { + traceSpace = createSpace(targetSpace.getName()); + targetToTraceSpaces.put(targetIdHash, traceSpace); + traceToTargetSpaces.put(System.identityHashCode(traceSpace), + targetSpace); + } + } + return traceSpace.getAddress(targetAddr.getOffset()); + } + + protected AddressSpace createSpace(String name) { + try (UndoableTransaction tid = + UndoableTransaction.start(trace, "Create space for mapping", true)) { + AddressFactory factory = trace.getBaseAddressFactory(); + AddressSpace space = factory.getAddressSpace(name); + if (space == null) { + return trace.getMemoryManager().createOverlayAddressSpace(name, base); + } + // Let the default space suffice for its own name + // NB. if overlay already exists, we've already issued a warning + if (space == base || space.isOverlaySpace()) { + return space; + } + // Otherwise, do not allow non-physical spaces to be used by accident. + return trace.getMemoryManager().createOverlayAddressSpace('_' + name, base); + } + catch (DuplicateNameException e) { + throw new AssertionError(e); + } + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/mapping/ObjectBasedDebuggerTargetTraceMapper.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/mapping/ObjectBasedDebuggerTargetTraceMapper.java new file mode 100644 index 0000000000..e0125c17c2 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/mapping/ObjectBasedDebuggerTargetTraceMapper.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.app.plugin.core.debug.mapping; + +import java.util.Collection; + +import ghidra.app.plugin.core.debug.service.model.DebuggerModelServicePlugin; +import ghidra.app.plugin.core.debug.service.model.record.ObjectBasedTraceRecorder; +import ghidra.app.services.TraceRecorder; +import ghidra.dbg.target.*; +import ghidra.program.model.lang.*; +import ghidra.trace.model.Trace; + +public class ObjectBasedDebuggerTargetTraceMapper extends DefaultDebuggerTargetTraceMapper { + + protected ObjectBasedDebuggerMemoryMapper memoryMapper; + + public ObjectBasedDebuggerTargetTraceMapper(TargetObject target, LanguageID langID, + CompilerSpecID csID, Collection extraRegNames) + throws LanguageNotFoundException, CompilerSpecNotFoundException { + super(target, langID, csID, extraRegNames); + } + + @Override + protected DebuggerMemoryMapper createMemoryMapper(TargetMemory memory) { + // TODO: Validate regions to not overlap? + // Could probably do that in unit testing of model instead + return memoryMapper; + } + + @Override + protected DebuggerRegisterMapper createRegisterMapper(TargetRegisterContainer registers) { + throw new UnsupportedOperationException(); + } + + @Override + public TraceRecorder startRecording(DebuggerModelServicePlugin service, Trace trace) { + this.memoryMapper = new ObjectBasedDebuggerMemoryMapper(trace); + return new ObjectBasedTraceRecorder(service, trace, target, this); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/OverridesDebuggerMappingOpinion.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/OverridesDebuggerMappingOpinion.java index bf1c7fb835..b878513a7c 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/OverridesDebuggerMappingOpinion.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/OverridesDebuggerMappingOpinion.java @@ -19,7 +19,8 @@ import java.util.Set; import java.util.stream.Collectors; import ghidra.app.plugin.core.debug.mapping.*; -import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetEnvironment; +import ghidra.dbg.target.TargetObject; import ghidra.program.model.lang.*; import ghidra.program.util.DefaultLanguageService; @@ -54,7 +55,7 @@ public class OverridesDebuggerMappingOpinion implements DebuggerMappingOpinion { } @Override - public Set offersForEnv(TargetEnvironment env, TargetProcess process, + public Set offersForEnv(TargetEnvironment env, TargetObject target, boolean includeOverrides) { if (!includeOverrides) { return Set.of(); @@ -65,7 +66,7 @@ public class OverridesDebuggerMappingOpinion implements DebuggerMappingOpinion { // ALL THE SPECS!!! new LanguageCompilerSpecQuery(null, null, null, null, null)) .stream() - .map(lcsp -> offerForLanguageAndCSpec(process, endian, lcsp)) + .map(lcsp -> offerForLanguageAndCSpec(target, endian, lcsp)) .collect(Collectors.toSet()); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/arm/FridaArmDebuggerMappingOpinion.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/arm/FridaArmDebuggerMappingOpinion.java index 7a921ddad8..44e50c831f 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/arm/FridaArmDebuggerMappingOpinion.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/arm/FridaArmDebuggerMappingOpinion.java @@ -49,24 +49,28 @@ public class FridaArmDebuggerMappingOpinion implements DebuggerMappingOpinion { protected static class FridaAarch64MacosOffer extends DefaultDebuggerMappingOffer { public FridaAarch64MacosOffer(TargetProcess process) { - super(process, 50, "AARCH64/Frida on macos", LANG_ID_AARCH64,COMP_ID_DEFAULT, + super(process, 50, "AARCH64/Frida on macos", LANG_ID_AARCH64, COMP_ID_DEFAULT, Set.of("cpsr")); } } @Override - public Set offersForEnv(TargetEnvironment env, TargetProcess process, + public Set offersForEnv(TargetEnvironment env, TargetObject target, boolean includesOverrides) { + if (!(target instanceof TargetProcess)) { + return Set.of(); + } if (!env.getDebugger().toLowerCase().contains("frida")) { return Set.of(); } String arch = env.getArchitecture(); - boolean is64Bit = arch.contains("AARCH64") || arch.contains("arm64") || arch.contains("arm"); + boolean is64Bit = + arch.contains("AARCH64") || arch.contains("arm64") || arch.contains("arm"); String os = env.getOperatingSystem(); if (os.contains("macos")) { if (is64Bit) { Msg.info(this, "Using os=" + os + " arch=" + arch); - return Set.of(new FridaAarch64MacosOffer(process)); + return Set.of(new FridaAarch64MacosOffer((TargetProcess) target)); } } return Set.of(); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/arm/LldbArmDebuggerMappingOpinion.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/arm/LldbArmDebuggerMappingOpinion.java index 07fa4cba13..fa7d6e85a0 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/arm/LldbArmDebuggerMappingOpinion.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/arm/LldbArmDebuggerMappingOpinion.java @@ -49,14 +49,17 @@ public class LldbArmDebuggerMappingOpinion implements DebuggerMappingOpinion { protected static class LldbAarch64MacosOffer extends DefaultDebuggerMappingOffer { public LldbAarch64MacosOffer(TargetProcess process) { - super(process, 50, "AARCH64/LLDB on macos", LANG_ID_AARCH64,COMP_ID_DEFAULT, + super(process, 50, "AARCH64/LLDB on macos", LANG_ID_AARCH64, COMP_ID_DEFAULT, Set.of("cpsr")); } } @Override - public Set offersForEnv(TargetEnvironment env, TargetProcess process, + public Set offersForEnv(TargetEnvironment env, TargetObject target, boolean includesOverrides) { + if (!(target instanceof TargetProcess)) { + return Set.of(); + } if (!env.getDebugger().toLowerCase().contains("lldb")) { return Set.of(); } @@ -66,7 +69,7 @@ public class LldbArmDebuggerMappingOpinion implements DebuggerMappingOpinion { if (os.contains("macos")) { if (is64Bit) { Msg.info(this, "Using os=" + os + " arch=" + arch); - return Set.of(new LldbAarch64MacosOffer(process)); + return Set.of(new LldbAarch64MacosOffer((TargetProcess) target)); } } return Set.of(); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/dbgeng/DbgengX64DebuggerMappingOpinion.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/dbgeng/DbgengX64DebuggerMappingOpinion.java index b5e4233800..a0d58a095c 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/dbgeng/DbgengX64DebuggerMappingOpinion.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/dbgeng/DbgengX64DebuggerMappingOpinion.java @@ -75,15 +75,18 @@ public class DbgengX64DebuggerMappingOpinion implements DebuggerMappingOpinion { } @Override - public Set offersForEnv(TargetEnvironment env, TargetProcess process, + public Set offersForEnv(TargetEnvironment env, TargetObject target, boolean includeOverrides) { + if (!(target instanceof TargetProcess)) { + return Set.of(); + } if (env == null || !env.getDebugger().toLowerCase().contains("dbg")) { return Set.of(); } boolean is64Bit = env.getArchitecture().contains("x86_64") || env.getArchitecture().contains("x64_32"); if (is64Bit) { - return Set.of(new DbgI386X86_64WindowsOffer(process)); + return Set.of(new DbgI386X86_64WindowsOffer((TargetProcess) target)); } return null; } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/dbgeng/DbgengX64DisassemblyInject.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/dbgeng/DbgengX64DisassemblyInject.java index 20b8299deb..8e80b52395 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/dbgeng/DbgengX64DisassemblyInject.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/dbgeng/DbgengX64DisassemblyInject.java @@ -96,7 +96,7 @@ public class DbgengX64DisassemblyInject implements DisassemblyInject { try { // This is on its own task thread, so whatever. // Just don't hang it indefinitely. - recorder.captureProcessMemory(set, TaskMonitor.DUMMY, false) + recorder.readMemoryBlocks(set, TaskMonitor.DUMMY, false) .get(1000, TimeUnit.MILLISECONDS); } catch (InterruptedException | ExecutionException | TimeoutException e) { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/frida/FridaX86DebuggerMappingOpinion.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/frida/FridaX86DebuggerMappingOpinion.java index b08c312630..5807ab8228 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/frida/FridaX86DebuggerMappingOpinion.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/frida/FridaX86DebuggerMappingOpinion.java @@ -18,8 +18,7 @@ package ghidra.app.plugin.core.debug.platform.frida; import java.util.Set; import ghidra.app.plugin.core.debug.mapping.*; -import ghidra.dbg.target.TargetEnvironment; -import ghidra.dbg.target.TargetProcess; +import ghidra.dbg.target.*; import ghidra.program.model.lang.CompilerSpecID; import ghidra.program.model.lang.LanguageID; import ghidra.util.Msg; @@ -71,16 +70,22 @@ public class FridaX86DebuggerMappingOpinion implements DebuggerMappingOpinion { } @Override - public Set offersForEnv(TargetEnvironment env, TargetProcess process, + public Set offersForEnv(TargetEnvironment env, TargetObject target, boolean includeOverrides) { + if (!(target instanceof TargetProcess)) { + return Set.of(); + } + TargetProcess process = (TargetProcess) target; if (!env.getDebugger().toLowerCase().contains("frida")) { return Set.of(); } String arch = env.getArchitecture(); - boolean is32Bit = arch.contains("ia32") ||arch.contains("x86-32") || arch.contains("i386") || - arch.contains("x86_32"); - boolean is64Bit = arch.contains("x64") ||arch.contains("x86-64") || arch.contains("x64-32") || - arch.contains("x86_64") || arch.contains("x64_32") || arch.contains("i686"); + boolean is32Bit = + arch.contains("ia32") || arch.contains("x86-32") || arch.contains("i386") || + arch.contains("x86_32"); + boolean is64Bit = + arch.contains("x64") || arch.contains("x86-64") || arch.contains("x64-32") || + arch.contains("x86_64") || arch.contains("x64_32") || arch.contains("i686"); String os = env.getOperatingSystem(); if (os.contains("darwin")) { if (is64Bit) { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/gdb/DefaultGdbDebuggerMappingOpinion.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/gdb/DefaultGdbDebuggerMappingOpinion.java index 8e03bf90fd..cb5525ff41 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/gdb/DefaultGdbDebuggerMappingOpinion.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/gdb/DefaultGdbDebuggerMappingOpinion.java @@ -21,7 +21,8 @@ import java.util.stream.Collectors; import org.apache.commons.lang3.tuple.Pair; import ghidra.app.plugin.core.debug.mapping.*; -import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetEnvironment; +import ghidra.dbg.target.TargetObject; import ghidra.program.model.lang.*; import ghidra.program.util.DefaultLanguageService; @@ -81,7 +82,7 @@ public class DefaultGdbDebuggerMappingOpinion implements DebuggerMappingOpinion } @Override - public Set offersForEnv(TargetEnvironment env, TargetProcess process, + public Set offersForEnv(TargetEnvironment env, TargetObject target, boolean includeOverrides) { if (!isGdb(env)) { return Set.of(); @@ -90,7 +91,7 @@ public class DefaultGdbDebuggerMappingOpinion implements DebuggerMappingOpinion String arch = env.getArchitecture(); return getCompilerSpecsForGnu(arch, endian).stream() - .flatMap(lcsp -> offersForLanguageAndCSpec(process, arch, endian, lcsp).stream()) + .flatMap(lcsp -> offersForLanguageAndCSpec(target, arch, endian, lcsp).stream()) .collect(Collectors.toSet()); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/gdb/GdbM68kDebuggerMappingOpinion.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/gdb/GdbM68kDebuggerMappingOpinion.java index fc05b96f42..5977d23864 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/gdb/GdbM68kDebuggerMappingOpinion.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/gdb/GdbM68kDebuggerMappingOpinion.java @@ -18,8 +18,7 @@ package ghidra.app.plugin.core.debug.platform.gdb; import java.util.Set; import ghidra.app.plugin.core.debug.mapping.*; -import ghidra.dbg.target.TargetEnvironment; -import ghidra.dbg.target.TargetProcess; +import ghidra.dbg.target.*; import ghidra.program.model.lang.CompilerSpecID; import ghidra.program.model.lang.LanguageID; @@ -39,8 +38,11 @@ public class GdbM68kDebuggerMappingOpinion implements DebuggerMappingOpinion { } @Override - public Set offersForEnv(TargetEnvironment env, TargetProcess process, + public Set offersForEnv(TargetEnvironment env, TargetObject target, boolean includeOverrides) { + if (!(target instanceof TargetProcess)) { + return Set.of(); + } if (!env.getDebugger().toLowerCase().contains("gdb")) { return Set.of(); } @@ -54,7 +56,7 @@ public class GdbM68kDebuggerMappingOpinion implements DebuggerMappingOpinion { } String arch = env.getArchitecture(); if (arch.startsWith("m68k")) { - return Set.of(new GdbM68kBELinux32DefOffer(process)); + return Set.of(new GdbM68kBELinux32DefOffer((TargetProcess) target)); } return Set.of(); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/gdb/GdbMipsDebuggerMappingOpinion.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/gdb/GdbMipsDebuggerMappingOpinion.java index 29a4a8fff6..b6d32009a9 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/gdb/GdbMipsDebuggerMappingOpinion.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/gdb/GdbMipsDebuggerMappingOpinion.java @@ -18,8 +18,7 @@ package ghidra.app.plugin.core.debug.platform.gdb; import java.util.Set; import ghidra.app.plugin.core.debug.mapping.*; -import ghidra.dbg.target.TargetEnvironment; -import ghidra.dbg.target.TargetProcess; +import ghidra.dbg.target.*; import ghidra.program.model.lang.CompilerSpecID; import ghidra.program.model.lang.LanguageID; @@ -79,8 +78,12 @@ public class GdbMipsDebuggerMappingOpinion implements DebuggerMappingOpinion { } @Override - public Set offersForEnv(TargetEnvironment env, TargetProcess process, + public Set offersForEnv(TargetEnvironment env, TargetObject target, boolean includeOverrides) { + if (!(target instanceof TargetProcess)) { + return Set.of(); + } + TargetProcess process = (TargetProcess) target; if (!env.getDebugger().toLowerCase().contains("gdb")) { return Set.of(); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/gdb/GdbPowerPCDebuggerMappingOpinion.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/gdb/GdbPowerPCDebuggerMappingOpinion.java index 34d029f7eb..6592c98f1b 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/gdb/GdbPowerPCDebuggerMappingOpinion.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/gdb/GdbPowerPCDebuggerMappingOpinion.java @@ -18,8 +18,7 @@ package ghidra.app.plugin.core.debug.platform.gdb; import java.util.Set; import ghidra.app.plugin.core.debug.mapping.*; -import ghidra.dbg.target.TargetEnvironment; -import ghidra.dbg.target.TargetProcess; +import ghidra.dbg.target.*; import ghidra.program.model.lang.CompilerSpecID; import ghidra.program.model.lang.LanguageID; @@ -65,8 +64,12 @@ public class GdbPowerPCDebuggerMappingOpinion implements DebuggerMappingOpinion } @Override - public Set offersForEnv(TargetEnvironment env, TargetProcess process, + public Set offersForEnv(TargetEnvironment env, TargetObject target, boolean includeOverrides) { + if (!(target instanceof TargetProcess)) { + return Set.of(); + } + TargetProcess process = (TargetProcess) target; if (!env.getDebugger().toLowerCase().contains("gdb")) { return Set.of(); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/gdb/GdbX86DebuggerMappingOpinion.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/gdb/GdbX86DebuggerMappingOpinion.java index 9752673551..6c28faceba 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/gdb/GdbX86DebuggerMappingOpinion.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/gdb/GdbX86DebuggerMappingOpinion.java @@ -100,8 +100,12 @@ public class GdbX86DebuggerMappingOpinion implements DebuggerMappingOpinion { } @Override - public Set offersForEnv(TargetEnvironment env, TargetProcess process, + public Set offersForEnv(TargetEnvironment env, TargetObject target, boolean includeOverrides) { + if (!(target instanceof TargetProcess)) { + return Set.of(); + } + TargetProcess process = (TargetProcess) target; if (!env.getDebugger().toLowerCase().contains("gdb")) { return Set.of(); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/jvm/JdiDalvikDebuggerMappingOpinion.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/jvm/JdiDalvikDebuggerMappingOpinion.java index 1e1a1039fd..e05b82e795 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/jvm/JdiDalvikDebuggerMappingOpinion.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/jvm/JdiDalvikDebuggerMappingOpinion.java @@ -64,8 +64,11 @@ public class JdiDalvikDebuggerMappingOpinion implements DebuggerMappingOpinion { } @Override - public Set offersForEnv(TargetEnvironment env, TargetProcess process, + public Set offersForEnv(TargetEnvironment env, TargetObject target, boolean includeOverrides) { + if (!(target instanceof TargetProcess)) { + return Set.of(); + } if (!env.getDebugger().contains("Java Debug Interface")) { return Set.of(); } @@ -73,6 +76,6 @@ public class JdiDalvikDebuggerMappingOpinion implements DebuggerMappingOpinion { return Set.of(); } // NOTE: Not worried about JRE version - return Set.of(new DalvikDebuggerMappingOffer(process)); + return Set.of(new DalvikDebuggerMappingOffer((TargetProcess) target)); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/jvm/JdiJavaDebuggerMappingOpinion.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/jvm/JdiJavaDebuggerMappingOpinion.java index ebc6dd09b5..8bcb5041b2 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/jvm/JdiJavaDebuggerMappingOpinion.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/jvm/JdiJavaDebuggerMappingOpinion.java @@ -64,8 +64,11 @@ public class JdiJavaDebuggerMappingOpinion implements DebuggerMappingOpinion { } @Override - public Set offersForEnv(TargetEnvironment env, TargetProcess process, + public Set offersForEnv(TargetEnvironment env, TargetObject target, boolean includeOverrides) { + if (!(target instanceof TargetProcess)) { + return Set.of(); + } if (!env.getDebugger().contains("Java Debug Interface")) { return Set.of(); } @@ -73,6 +76,6 @@ public class JdiJavaDebuggerMappingOpinion implements DebuggerMappingOpinion { return Set.of(); } // NOTE: Not worried about JRE version - return Set.of(new JavaDebuggerMappingOffer(process)); + return Set.of(new JavaDebuggerMappingOffer((TargetProcess) target)); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/lldb/LldbX86DebuggerMappingOpinion.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/lldb/LldbX86DebuggerMappingOpinion.java index a475e88b6d..463610d14e 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/lldb/LldbX86DebuggerMappingOpinion.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/lldb/LldbX86DebuggerMappingOpinion.java @@ -18,8 +18,7 @@ package ghidra.app.plugin.core.debug.platform.lldb; import java.util.Set; import ghidra.app.plugin.core.debug.mapping.*; -import ghidra.dbg.target.TargetEnvironment; -import ghidra.dbg.target.TargetProcess; +import ghidra.dbg.target.*; import ghidra.program.model.lang.CompilerSpecID; import ghidra.program.model.lang.LanguageID; import ghidra.util.Msg; @@ -71,8 +70,12 @@ public class LldbX86DebuggerMappingOpinion implements DebuggerMappingOpinion { } @Override - public Set offersForEnv(TargetEnvironment env, TargetProcess process, + public Set offersForEnv(TargetEnvironment env, TargetObject target, boolean includeOverrides) { + if (!(target instanceof TargetProcess)) { + return Set.of(); + } + TargetProcess process = (TargetProcess) target; if (!env.getDebugger().toLowerCase().contains("lldb")) { return Set.of(); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/DebuggerLogicalBreakpointServicePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/DebuggerLogicalBreakpointServicePlugin.java index e88bfe088d..00f2f8dbe0 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/DebuggerLogicalBreakpointServicePlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/DebuggerLogicalBreakpointServicePlugin.java @@ -250,6 +250,11 @@ public class DebuggerLogicalBreakpointServicePlugin extends Plugin Msg.info(this, "Ignoring " + breakpoint + " changed until service has finished loading its trace"); } + catch (NoSuchElementException e) { + // TODO: This catch clause should not be necessary. + Msg.error(this, + "!!!! Object-based breakpoint emitted event without a spec: " + breakpoint); + } } private void breakpointLifespanChanged(TraceAddressSpace spaceIsNull, 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 index 9f7d769c5a..04f080fcef 100644 --- 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 @@ -69,7 +69,7 @@ public class ReadsTargetMemoryPcodeExecutorState if (!isLive()) { return false; } - waitTimeout(recorder.captureProcessMemory(unknown, TaskMonitor.DUMMY, false)); + waitTimeout(recorder.readMemoryBlocks(unknown, TaskMonitor.DUMMY, false)); return true; } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultMemoryRecorder.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultMemoryRecorder.java index 217057e916..8624a9bcc7 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultMemoryRecorder.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultMemoryRecorder.java @@ -21,11 +21,11 @@ import java.util.concurrent.CompletableFuture; import com.google.common.collect.Range; import ghidra.app.plugin.core.debug.service.model.interfaces.ManagedMemoryRecorder; -import ghidra.async.AsyncUtils; -import ghidra.async.TypeSpec; +import ghidra.app.plugin.core.debug.service.model.record.RecorderUtils; import ghidra.dbg.target.TargetMemory; import ghidra.dbg.target.TargetMemoryRegion; -import ghidra.program.model.address.*; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressSetView; import ghidra.trace.model.Trace; import ghidra.trace.model.memory.*; import ghidra.util.Msg; @@ -35,20 +35,7 @@ import ghidra.util.task.TaskMonitor; public class DefaultMemoryRecorder implements ManagedMemoryRecorder { // For large memory captures - private static final int BLOCK_SIZE = 4096; - private static final long BLOCK_MASK = -1L << 12; - - protected static AddressSetView expandToBlocks(AddressSetView asv) { - AddressSet result = new AddressSet(); - // Not terribly efficient, but this is one range most of the time - for (AddressRange range : asv) { - AddressSpace space = range.getAddressSpace(); - Address min = space.getAddress(range.getMinAddress().getOffset() & BLOCK_MASK); - Address max = space.getAddress(range.getMaxAddress().getOffset() | ~BLOCK_MASK); - result.add(new AddressRangeImpl(min, max)); - } - return result; - } + private static final int BLOCK_BITS = 12; // 4096 bytes private final DefaultTraceRecorder recorder; private final Trace trace; @@ -62,45 +49,7 @@ public class DefaultMemoryRecorder implements ManagedMemoryRecorder { public CompletableFuture> captureProcessMemory(AddressSetView set, TaskMonitor monitor, boolean toMap) { - // TODO: Figure out how to display/select per-thread memory. - // Probably need a thread parameter passed in then? - // NOTE: That thread memory will already be chained to process memory. Good. - - // NOTE: I don't intend to warn about the number of requests. - // They're delivered in serial, and there's a cancel button that works - - int total = 0; - AddressSetView expSet = expandToBlocks(set) - .intersect(trace.getMemoryManager().getRegionsAddressSet(recorder.getSnap())); - for (AddressRange r : expSet) { - total += Long.divideUnsigned(r.getLength() + BLOCK_SIZE - 1, BLOCK_SIZE); - } - monitor.initialize(total); - monitor.setMessage("Capturing memory"); - // TODO: Read blocks in parallel? Probably NO. Tends to overload the agent. - NavigableMap result = toMap ? new TreeMap<>() : null; - return AsyncUtils.each(TypeSpec.VOID, expSet.iterator(), (r, loop) -> { - AddressRangeChunker blocks = new AddressRangeChunker(r, BLOCK_SIZE); - AsyncUtils.each(TypeSpec.VOID, blocks.iterator(), (vBlk, inner) -> { - // The listener in the recorder will copy to the Trace. - monitor.incrementProgress(1); - AddressRange tBlk = recorder.getMemoryMapper().traceToTarget(vBlk); - recorder.getProcessMemory() - .readMemory(tBlk.getMinAddress(), (int) tBlk.getLength()) - .thenAccept(data -> { - if (toMap) { - result.put(tBlk.getMinAddress(), data); - } - }) - .exceptionally(e -> { - Msg.error(this, "Error reading block " + tBlk + ": " + e); - // NOTE: Above may double log, since recorder listens for errors, too - return null; // Continue looping on errors - }) - .thenApply(__ -> !monitor.isCancelled()) - .handle(inner::repeatWhile); - }).thenApply(v -> !monitor.isCancelled()).handle(loop::repeatWhile); - }).thenApply(__ -> result); + return RecorderUtils.INSTANCE.readMemoryBlocks(recorder, BLOCK_BITS, set, monitor, toMap); } @Override diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultStackRecorder.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultStackRecorder.java index 596c49cff9..61262c0f08 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultStackRecorder.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultStackRecorder.java @@ -86,7 +86,7 @@ public class DefaultStackRecorder implements ManagedStackRecorder { public void doRecordFrame(TraceStack traceStack, int frameLevel, Address pc) { TraceStackFrame traceFrame = traceStack.getFrame(frameLevel, true); - traceFrame.setProgramCounter(pc); + traceFrame.setProgramCounter(null, pc); // Not object-based, so span=null } public void recordFrame(TargetStackFrame frame) { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultTraceRecorder.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultTraceRecorder.java index 695ded4b4e..d17c12e916 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultTraceRecorder.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultTraceRecorder.java @@ -22,6 +22,8 @@ import org.apache.commons.lang3.concurrent.BasicThreadFactory; import ghidra.app.plugin.core.debug.mapping.*; import ghidra.app.plugin.core.debug.service.model.interfaces.*; +import ghidra.app.plugin.core.debug.service.model.record.DataTypeRecorder; +import ghidra.app.plugin.core.debug.service.model.record.SymbolRecorder; import ghidra.app.services.TraceRecorder; import ghidra.app.services.TraceRecorderListener; import ghidra.async.AsyncLazyValue; @@ -64,11 +66,11 @@ public class DefaultTraceRecorder implements TraceRecorder { TraceObjectManager objectManager; DefaultBreakpointRecorder breakpointRecorder; - DefaultDataTypeRecorder datatypeRecorder; + DataTypeRecorder datatypeRecorder; DefaultMemoryRecorder memoryRecorder; DefaultModuleRecorder moduleRecorder; DefaultProcessRecorder processRecorder; - DefaultSymbolRecorder symbolRecorder; + SymbolRecorder symbolRecorder; DefaultTimeRecorder timeRecorder; //protected final PermanentTransactionExecutor seqTx; @@ -94,10 +96,10 @@ public class DefaultTraceRecorder implements TraceRecorder { this.processRecorder = new DefaultProcessRecorder(this); this.breakpointRecorder = new DefaultBreakpointRecorder(this); - this.datatypeRecorder = new DefaultDataTypeRecorder(this); + this.datatypeRecorder = new DataTypeRecorder(this); this.memoryRecorder = new DefaultMemoryRecorder(this); this.moduleRecorder = new DefaultModuleRecorder(this); - this.symbolRecorder = new DefaultSymbolRecorder(this); + this.symbolRecorder = new SymbolRecorder(this); this.timeRecorder = new DefaultTimeRecorder(this); this.objectManager = new TraceObjectManager(target, mapper, this); } @@ -266,7 +268,7 @@ public class DefaultTraceRecorder implements TraceRecorder { /*---------------- CAPTURE METHODS -------------------*/ @Override - public CompletableFuture> captureProcessMemory(AddressSetView set, + public CompletableFuture> readMemoryBlocks(AddressSetView set, TaskMonitor monitor, boolean toMap) { if (set.isEmpty()) { return CompletableFuture.completedFuture(new TreeMap<>()); @@ -499,12 +501,6 @@ public class DefaultTraceRecorder implements TraceRecorder { /*---------------- LISTENER METHODS -------------------*/ - // UNUSED? - @Override - public TraceEventListener getListenerForRecord() { - return objectManager.getEventListener(); - } - public ListenerSet getListeners() { return objectManager.getListeners(); } @@ -526,17 +522,17 @@ public class DefaultTraceRecorder implements TraceRecorder { } @Override - public AddressSetView getAccessibleProcessMemory() { + public AddressSetView getAccessibleMemory() { return processRecorder.getAccessibleProcessMemory(); } @Override - public CompletableFuture readProcessMemory(Address start, int length) { + public CompletableFuture readMemory(Address start, int length) { return processRecorder.readProcessMemory(start, length); } @Override - public CompletableFuture writeProcessMemory(Address start, byte[] data) { + public CompletableFuture writeMemory(Address start, byte[] data) { return processRecorder.writeProcessMemory(start, data); } @@ -566,6 +562,7 @@ public class DefaultTraceRecorder implements TraceRecorder { return true; } + // UNUSED? @Override public CompletableFuture flushTransactions() { return parTx.flush(); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/PermanentTransactionExecutor.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/PermanentTransactionExecutor.java index e99ce6447d..b3687cb400 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/PermanentTransactionExecutor.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/PermanentTransactionExecutor.java @@ -24,20 +24,22 @@ import org.apache.commons.lang3.concurrent.BasicThreadFactory; import ghidra.app.plugin.core.debug.utils.DefaultTransactionCoalescer; import ghidra.app.plugin.core.debug.utils.TransactionCoalescer; import ghidra.app.plugin.core.debug.utils.TransactionCoalescer.CoalescedTx; +import ghidra.framework.model.DomainObjectException; import ghidra.framework.model.UndoableDomainObject; import ghidra.util.Msg; +import ghidra.util.exception.ClosedException; public class PermanentTransactionExecutor { private final TransactionCoalescer txc; - private final Executor[] threads; + private final ExecutorService[] threads; private final UndoableDomainObject obj; public PermanentTransactionExecutor(UndoableDomainObject obj, String name, int threadCount, int delayMs) { this.obj = obj; txc = new DefaultTransactionCoalescer<>(obj, RecorderPermanentTransaction::start, delayMs); - this.threads = new Executor[threadCount]; + this.threads = new ExecutorService[threadCount]; for (int i = 0; i < threadCount; i++) { ThreadFactory factory = new BasicThreadFactory.Builder() .namingPattern(name + "thread-" + i + "-%d") @@ -46,6 +48,12 @@ public class PermanentTransactionExecutor { } } + public void shutdownNow() { + for (ExecutorService t : threads) { + t.shutdownNow(); + } + } + /** * This hash is borrowed from {@link HashMap}, except for the power-of-two masking, since I * don't want to force the thread count to be a power of two (though it probably is). In the @@ -70,6 +78,12 @@ public class PermanentTransactionExecutor { try (CoalescedTx tx = txc.start(description)) { runnable.run(); } + catch (DomainObjectException e) { + if (e.getCause() instanceof ClosedException) { + Msg.info(this, obj + " is closed. Shutting down transaction executor."); + shutdownNow(); + } + } }, selectThread(sel)).exceptionally(e -> { Msg.error(this, "Trouble recording " + description, e); return null; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/RecorderPermanentTransaction.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/RecorderPermanentTransaction.java index 51292926b2..30cd7a5b23 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/RecorderPermanentTransaction.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/RecorderPermanentTransaction.java @@ -20,7 +20,7 @@ import ghidra.util.database.UndoableTransaction; public class RecorderPermanentTransaction implements AutoCloseable { - static RecorderPermanentTransaction start(UndoableDomainObject obj, String description) { + public static RecorderPermanentTransaction start(UndoableDomainObject obj, String description) { UndoableTransaction tid = UndoableTransaction.start(obj, description, true); return new RecorderPermanentTransaction(obj, tid); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/RecorderSimpleMemory.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/RecorderSimpleMemory.java index 4dd8172115..d9e13356a2 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/RecorderSimpleMemory.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/RecorderSimpleMemory.java @@ -79,13 +79,6 @@ public class RecorderSimpleMemory implements AbstractRecorderMemory { } } - /** - * Get accessible memory, as viewed in the trace - * - * @param pred an additional predicate applied via "AND" with accessibility - * @param memMapper target-to-trace mapping utility - * @return the computed set - */ @Override public AddressSet getAccessibleMemory(Predicate pred, DebuggerMemoryMapper memMapper) { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/interfaces/AbstractRecorderMemory.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/interfaces/AbstractRecorderMemory.java index 9d49785cb2..89f6100add 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/interfaces/AbstractRecorderMemory.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/interfaces/AbstractRecorderMemory.java @@ -32,6 +32,13 @@ public interface AbstractRecorderMemory { public CompletableFuture writeMemory(Address address, byte[] data); + /** + * Get accessible memory, as viewed in the trace + * + * @param pred an additional predicate applied via "AND" with accessibility + * @param memMapper target-to-trace mapping utility + * @return the computed set + */ public AddressSet getAccessibleMemory(Predicate pred, DebuggerMemoryMapper memMapper); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultDataTypeRecorder.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/record/DataTypeRecorder.java similarity index 89% rename from Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultDataTypeRecorder.java rename to Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/record/DataTypeRecorder.java index cc9e6bc2a3..3a277359cc 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultDataTypeRecorder.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/record/DataTypeRecorder.java @@ -13,11 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package ghidra.app.plugin.core.debug.service.model; +package ghidra.app.plugin.core.debug.service.model.record; import java.util.*; import java.util.concurrent.CompletableFuture; +import ghidra.app.plugin.core.debug.service.model.RecorderPermanentTransaction; +import ghidra.app.services.TraceRecorder; import ghidra.async.AsyncFence; import ghidra.async.AsyncUtils; import ghidra.dbg.target.*; @@ -27,13 +29,13 @@ import ghidra.program.model.data.*; import ghidra.trace.model.Trace; import ghidra.util.task.TaskMonitor; -public class DefaultDataTypeRecorder { +public class DataTypeRecorder { - //private DefaultTraceRecorder recorder; - private Trace trace; + //private TraceRecorder recorder; + private final Trace trace; private final TargetDataTypeConverter typeConverter; - public DefaultDataTypeRecorder(DefaultTraceRecorder recorder) { + public DataTypeRecorder(TraceRecorder recorder) { //this.recorder = recorder; this.trace = recorder.getTrace(); this.typeConverter = new TargetDataTypeConverter(trace.getDataTypeManager()); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/record/EmptyDebuggerRegisterMapper.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/record/EmptyDebuggerRegisterMapper.java new file mode 100644 index 0000000000..0713157671 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/record/EmptyDebuggerRegisterMapper.java @@ -0,0 +1,65 @@ +/* ### + * 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.model.record; + +import java.util.Set; + +import ghidra.app.plugin.core.debug.mapping.DebuggerRegisterMapper; +import ghidra.app.plugin.core.debug.register.RegisterTypeInfo; +import ghidra.dbg.target.TargetRegister; +import ghidra.program.model.lang.Register; + +public class EmptyDebuggerRegisterMapper implements DebuggerRegisterMapper { + @Override + public TargetRegister getTargetRegister(String name) { + return null; + } + + @Override + public Register getTraceRegister(String name) { + return null; + } + + @Override + public TargetRegister traceToTarget(Register register) { + return null; + } + + @Override + public Register targetToTrace(TargetRegister tReg) { + return null; + } + + @Override + public RegisterTypeInfo getDefaultTypeInfo(Register lReg) { + return null; + } + + @Override + public Set getRegistersOnTarget() { + return null; + } + + @Override + public void targetRegisterAdded(TargetRegister register) { + throw new UnsupportedOperationException(); + } + + @Override + public void targetRegisterRemoved(TargetRegister register) { + throw new UnsupportedOperationException(); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/record/MemoryRecorder.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/record/MemoryRecorder.java new file mode 100644 index 0000000000..aa13d511c6 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/record/MemoryRecorder.java @@ -0,0 +1,179 @@ +/* ### + * 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.model.record; + +import java.nio.ByteBuffer; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.function.*; +import java.util.stream.Collector; +import java.util.stream.Collectors; + +import ghidra.dbg.error.DebuggerMemoryAccessException; +import ghidra.dbg.target.TargetMemory; +import ghidra.dbg.target.TargetMemoryRegion; +import ghidra.program.model.address.*; +import ghidra.trace.model.memory.*; +import ghidra.util.Msg; +import utilities.util.IDKeyed; + +class MemoryRecorder { + protected final ObjectBasedTraceRecorder recorder; + protected final TraceMemoryManager memoryManager; + + protected final Map, TargetMemory> memoriesByTargetSpace = + new HashMap<>(); + protected final Map, AddressRange> regions = new HashMap<>(); + + protected MemoryRecorder(ObjectBasedTraceRecorder recorder) { + this.recorder = recorder; + this.memoryManager = recorder.trace.getMemoryManager(); + } + + private TargetMemory getMemoryForSpace(AddressSpace space) { + return memoriesByTargetSpace.get(new IDKeyed<>(space)); + } + + private void addMemoryForSpace(AddressSpace targetSpace, TargetMemory memory) { + TargetMemory exists = + memoriesByTargetSpace.put(new IDKeyed<>(targetSpace), memory); + if (exists != null && exists != memory) { + Msg.warn(this, + "Address space duplicated between memories: " + exists + " and " + memory); + } + } + + protected void addRegionMemory(TargetMemoryRegion region, TargetMemory memory) { + addMemoryForSpace(region.getRange().getMinAddress().getAddressSpace(), memory); + } + + protected void adjustRegionRange(TargetMemoryRegion region, AddressRange range) { + synchronized (regions) { + AddressRange tRange = recorder.memoryMapper.targetToTrace(range); + if (tRange == null) { + regions.remove(new IDKeyed<>(region)); + } + else { + regions.put(new IDKeyed<>(region), tRange); + } + } + } + + protected void removeMemory(TargetMemory memory) { + while (memoriesByTargetSpace.values().remove(memory)) + ; + } + + protected void removeRegion(TargetMemoryRegion region) { + synchronized (regions) { + regions.remove(new IDKeyed<>(region)); + } + } + + protected CompletableFuture read(Address start, int length) { + Address tStart = recorder.memoryMapper.traceToTarget(start); + if (tStart == null) { + return CompletableFuture.completedFuture(new byte[] {}); + } + TargetMemory memory = getMemoryForSpace(tStart.getAddressSpace()); + if (memory == null) { + return CompletableFuture.completedFuture(new byte[] {}); + } + return memory.readMemory(tStart, length); + } + + protected CompletableFuture write(Address start, byte[] data) { + Address tStart = recorder.memoryMapper.traceToTarget(start); + if (tStart == null) { + throw new IllegalArgumentException( + "Address space " + start.getAddressSpace() + " not defined on the target"); + } + TargetMemory memory = getMemoryForSpace(tStart.getAddressSpace()); + if (memory == null) { + throw new IllegalArgumentException( + "Address space " + tStart.getAddressSpace() + + " cannot be found in target memory"); + } + return memory.writeMemory(tStart, data); + } + + protected void invalidate(TargetMemory memory, long snap) { + Set targetSpaces = memoriesByTargetSpace.entrySet() + .stream() + .filter(e -> e.getValue() == memory) + .map(e -> e.getKey().obj) + .collect(Collectors.toSet()); + for (AddressSpace targetSpace : targetSpaces) { + Address traceMin = recorder.memoryMapper.targetToTrace(targetSpace.getMinAddress()); + Address traceMax = traceMin.getAddressSpace().getMaxAddress(); + memoryManager.setState(snap, traceMin, traceMax, TraceMemoryState.UNKNOWN); + } + } + + protected void recordMemory(long snap, Address start, byte[] data) { + memoryManager.putBytes(snap, start, ByteBuffer.wrap(data)); + } + + public void recordError(long snap, Address tMin, DebuggerMemoryAccessException e) { + // TODO: Bookmark to describe error? + memoryManager.setState(snap, tMin, TraceMemoryState.ERROR); + } + + protected boolean isAccessible(TraceMemoryRegion r) { + // TODO: Perhaps a bit aggressive, but haven't really been checking anyway. + return true; + } + + protected Collector toAddressSet() { + return new Collector<>() { + @Override + public Supplier supplier() { + return AddressSet::new; + } + + @Override + public BiConsumer accumulator() { + return AddressSet::add; + } + + @Override + public BinaryOperator combiner() { + return (s1, s2) -> { + s1.add(s2); + return s1; + }; + } + + @Override + public Function finisher() { + return Function.identity(); + } + + @Override + public Set characteristics() { + return Set.of(); + } + }; + } + + public AddressSetView getAccessible() { + synchronized (regions) { + return regions.values() + .stream() + .collect(toAddressSet()); + } + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/record/ObjectBasedTraceRecorder.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/record/ObjectBasedTraceRecorder.java new file mode 100644 index 0000000000..a3dcbe4608 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/record/ObjectBasedTraceRecorder.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.app.plugin.core.debug.service.model.record; + +import java.lang.invoke.MethodHandles; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +import com.google.common.collect.Range; + +import ghidra.app.plugin.core.debug.mapping.*; +import ghidra.app.plugin.core.debug.service.model.DebuggerModelServicePlugin; +import ghidra.app.plugin.core.debug.service.model.PermanentTransactionExecutor; +import ghidra.app.services.TraceRecorder; +import ghidra.app.services.TraceRecorderListener; +import ghidra.async.AsyncUtils; +import ghidra.dbg.AnnotatedDebuggerAttributeListener; +import ghidra.dbg.error.DebuggerMemoryAccessException; +import ghidra.dbg.error.DebuggerModelAccessException; +import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetEventScope.TargetEventType; +import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; +import ghidra.dbg.util.PathUtils; +import ghidra.program.model.address.*; +import ghidra.program.model.lang.Register; +import ghidra.program.model.lang.RegisterValue; +import ghidra.trace.database.module.TraceObjectSection; +import ghidra.trace.model.Trace; +import ghidra.trace.model.breakpoint.*; +import ghidra.trace.model.memory.TraceMemoryRegion; +import ghidra.trace.model.memory.TraceObjectMemoryRegion; +import ghidra.trace.model.modules.*; +import ghidra.trace.model.stack.TraceObjectStackFrame; +import ghidra.trace.model.stack.TraceStackFrame; +import ghidra.trace.model.target.TraceObject; +import ghidra.trace.model.thread.TraceObjectThread; +import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.model.time.TraceSnapshot; +import ghidra.util.Msg; +import ghidra.util.database.UndoableTransaction; +import ghidra.util.datastruct.ListenerSet; +import ghidra.util.task.TaskMonitor; + +public class ObjectBasedTraceRecorder implements TraceRecorder { + protected static final int POOL_SIZE = Math.min(16, Runtime.getRuntime().availableProcessors()); + protected static final int DELAY_MS = 100; + protected static final int BLOCK_BITS = 12; + + protected final Trace trace; + protected final TargetObject target; + protected final ObjectBasedDebuggerTargetTraceMapper mapper; + protected final DebuggerMemoryMapper memoryMapper; + protected final DebuggerRegisterMapper emptyRegisterMapper = new EmptyDebuggerRegisterMapper(); + + protected final TimeRecorder timeRecorder; + protected final ObjectRecorder objectRecorder; + protected final MemoryRecorder memoryRecorder; + protected final DataTypeRecorder dataTypeRecorder; + protected final SymbolRecorder symbolRecorder; + + // upstream listeners + protected final ListenerForRecord listenerForRecord; + + protected final ListenerSet listeners = + new ListenerSet<>(TraceRecorderListener.class); + + // TODO: I don't like this here. Should ask the model, not the recorder. + protected TargetObject curFocus; + protected boolean valid = true; + + protected class ListenerForRecord extends AnnotatedDebuggerAttributeListener { + private final PermanentTransactionExecutor tx = + new PermanentTransactionExecutor(trace, "OBTraceRecorder: ", POOL_SIZE, DELAY_MS); + + private boolean ignoreInvalidation = false; + + // TODO: Do I need DebuggerCallbackReorderer? + public ListenerForRecord() { + super(MethodHandles.lookup()); + } + + @Override + public void event(TargetObject object, TargetThread eventThread, TargetEventType type, + String description, List parameters) { + if (!valid) { + return; + } + if (type == TargetEventType.RUNNING) { + /** + * Do not permit the current snapshot to be invalidated on account of the target + * running. When the STOP occurs, a new (completely UNKNOWN) snapshot is generated. + */ + ignoreInvalidation = true; + return; + /** + * TODO: Perhaps some configuration for this later. It's kind of interesting to + * record the RUNNING event time, but it gets pedantic when these exist between + * steps. + */ + } + /** + * Snapshot creation should not be offloaded to parallel executor or we may confuse + * event snaps. + */ + TraceObjectThread traceEventThread = + objectRecorder.getTraceInterface(eventThread, TraceObjectThread.class); + timeRecorder.createSnapshot(description, traceEventThread, null); + ignoreInvalidation = false; + // NB. Need not worry about CREATED, LOADED, etc. Just recording objects. + } + + // NB. ignore executionStateChanged, since recording whole model + + @Override + public void invalidateCacheRequested(TargetObject object) { + if (!valid) { + return; + } + if (ignoreInvalidation) { + return; + } + if (object instanceof TargetMemory) { + long snap = timeRecorder.getSnap(); + String path = object.getJoinedPath("."); + tx.execute("Memory invalidated: " + path, () -> { + memoryRecorder.invalidate((TargetMemory) object, snap); + }, path); + } + } + + // NB. ignore registersUpdated. All object-based, now. + + @Override + public void memoryUpdated(TargetObject memory, Address address, byte[] data) { + if (!valid) { + return; + } + long snap = timeRecorder.getSnap(); + String path = memory.getJoinedPath("."); + Address tAddress = getMemoryMapper().targetToTrace(address); + tx.execute("Memory observed: " + path, () -> { + memoryRecorder.recordMemory(snap, tAddress, data); + }, path); + } + + @Override + public void memoryReadError(TargetObject memory, AddressRange range, + DebuggerMemoryAccessException e) { + if (!valid) { + return; + } + long snap = timeRecorder.getSnap(); + String path = memory.getJoinedPath("."); + Address tMin = getMemoryMapper().targetToTrace(range.getMinAddress()); + tx.execute("Memory read error: " + path, () -> { + memoryRecorder.recordError(snap, tMin, e); + }, path); + } + + @AttributeCallback(TargetFocusScope.FOCUS_ATTRIBUTE_NAME) + protected void focusChanged(TargetObject scope, TargetObject focused) { + if (!valid) { + return; + } + // NB. Don't care about ancestry. Focus should be model-wide anyway. + curFocus = focused; + } + + @Override + public void created(TargetObject object) { + if (!valid) { + return; + } + long snap = timeRecorder.getSnap(); + String path = object.getJoinedPath("."); + // Don't offload, because we need a consistent map + try (UndoableTransaction tid = + UndoableTransaction.start(trace, "Object created: " + path, true)) { + objectRecorder.recordCreated(snap, object); + } + } + + @Override + public void invalidated(TargetObject object, TargetObject branch, String reason) { + if (!valid) { + return; + } + long snap = timeRecorder.getSnap(); + String path = object.getJoinedPath("."); + tx.execute("Object invalidated: " + path, () -> { + objectRecorder.recordInvalidated(snap, object); + }, path); + if (object == target) { + stopRecording(); + return; + } + if (object instanceof TargetMemory) { + memoryRecorder.removeMemory((TargetMemory) object); + } + if (object instanceof TargetMemoryRegion) { + memoryRecorder.removeRegion((TargetMemoryRegion) object); + } + } + + @Override + public void attributesChanged(TargetObject object, Collection removed, + Map added) { + if (!valid) { + return; + } + long snap = timeRecorder.getSnap(); + String path = object.getJoinedPath("."); + tx.execute("Object attributes changed: " + path, () -> { + objectRecorder.recordAttributes(snap, object, removed, added); + }, path); + super.attributesChanged(object, removed, added); + } + + @AttributeCallback(TargetMemoryRegion.RANGE_ATTRIBUTE_NAME) + public void rangeChanged(TargetObject object, AddressRange range) { + if (!valid) { + return; + } + if (!(object instanceof TargetMemoryRegion)) { + return; + } + memoryRecorder.adjustRegionRange((TargetMemoryRegion) object, range); + } + + @AttributeCallback(TargetMemoryRegion.MEMORY_ATTRIBUTE_NAME) + public void memoryChanged(TargetObject object, TargetMemory memory) { + if (!valid) { + return; + } + if (!(object instanceof TargetMemoryRegion)) { + return; + } + memoryRecorder.addRegionMemory((TargetMemoryRegion) object, memory); + } + + @Override + public void elementsChanged(TargetObject object, Collection removed, + Map added) { + if (!valid) { + return; + } + long snap = timeRecorder.getSnap(); + String path = object.getJoinedPath("."); + tx.execute("Object elements changed: " + path, () -> { + objectRecorder.recordElements(snap, object, removed, added); + }, path); + } + } + + public ObjectBasedTraceRecorder(DebuggerModelServicePlugin service, Trace trace, + TargetObject target, ObjectBasedDebuggerTargetTraceMapper mapper) { + trace.addConsumer(this); + this.trace = trace; + this.target = target; + this.mapper = mapper; + + // TODO: Don't depend on memory in interface. + // TODO: offerMemory not async + memoryMapper = mapper.offerMemory(null).getNow(null); + + timeRecorder = new TimeRecorder(this); + objectRecorder = new ObjectRecorder(this); + memoryRecorder = new MemoryRecorder(this); + dataTypeRecorder = new DataTypeRecorder(this); + symbolRecorder = new SymbolRecorder(this); + + listenerForRecord = new ListenerForRecord(); + } + + @Override + public CompletableFuture init() { + // TODO: Make this method synchronous? + timeRecorder.createSnapshot("Started recording " + target.getModel(), null, null); + target.getModel().addModelListener(listenerForRecord, true); + return AsyncUtils.NIL; + } + + @Override + public TargetObject getTarget() { + return target; + } + + @Override + public Trace getTrace() { + return trace; + } + + @Override + public long getSnap() { + return timeRecorder.getSnap(); + } + + @Override + public TraceSnapshot forceSnapshot() { + return timeRecorder.forceSnapshot(); + } + + @Override + public boolean isRecording() { + return valid; + } + + @Override + public void stopRecording() { + invalidate(); + fireRecordingStopped(); + } + + protected void invalidate() { + target.getModel().removeModelListener(listenerForRecord); + synchronized (this) { + if (!valid) { + return; + } + valid = false; + } + trace.release(this); + } + + @Override + public void addListener(TraceRecorderListener listener) { + listeners.add(listener); + } + + @Override + public void removeListener(TraceRecorderListener listener) { + listeners.remove(listener); + } + + @Override + public TargetBreakpointLocation getTargetBreakpoint(TraceBreakpoint bpt) { + return objectRecorder.getTargetInterface(bpt, TraceObjectBreakpointLocation.class, + TargetBreakpointLocation.class); + } + + @Override + public TraceBreakpoint getTraceBreakpoint(TargetBreakpointLocation bpt) { + return objectRecorder.getTraceInterface(bpt, TraceObjectBreakpointLocation.class); + } + + @Override + public TargetMemoryRegion getTargetMemoryRegion(TraceMemoryRegion region) { + return objectRecorder.getTargetInterface(region, TraceObjectMemoryRegion.class, + TargetMemoryRegion.class); + } + + @Override + public TraceMemoryRegion getTraceMemoryRegion(TargetMemoryRegion region) { + return objectRecorder.getTraceInterface(region, TraceObjectMemoryRegion.class); + } + + @Override + public TargetModule getTargetModule(TraceModule module) { + return objectRecorder.getTargetInterface(module, TraceObjectModule.class, + TargetModule.class); + } + + @Override + public TraceModule getTraceModule(TargetModule module) { + return objectRecorder.getTraceInterface(module, TraceObjectModule.class); + } + + @Override + public TargetSection getTargetSection(TraceSection section) { + return objectRecorder.getTargetInterface(section, TraceObjectSection.class, + TargetSection.class); + } + + @Override + public TraceSection getTraceSection(TargetSection section) { + return objectRecorder.getTraceInterface(section, TraceObjectSection.class); + } + + @Override + public TargetThread getTargetThread(TraceThread thread) { + return objectRecorder.getTargetInterface(thread, TraceObjectThread.class, + TargetThread.class); + } + + @Override + public TargetExecutionState getTargetThreadState(TargetThread thread) { + return thread.getTypedAttributeNowByName(TargetExecutionStateful.STATE_ATTRIBUTE_NAME, + TargetExecutionState.class, TargetExecutionState.INACTIVE); + } + + @Override + public TargetExecutionState getTargetThreadState(TraceThread thread) { + return getTargetThreadState(getTargetThread(thread)); + } + + @Override + public TargetRegisterBank getTargetRegisterBank(TraceThread thread, int frameLevel) { + return objectRecorder.getTargetFrameInterface(thread, frameLevel, TargetRegisterBank.class); + } + + @Override + public TraceThread getTraceThread(TargetThread thread) { + return objectRecorder.getTraceInterface(thread, TraceObjectThread.class); + } + + @Override + public TraceThread getTraceThreadForSuccessor(TargetObject successor) { + TraceObject traceObject = objectRecorder.toTrace(successor); + return traceObject.queryCanonicalAncestorsInterface(Range.singleton(getSnap()), + TraceObjectThread.class).findFirst().orElse(null); + } + + @Override + public TraceStackFrame getTraceStackFrame(TargetStackFrame frame) { + return objectRecorder.getTraceInterface(frame, TraceObjectStackFrame.class); + } + + @Override + public TraceStackFrame getTraceStackFrameForSuccessor(TargetObject successor) { + TraceObject traceObject = objectRecorder.toTrace(successor); + return traceObject.queryCanonicalAncestorsInterface(Range.singleton(getSnap()), + TraceObjectStackFrame.class).findFirst().orElse(null); + } + + @Override + public TargetStackFrame getTargetStackFrame(TraceThread thread, int frameLevel) { + return objectRecorder.getTargetFrameInterface(thread, frameLevel, TargetStackFrame.class); + } + + @Override + public Set getLiveTargetThreads() { + return trace.getObjectManager() + .getRootObject() + .querySuccessorsInterface(Range.singleton(getSnap()), TraceObjectThread.class) + .map(t -> objectRecorder.getTargetInterface(t.getObject(), TargetThread.class)) + .collect(Collectors.toSet()); + } + + @Override + public DebuggerRegisterMapper getRegisterMapper(TraceThread thread) { + return emptyRegisterMapper; + } + + @Override + public DebuggerMemoryMapper getMemoryMapper() { + return memoryMapper; + } + + @Override + public boolean isRegisterBankAccessible(TargetRegisterBank bank) { + // TODO: This seems a little aggressive, but the accessbility thing is already out of hand + return true; + } + + @Override + public boolean isRegisterBankAccessible(TraceThread thread, int frameLevel) { + // TODO: This seems a little aggressive, but the accessbility thing is already out of hand + return true; + } + + @Override + public AddressSetView getAccessibleMemory() { + return memoryRecorder.getAccessible(); + } + + @Override + public CompletableFuture> captureThreadRegisters( + TraceThread thread, int frameLevel, Set registers) { + return CompletableFuture.completedFuture(Map.of()); + } + + @Override + public CompletableFuture writeThreadRegisters(TraceThread thread, int frameLevel, + Map values) { + throw new UnsupportedOperationException(); + } + + @Override + public CompletableFuture readMemory(Address start, int length) { + return memoryRecorder.read(start, length); + } + + @Override + public CompletableFuture writeMemory(Address start, byte[] data) { + return memoryRecorder.write(start, data); + } + + @Override + public CompletableFuture> readMemoryBlocks( + AddressSetView set, TaskMonitor monitor, boolean returnResult) { + return RecorderUtils.INSTANCE.readMemoryBlocks(this, BLOCK_BITS, set, monitor, + returnResult); + } + + @Override + public CompletableFuture captureDataTypes(TraceModule module, TaskMonitor monitor) { + return dataTypeRecorder.captureDataTypes(getTargetModule(module), monitor); + } + + @Override + public CompletableFuture captureDataTypes(TargetDataTypeNamespace namespace, + TaskMonitor monitor) { + return dataTypeRecorder.captureDataTypes(namespace, monitor); + } + + @Override + public CompletableFuture captureSymbols(TraceModule module, TaskMonitor monitor) { + return symbolRecorder.captureSymbols(getTargetModule(module), monitor); + } + + @Override + public CompletableFuture captureSymbols(TargetSymbolNamespace namespace, + TaskMonitor monitor) { + return symbolRecorder.captureSymbols(namespace, monitor); + } + + @Override + public List collectBreakpointContainers(TargetThread thread) { + if (thread == null) { + return objectRecorder.collectTargetSuccessors(target, + TargetBreakpointSpecContainer.class); + } + return objectRecorder.collectTargetSuccessors(thread, TargetBreakpointSpecContainer.class); + } + + private class BreakpointConvention { + private final TraceObjectThread thread; + private final TraceObject process; + + private BreakpointConvention(TraceObjectThread thread) { + this.thread = thread; + TraceObject object = thread.getObject(); + this.process = object + .queryAncestorsTargetInterface(Range.singleton(getSnap()), TargetProcess.class) + .map(p -> p.getFirstParent(object)) + .findFirst() + .orElse(null); + } + + private boolean appliesTo(TraceObjectBreakpointLocation loc) { + TraceObject object = loc.getObject(); + if (object.queryAncestorsInterface(Range.singleton(getSnap()), TraceObjectThread.class) + .anyMatch(t -> t == thread)) { + return true; + } + if (process == null) { + return false; + } + return object + .queryAncestorsTargetInterface(Range.singleton(getSnap()), TargetProcess.class) + .map(p -> p.getFirstParent(object)) + .anyMatch(p -> p == process); + } + } + + @Override + public List collectBreakpoints(TargetThread thread) { + if (thread == null) { + return objectRecorder.collectTargetSuccessors(target, TargetBreakpointLocation.class); + } + BreakpointConvention convention = new BreakpointConvention( + objectRecorder.getTraceInterface(thread, TraceObjectThread.class)); + return trace.getObjectManager() + .queryAllInterface(Range.singleton(getSnap()), TraceObjectBreakpointLocation.class) + .filter(convention::appliesTo) + .map(tl -> objectRecorder.getTargetInterface(tl.getObject(), + TargetBreakpointLocation.class)) + .collect(Collectors.toList()); + } + + @Override + public Set getSupportedBreakpointKinds() { + return objectRecorder.collectTargetSuccessors(target, TargetBreakpointSpecContainer.class) + .stream() + .flatMap(c -> c.getSupportedBreakpointKinds().stream()) + .map(k -> TraceRecorder.targetToTraceBreakpointKind(k)) + .collect(Collectors.toSet()); + } + + @Override + public boolean isSupportsFocus() { + return objectRecorder.isSupportsFocus; + } + + @Override + public TargetObject getFocus() { + return curFocus; + } + + @Override + public CompletableFuture requestFocus(TargetObject focus) { + for (TargetFocusScope scope : objectRecorder.collectTargetSuccessors(target, + TargetFocusScope.class)) { + if (PathUtils.isAncestor(scope.getPath(), focus.getPath())) { + return scope.requestFocus(focus).thenApply(__ -> true).exceptionally(ex -> { + ex = AsyncUtils.unwrapThrowable(ex); + String msg = "Could not focus " + focus + ": " + ex.getMessage(); + if (ex instanceof DebuggerModelAccessException) { + Msg.info(this, msg); + } + else { + Msg.error(this, msg, ex); + } + return false; + }); + } + } + Msg.info(this, "Could not find suitable focus scope for " + focus); + return CompletableFuture.completedFuture(false); + } + + // UNUSED? + @Override + public CompletableFuture flushTransactions() { + return listenerForRecord.tx.flush(); + } + + protected void fireSnapAdvanced(long key) { + listeners.fire.snapAdvanced(this, key); + } + + protected void fireRecordingStopped() { + listeners.fire.recordingStopped(this); + } + + // TODO: Deprecate/remove the other callbacks: registerBankMapped, *accessibilityChanged +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/record/ObjectRecorder.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/record/ObjectRecorder.java new file mode 100644 index 0000000000..a65a667f04 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/record/ObjectRecorder.java @@ -0,0 +1,290 @@ +/* ### + * 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.model.record; + +import java.util.*; +import java.util.stream.Collectors; + +import org.apache.commons.collections4.BidiMap; +import org.apache.commons.collections4.bidimap.DualHashBidiMap; + +import com.google.common.collect.Range; + +import ghidra.dbg.target.TargetAttacher.TargetAttachKind; +import ghidra.dbg.target.TargetAttacher.TargetAttachKindSet; +import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; +import ghidra.dbg.target.TargetBreakpointSpecContainer.TargetBreakpointKindSet; +import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; +import ghidra.dbg.target.TargetFocusScope; +import ghidra.dbg.target.TargetMethod.TargetParameterMap; +import ghidra.dbg.target.TargetObject; +import ghidra.dbg.target.TargetSteppable.TargetStepKind; +import ghidra.dbg.target.TargetSteppable.TargetStepKindSet; +import ghidra.dbg.target.schema.TargetObjectSchema; +import ghidra.dbg.util.*; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressRange; +import ghidra.trace.model.TraceUniqueObject; +import ghidra.trace.model.target.*; +import ghidra.trace.model.thread.TraceObjectThread; +import ghidra.trace.model.thread.TraceThread; +import ghidra.util.Msg; +import ghidra.util.database.UndoableTransaction; +import utilities.util.IDKeyed; + +class ObjectRecorder { + protected final ObjectBasedTraceRecorder recorder; + protected final TraceObjectManager objectManager; + protected final boolean isSupportsFocus; + + private final BidiMap, IDKeyed> objectMap = + new DualHashBidiMap<>(); + + protected ObjectRecorder(ObjectBasedTraceRecorder recorder) { + this.recorder = recorder; + this.objectManager = recorder.trace.getObjectManager(); + TargetObjectSchema schema = recorder.target.getSchema(); + this.isSupportsFocus = !schema.searchFor(TargetFocusScope.class, false).isEmpty(); + + try (UndoableTransaction tid = + UndoableTransaction.start(recorder.trace, "Create root", true)) { + objectManager.createRootObject(schema); + } + } + + protected TraceObject toTrace(TargetObject targetObject) { + IDKeyed traceObject = objectMap.get(new IDKeyed<>(targetObject)); + return traceObject == null ? null : traceObject.obj; + } + + protected TargetObject toTarget(TraceObject traceObject) { + IDKeyed targetObject = objectMap.getKey(new IDKeyed<>(traceObject)); + return targetObject == null ? null : targetObject.obj; + } + + protected void recordCreated(long snap, TargetObject object) { + TraceObject traceObject; + if (object.isRoot()) { + // Already have the root object + traceObject = objectManager.getRootObject(); + } + else { + traceObject = objectManager + .createObject(TraceObjectKeyPath.of(object.getPath()), Range.atLeast(snap)); + } + synchronized (objectMap) { + objectMap.put(new IDKeyed<>(object), new IDKeyed<>(traceObject)); + } + } + + protected void recordInvalidated(long snap, TargetObject object) { + if (object.isRoot()) { + return; + } + IDKeyed traceObject; + synchronized (objectMap) { + traceObject = objectMap.remove(new IDKeyed<>(object)); + } + if (traceObject == null) { + Msg.error(this, "Unknown object was invalidated: " + object); + return; + } + traceObject.obj.truncateOrDelete(Range.atLeast(snap)); + } + + protected String encodeEnum(Enum e) { + return e.name(); + } + + protected String encodeEnumSet(Set> s) { + return s.stream() + .sorted(Comparator.comparing(Enum::ordinal)) + .map(Enum::name) + .collect(Collectors.joining(",")); + } + + protected Object mapAttribute(Object attribute) { + if (attribute instanceof TargetObject) { + TraceObject traceObject = toTrace((TargetObject) attribute); + if (traceObject == null) { + Msg.error(this, "Unknown object appeared as an attribute: " + attribute); + } + return traceObject; + } + if (attribute instanceof Address) { + Address traceAddress = recorder.memoryMapper.targetToTrace((Address) attribute); + if (traceAddress == null) { + Msg.error(this, "Unmappable address appeared as an attribute: " + attribute); + } + return traceAddress; + } + if (attribute instanceof AddressRange) { + AddressRange traceRange = recorder.memoryMapper.targetToTrace((AddressRange) attribute); + if (traceRange == null) { + Msg.error(this, "Unmappable range appeared as an attribute: " + attribute); + } + return traceRange; + } + if (attribute instanceof TargetAttachKind) { + return encodeEnum((TargetAttachKind) attribute); + } + if (attribute instanceof TargetAttachKindSet) { + return encodeEnumSet((TargetAttachKindSet) attribute); + } + if (attribute instanceof TargetBreakpointKind) { + return encodeEnum((TargetBreakpointKind) attribute); + } + if (attribute instanceof TargetBreakpointKindSet) { + return encodeEnumSet((TargetBreakpointKindSet) attribute); + } + if (attribute instanceof TargetExecutionState) { + return encodeEnum((TargetExecutionState) attribute); + } + if (attribute instanceof TargetParameterMap) { + return "[parameter map not recorded]"; + } + if (attribute instanceof TargetStepKind) { + return encodeEnum((TargetStepKind) attribute); + } + if (attribute instanceof TargetStepKindSet) { + return encodeEnumSet((TargetStepKindSet) attribute); + } + return attribute; + } + + protected void recordAttributes(long snap, TargetObject object, Collection removed, + Map added) { + TraceObject traceObject; + Map traceAdded = new HashMap<>(); + synchronized (objectMap) { + traceObject = toTrace(object); + if (traceObject == null) { + Msg.error(this, "Unknown object had attributes changed: " + object); + return; + } + for (Map.Entry entry : added.entrySet()) { + Object value = mapAttribute(entry.getValue()); + if (value == null) { + continue; + } + traceAdded.put(entry.getKey(), value); + } + } + for (Map.Entry entry : traceAdded.entrySet()) { + traceObject.setAttribute(Range.atLeast(snap), entry.getKey(), entry.getValue()); + } + } + + protected TraceObject mapElement(TargetObject element) { + TraceObject traceObject = toTrace(element); + if (traceObject == null) { + Msg.error(this, "Unknown object appeared as an element: " + element); + return null; + } + return traceObject; + } + + protected void recordElements(long snap, TargetObject object, Collection removed, + Map added) { + TraceObject traceObject; + Map traceAdded = new HashMap<>(); + synchronized (objectMap) { + traceObject = toTrace(object); + if (traceObject == null) { + Msg.error(this, "Unknown object had attributes changed: " + object); + return; + } + for (Map.Entry entry : added.entrySet()) { + Object value = mapElement(entry.getValue()); + if (value == null) { + continue; + } + traceAdded.put(entry.getKey(), value); + } + } + for (Map.Entry entry : traceAdded.entrySet()) { + traceObject.setElement(Range.atLeast(snap), entry.getKey(), entry.getValue()); + } + } + + protected T getTargetInterface( + TraceUniqueObject traceUnique, Class traceObjectIf, Class targetObjectIf) { + if (!traceObjectIf.isAssignableFrom(traceUnique.getClass())) { + return null; + } + TraceObject traceObject = traceObjectIf.cast(traceUnique).getObject(); + return getTargetInterface(traceObject, targetObjectIf); + } + + protected T getTargetInterface(TraceObject traceObject, + Class targetObjectIf) { + TargetObject targetObject = toTarget(traceObject); + return targetObject == null ? null : targetObject.as(targetObjectIf); + } + + protected I getTraceInterface(TargetObject targetObject, + Class traceObjectIf) { + TraceObject traceObject = toTrace(targetObject); + return traceObject == null ? null : traceObject.queryInterface(traceObjectIf); + } + + protected T getTargetFrameInterface(TraceThread thread, int frameLevel, + Class targetObjectIf) { + TraceObject object = ((TraceObjectThread) thread).getObject(); + PathMatcher matcher = object.getTargetSchema().searchFor(targetObjectIf, false); + PathPattern pattern = matcher.getSingletonPattern(); + if (pattern == null) { + return null; + } + PathPredicates applied; + if (pattern.countWildcards() == 0) { + if (frameLevel != 0) { + return null; + } + applied = pattern; + } + else if (pattern.countWildcards() == 1) { + applied = pattern.applyIntKeys(frameLevel); + } + else { + return null; + } + TraceObjectValPath found = object + .getSuccessors(Range.singleton(recorder.getSnap()), applied) + .findAny() + .orElse(null); + if (found == null) { + return null; + } + TraceObject last = found.getLastChild(null); + if (last == null) { + return null; + } + return getTargetInterface(last, targetObjectIf); + } + + protected List collectTargetSuccessors(TargetObject targetSeed, + Class targetIf) { + // TODO: Should this really go through the database? + TraceObject seed = toTrace(targetSeed); + if (seed == null) { + return List.of(); + } + return seed.querySuccessorsTargetInterface(Range.singleton(recorder.getSnap()), targetIf) + .map(p -> toTarget(p.getLastChild(seed)).as(targetIf)) + .collect(Collectors.toList()); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/record/RecorderUtils.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/record/RecorderUtils.java new file mode 100644 index 0000000000..54a1f22e71 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/record/RecorderUtils.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.service.model.record; + +import java.util.NavigableMap; +import java.util.TreeMap; +import java.util.concurrent.CompletableFuture; + +import ghidra.app.services.TraceRecorder; +import ghidra.async.AsyncUtils; +import ghidra.async.TypeSpec; +import ghidra.program.model.address.*; +import ghidra.util.Msg; +import ghidra.util.task.TaskMonitor; + +public enum RecorderUtils { + INSTANCE; + + public AddressSetView quantize(int blockBits, AddressSetView set) { + if (blockBits == 1) { + return set; + } + long blockMask = -1L << blockBits; + AddressSet result = new AddressSet(); + // Not terribly efficient, but this is one range most of the time + for (AddressRange range : set) { + AddressSpace space = range.getAddressSpace(); + Address min = space.getAddress(range.getMinAddress().getOffset() & blockMask); + Address max = space.getAddress(range.getMaxAddress().getOffset() | ~blockMask); + result.add(new AddressRangeImpl(min, max)); + } + return result; + } + + public CompletableFuture> readMemoryBlocks( + TraceRecorder recorder, int blockBits, AddressSetView set, TaskMonitor monitor, + boolean returnResult) { + + // NOTE: I don't intend to warn about the number of requests. + // They're delivered in serial, and there's a cancel button that works + + int blockSize = 1 << blockBits; + int total = 0; + AddressSetView expSet = quantize(blockBits, set) + .intersect(recorder.getTrace() + .getMemoryManager() + .getRegionsAddressSet(recorder.getSnap())); + for (AddressRange r : expSet) { + total += Long.divideUnsigned(r.getLength() + blockSize - 1, blockSize); + } + monitor.initialize(total); + monitor.setMessage("Reading memory"); + // TODO: Read blocks in parallel? Probably NO. Tends to overload the connector. + NavigableMap result = returnResult ? new TreeMap<>() : null; + return AsyncUtils.each(TypeSpec.VOID, expSet.iterator(), (r, loop) -> { + AddressRangeChunker blocks = new AddressRangeChunker(r, blockSize); + AsyncUtils.each(TypeSpec.VOID, blocks.iterator(), (blk, inner) -> { + // The listener in the recorder will copy to the Trace. + monitor.incrementProgress(1); + CompletableFuture future = + recorder.readMemory(blk.getMinAddress(), (int) blk.getLength()); + future.thenAccept(data -> { + if (returnResult) { + result.put(blk.getMinAddress(), data); + } + }).exceptionally(e -> { + Msg.error(this, "Could not read " + blk + ": " + e); + return null; // Continue looping on errors + }).thenApply(__ -> !monitor.isCancelled()).handle(inner::repeatWhile); + }).thenApply(v -> !monitor.isCancelled()).handle(loop::repeatWhile); + }).thenApply(__ -> result); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultSymbolRecorder.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/record/SymbolRecorder.java similarity index 93% rename from Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultSymbolRecorder.java rename to Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/record/SymbolRecorder.java index d67429e4c0..9c12c10d8b 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultSymbolRecorder.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/record/SymbolRecorder.java @@ -13,11 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package ghidra.app.plugin.core.debug.service.model; +package ghidra.app.plugin.core.debug.service.model.record; import java.util.Map; import java.util.concurrent.CompletableFuture; +import ghidra.app.plugin.core.debug.service.model.RecorderPermanentTransaction; +import ghidra.app.services.TraceRecorder; import ghidra.async.AsyncFence; import ghidra.dbg.target.*; import ghidra.dbg.util.PathUtils; @@ -30,12 +32,12 @@ import ghidra.util.exception.DuplicateNameException; import ghidra.util.exception.InvalidInputException; import ghidra.util.task.TaskMonitor; -public class DefaultSymbolRecorder { +public class SymbolRecorder { - private DefaultTraceRecorder recorder; - private Trace trace; + private final TraceRecorder recorder; + private final Trace trace; - public DefaultSymbolRecorder(DefaultTraceRecorder recorder) { + public SymbolRecorder(TraceRecorder recorder) { this.recorder = recorder; this.trace = recorder.getTrace(); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/record/TimeRecorder.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/record/TimeRecorder.java new file mode 100644 index 0000000000..be754a33bc --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/record/TimeRecorder.java @@ -0,0 +1,66 @@ +/* ### + * 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.model.record; + +import ghidra.app.plugin.core.debug.service.model.RecorderPermanentTransaction; +import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.model.time.TraceSnapshot; + +class TimeRecorder { + protected final ObjectBasedTraceRecorder recorder; + + protected TraceSnapshot snapshot = null; + + protected TimeRecorder(ObjectBasedTraceRecorder recorder) { + this.recorder = recorder; + } + + protected TraceSnapshot getSnapshot() { + return snapshot; + } + + protected long getSnap() { + return snapshot.getKey(); + } + + protected synchronized TraceSnapshot doCreateSnapshot(String description, + TraceThread eventThread) { + snapshot = recorder.trace.getTimeManager().createSnapshot(description); + snapshot.setEventThread(eventThread); + return snapshot; + } + + protected TraceSnapshot createSnapshot(String description, TraceThread eventThread, + RecorderPermanentTransaction tid) { + TraceSnapshot snapshot; + if (tid != null) { + snapshot = doCreateSnapshot(description, eventThread); + + } + else { + try (RecorderPermanentTransaction tid2 = + RecorderPermanentTransaction.start(recorder.trace, description)) { + snapshot = doCreateSnapshot(description, eventThread); + } + } + recorder.fireSnapAdvanced(snapshot.getKey()); + return snapshot; + } + + protected TraceSnapshot forceSnapshot() { + return createSnapshot("User-forced snapshot", null, null); + } +} 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 c4e1b0c782..5370389b50 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 @@ -83,7 +83,7 @@ public class DisassembleAtPcDebuggerBot implements DebuggerBot { this.viewport = trace.getProgramView().getViewport(); this.pc = trace.getBaseLanguage().getProgramCounter(); - this.pcRange = TraceRegisterUtils.rangeForRegister(pc); + this.pcRange = pc == null ? null : TraceRegisterUtils.rangeForRegister(pc); ClassSearcher.addChangeListener(injectsChangeListener); updateInjects(); @@ -140,9 +140,9 @@ public class DisassembleAtPcDebuggerBot implements DebuggerBot { } } - private void stackChanged(TraceStack stack) { + private void stackChanged(TraceStack stack, long zero, long snap) { queueRunnable(() -> { - disassembleStackPcVals(stack, stack.getSnap(), null); + disassembleStackPcVals(stack, snap, null); }); } @@ -186,7 +186,7 @@ public class DisassembleAtPcDebuggerBot implements DebuggerBot { if (space.getFrameLevel() != 0) { return; } - if (!range.getRange().intersects(pcRange)) { + if (pcRange == null || !range.getRange().intersects(pcRange)) { return; } TraceThread thread = space.getThread(); @@ -198,18 +198,18 @@ public class DisassembleAtPcDebuggerBot implements DebuggerBot { }); } - protected void disassembleStackPcVals(TraceStack stack, long memSnap, AddressRange range) { + protected void disassembleStackPcVals(TraceStack stack, long snap, AddressRange range) { TraceStackFrame frame = stack.getFrame(0, false); if (frame == null) { return; } - Address pcVal = frame.getProgramCounter(); + Address pcVal = frame.getProgramCounter(snap); if (pcVal == null) { return; } 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(), memSnap); + disassemble(pcVal, stack.getThread(), snap); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/TraceRecorder.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/TraceRecorder.java index 3bc79e589d..6f6a8186e5 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/TraceRecorder.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/TraceRecorder.java @@ -21,11 +21,9 @@ import java.util.stream.Collectors; import ghidra.app.plugin.core.debug.mapping.DebuggerMemoryMapper; import ghidra.app.plugin.core.debug.mapping.DebuggerRegisterMapper; -import ghidra.app.plugin.core.debug.service.model.TraceEventListener; import ghidra.dbg.target.*; import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; -import ghidra.lifecycle.Internal; import ghidra.pcode.utils.Utils; import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressSetView; @@ -251,11 +249,11 @@ public interface TraceRecorder { boolean isRegisterBankAccessible(TraceThread thread, int frameLevel); /** - * Get the set of accessible process memory, as viewed in the trace + * Get the set of accessible target memory, as viewed in the trace * * @return the computed set */ - AddressSetView getAccessibleProcessMemory(); + AddressSetView getAccessibleMemory(); /** * Capture a target thread's registers. @@ -293,50 +291,49 @@ public interface TraceRecorder { Map values); /** - * Read (and capture) a range of process memory - * - *

- * This will not quantize the blocks; whereas - * {@link #captureProcessMemory(AddressSetView, TaskMonitor)} will. + * Read (and capture) a range of target memory * * @param start the address to start at, as viewed in the trace * @param length the number of bytes to read * @return a future which completes with the read bytes */ - CompletableFuture readProcessMemory(Address start, int length); + CompletableFuture readMemory(Address start, int length); /** - * Write (and capture) a range of process memory + * Write (and capture) a range of target memory * * @param start the address to start at, as viewed in the trace * @param data the data to write * @return a future which completes when the entire write is complete */ - CompletableFuture writeProcessMemory(Address start, byte[] data); + CompletableFuture writeMemory(Address start, byte[] data); /** - * Capture a portion of the target's memory. + * Read (and capture) several blocks of target memory * *

- * Though this function returns immediately, the given monitor will be updated in the background - * as the task progresses. Thus, the caller should ensure the monitor is visible until the - * returned future completes. + * The given address set is quantized to the minimal set of blocks covering the requested set. + * To capture a precise range, use {@link #readMemory(Address, int)} instead. Though this + * function returns immediately, the given monitor will be updated in the background as the task + * progresses. Thus, the caller should ensure the monitor is visible until the returned future + * completes. * *

* This task is relatively error tolerant. If a block or region cannot be captured -- a common - * occurrence -- the error is logged, but the future may still complete successfully. For large - * captures, it is recommended to set {@code toMap} to false. The recorder will place the bytes - * into the trace where they can be retrieved later. For small captures, and where bypassing the - * database may offer some advantage, set {@code toMap} to true, and the captured bytes will be - * returned in an interval map. Connected intervals may or may not be joined. + * occurrence -- the error is logged, but the task may still complete "successfully." For large + * captures, it is recommended to set {@code returnResult} to false. The recorder will capture + * the bytes into the trace where they can be retrieved later. For small captures, and where + * bypassing the database may offer some advantage, set {@code returnResult} to true, and the + * captured bytes will be returned in an interval map. Connected intervals may or may not be + * joined. * - * @param selection the addresses to capture, as viewed in the trace + * @param set the addresses to capture, as viewed in the trace * @param monitor a monitor for displaying task steps - * @param toMap true to return results in a map, false to complete with null - * @return a future which completes with the capture results + * @param returnResult true to complete with results, false to complete with null + * @return a future which completes when the task finishes */ - CompletableFuture> captureProcessMemory(AddressSetView selection, - TaskMonitor monitor, boolean toMap); + CompletableFuture> readMemoryBlocks(AddressSetView set, + TaskMonitor monitor, boolean returnResult); /** * Write a variable (memory or register) of the given thread or the process @@ -345,7 +342,7 @@ public interface TraceRecorder { * This is a convenience for writing target memory or registers, based on address. If the given * address represents a register, this will attempt to map it to a register and write it in the * given thread and frame. If the address is in memory, it will simply delegate to - * {@link #writeProcessMemory(Address, byte[])}. + * {@link #writeMemory(Address, byte[])}. * * @param thread the thread. Ignored (may be null) if address is in memory * @param frameLevel the frame, usually 0. Ignored if address is in memory @@ -356,7 +353,7 @@ public interface TraceRecorder { default CompletableFuture writeVariable(TraceThread thread, int frameLevel, Address address, byte[] data) { if (address.isMemoryAddress()) { - return writeProcessMemory(address, data); + return writeMemory(address, data); } if (address.isRegisterAddress()) { Language lang = getTrace().getBaseLanguage(); @@ -550,22 +547,6 @@ public interface TraceRecorder { */ CompletableFuture requestFocus(TargetObject focus); - /** - * Get the internal listener on the model used by the recorder - * - *

- * This allows external "hints" to be given to the recorder by manually injecting objects into - * its listener. - * - *

- * TODO: This is a bit of a stop-gap until we have a better way of drawing the recorder's - * attention to certain object, or otherwise controlling what it records. - * - * @return the listener - */ - @Internal - TraceEventListener getListenerForRecord(); - /** * Wait for pending transactions finish execution. * diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/pcode/exec/TraceRecorderAsyncPcodeExecutorState.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/pcode/exec/TraceRecorderAsyncPcodeExecutorState.java index f7724b0b32..88476a441e 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/pcode/exec/TraceRecorderAsyncPcodeExecutorState.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/pcode/exec/TraceRecorderAsyncPcodeExecutorState.java @@ -76,7 +76,7 @@ public class TraceRecorderAsyncPcodeExecutorState Address addr = space.getAddress(truncateOffset(space, offset)); AddressSet set = new AddressSet(addr, space.getAddress(offset + size - 1)); CompletableFuture> future = - recorder.captureProcessMemory(set, TaskMonitor.DUMMY, true); + recorder.readMemoryBlocks(set, TaskMonitor.DUMMY, true); return future.thenApply(map -> { return knitFromResults(map, addr, size); }); diff --git a/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackPluginScreenShots.java b/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackPluginScreenShots.java index c389c5889e..cf8d62bc72 100644 --- a/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackPluginScreenShots.java +++ b/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackPluginScreenShots.java @@ -106,11 +106,11 @@ public class DebuggerStackPluginScreenShots extends GhidraScreenShotGenerator { TraceStackFrame frame; frame = stack.getFrame(0, false); - frame.setProgramCounter(tb.addr(0x00404321)); + frame.setProgramCounter(Range.all(), tb.addr(0x00404321)); frame = stack.getFrame(1, false); - frame.setProgramCounter(tb.addr(0x00401234)); + frame.setProgramCounter(Range.all(), tb.addr(0x00401234)); frame = stack.getFrame(2, false); - frame.setProgramCounter(tb.addr(0x00401001)); + frame.setProgramCounter(Range.all(), tb.addr(0x00401001)); } root.createFile("trace", tb.trace, TaskMonitor.DUMMY); root.createFile("echo", program, TaskMonitor.DUMMY); diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/AbstractGhidraHeadedDebuggerGUITest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/AbstractGhidraHeadedDebuggerGUITest.java index 9d542083de..3e72a91702 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/AbstractGhidraHeadedDebuggerGUITest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/AbstractGhidraHeadedDebuggerGUITest.java @@ -454,8 +454,13 @@ public abstract class AbstractGhidraHeadedDebuggerGUITest protected static void performEnabledAction(ActionContextProvider provider, DockingActionIf action, boolean wait) { - ActionContext context = provider.getActionContext(null); - waitForCondition(() -> action.isEnabledForContext(context)); + ActionContext context = waitForValue(() -> { + ActionContext ctx = provider.getActionContext(null); + if (!action.isEnabledForContext(ctx)) { + return null; + } + return ctx; + }); performAction(action, context, wait); } diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProviderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProviderTest.java index 4517a5a098..0966dc3f88 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProviderTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProviderTest.java @@ -15,7 +15,7 @@ */ package ghidra.app.plugin.core.debug.gui.listing; -import static ghidra.lifecycle.Unfinished.*; +import static ghidra.lifecycle.Unfinished.TODO; import static org.junit.Assert.*; import java.awt.Color; @@ -1254,8 +1254,8 @@ public class DebuggerListingProviderTest extends AbstractGhidraHeadedDebuggerGUI thread = tb.getOrAddThread("Thread 1", 0); DBTraceStackManager sm = tb.trace.getStackManager(); TraceStack stack = sm.getStack(thread, 0, true); - stack.getFrame(0, true).setProgramCounter(tb.addr(0x00401234)); - stack.getFrame(1, true).setProgramCounter(tb.addr(0x00404321)); + stack.getFrame(0, true).setProgramCounter(Range.all(), tb.addr(0x00401234)); + stack.getFrame(1, true).setProgramCounter(Range.all(), tb.addr(0x00404321)); } waitForDomainObject(tb.trace); traceManager.activateThread(thread); @@ -1346,7 +1346,7 @@ public class DebuggerListingProviderTest extends AbstractGhidraHeadedDebuggerGUI TraceMemoryFlag.READ, TraceMemoryFlag.EXECUTE); thread = tb.getOrAddThread("Thread 1", 0); TraceStack stack = sm.getStack(thread, 0, true); - stack.getFrame(0, true).setProgramCounter(tb.addr(0x00401234)); + stack.getFrame(0, true).setProgramCounter(Range.all(), tb.addr(0x00401234)); } waitForDomainObject(tb.trace); traceManager.activateThread(thread); @@ -1356,7 +1356,7 @@ public class DebuggerListingProviderTest extends AbstractGhidraHeadedDebuggerGUI try (UndoableTransaction tid = tb.startTransaction()) { TraceStack stack = sm.getStack(thread, 0, true); - stack.getFrame(0, true).setProgramCounter(tb.addr(0x00404321)); + stack.getFrame(0, true).setProgramCounter(Range.all(), tb.addr(0x00404321)); } waitForDomainObject(tb.trace); diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerMemoryBytesProviderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerMemoryBytesProviderTest.java index a87775cae5..b0708d7795 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerMemoryBytesProviderTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerMemoryBytesProviderTest.java @@ -969,8 +969,8 @@ public class DebuggerMemoryBytesProviderTest extends AbstractGhidraHeadedDebugge thread = tb.getOrAddThread("Thread 1", 0); DBTraceStackManager sm = tb.trace.getStackManager(); TraceStack stack = sm.getStack(thread, 0, true); - stack.getFrame(0, true).setProgramCounter(tb.addr(0x00401234)); - stack.getFrame(1, true).setProgramCounter(tb.addr(0x00404321)); + stack.getFrame(0, true).setProgramCounter(Range.all(), tb.addr(0x00401234)); + stack.getFrame(1, true).setProgramCounter(Range.all(), tb.addr(0x00404321)); } waitForDomainObject(tb.trace); traceManager.activateThread(thread); @@ -1061,7 +1061,7 @@ public class DebuggerMemoryBytesProviderTest extends AbstractGhidraHeadedDebugge TraceMemoryFlag.READ, TraceMemoryFlag.EXECUTE); thread = tb.getOrAddThread("Thread 1", 0); TraceStack stack = sm.getStack(thread, 0, true); - stack.getFrame(0, true).setProgramCounter(tb.addr(0x00401234)); + stack.getFrame(0, true).setProgramCounter(Range.all(), tb.addr(0x00401234)); } waitForDomainObject(tb.trace); traceManager.activateThread(thread); @@ -1071,7 +1071,7 @@ public class DebuggerMemoryBytesProviderTest extends AbstractGhidraHeadedDebugge try (UndoableTransaction tid = tb.startTransaction()) { TraceStack stack = sm.getStack(thread, 0, true); - stack.getFrame(0, true).setProgramCounter(tb.addr(0x00404321)); + stack.getFrame(0, true).setProgramCounter(Range.all(), tb.addr(0x00404321)); } waitForDomainObject(tb.trace); diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackProviderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackProviderTest.java index 92e7ef8ac7..41da51bc7f 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackProviderTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackProviderTest.java @@ -94,11 +94,11 @@ public class DebuggerStackProviderTest extends AbstractGhidraHeadedDebuggerGUITe stack.setDepth(2, false); TraceStackFrame frame = stack.getFrame(0, false); - frame.setProgramCounter(tb.addr(0x00400100)); + frame.setProgramCounter(Range.all(), tb.addr(0x00400100)); frame.setComment("Hello"); frame = stack.getFrame(1, false); - frame.setProgramCounter(tb.addr(0x00400200)); + frame.setProgramCounter(Range.all(), tb.addr(0x00400200)); frame.setComment("World"); } } diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/model/TestKnownArchDebuggerMappingOpinion.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/model/TestKnownArchDebuggerMappingOpinion.java index 9dcfef8c66..d84212f2d1 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/model/TestKnownArchDebuggerMappingOpinion.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/model/TestKnownArchDebuggerMappingOpinion.java @@ -19,7 +19,7 @@ import java.util.Set; import ghidra.app.plugin.core.debug.mapping.*; import ghidra.dbg.target.TargetEnvironment; -import ghidra.dbg.target.TargetProcess; +import ghidra.dbg.target.TargetObject; import ghidra.program.model.lang.CompilerSpecID; import ghidra.program.model.lang.LanguageID; @@ -27,12 +27,12 @@ public class TestKnownArchDebuggerMappingOpinion implements DebuggerMappingOpini public static final String ARCH = "test-known-arch"; @Override - public Set offersForEnv(TargetEnvironment env, TargetProcess process, + public Set offersForEnv(TargetEnvironment env, TargetObject target, boolean includeOverrides) { if (!ARCH.equals(env.getArchitecture())) { return Set.of(); } - return Set.of(new DefaultDebuggerMappingOffer(process, 100, "Offer for test-known-arch", + return Set.of(new DefaultDebuggerMappingOffer(target, 100, "Offer for test-known-arch", new LanguageID(DebuggerModelServiceTest.LANGID_TOYBE64), new CompilerSpecID("default"), Set.of())); } diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/model/record/ObjectBasedTraceRecorderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/model/record/ObjectBasedTraceRecorderTest.java new file mode 100644 index 0000000000..1bcc5e3a93 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/model/record/ObjectBasedTraceRecorderTest.java @@ -0,0 +1,565 @@ +/* ### + * 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.model.record; + +import static org.hamcrest.Matchers.isOneOf; +import static org.junit.Assert.*; + +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.util.*; + +import org.junit.Test; + +import com.google.common.collect.Range; + +import generic.Unique; +import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest; +import ghidra.app.plugin.core.debug.mapping.*; +import ghidra.app.services.ActionSource; +import ghidra.app.services.TraceRecorder; +import ghidra.async.AsyncTestUtils; +import ghidra.dbg.error.DebuggerMemoryAccessException; +import ghidra.dbg.model.*; +import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; +import ghidra.dbg.target.TargetEventScope.TargetEventType; +import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressRangeImpl; +import ghidra.trace.model.ImmutableTraceAddressSnapRange; +import ghidra.trace.model.TraceAddressSnapRange; +import ghidra.trace.model.breakpoint.*; +import ghidra.trace.model.memory.*; +import ghidra.trace.model.modules.*; +import ghidra.trace.model.stack.TraceStackFrame; +import ghidra.trace.model.target.*; +import ghidra.trace.model.thread.*; +import ghidra.trace.model.time.TraceSnapshot; +import ghidra.trace.model.time.TraceTimeManager; +import ghidra.util.task.TaskMonitor; + +public class ObjectBasedTraceRecorderTest extends AbstractGhidraHeadedDebuggerGUITest + implements AsyncTestUtils { + DebuggerMappingOpinion opinion = new ObjectBasedDebuggerMappingOpinion(); + TraceRecorder recorder; + + TraceBreakpointManager breaks; + TraceObjectManager objects; + TraceMemoryManager memory; + TraceModuleManager modules; + TraceThreadManager threads; + TraceTimeManager time; + + protected void startRecording() throws Exception { + createTestModel(); + TargetObject target = mb.testModel.session; + DebuggerTargetTraceMapper mapper = Unique.assertOne(opinion.getOffers(target, true)).take(); + recorder = modelService.recordTarget(mb.testModel.session, mapper, ActionSource.MANUAL); + + useTrace(recorder.getTrace()); + breaks = tb.trace.getBreakpointManager(); + objects = tb.trace.getObjectManager(); + memory = tb.trace.getMemoryManager(); + modules = tb.trace.getModuleManager(); + threads = tb.trace.getThreadManager(); + time = tb.trace.getTimeManager(); + } + + protected void dumpValues(TraceObject obj) { + System.err.println("Values of " + obj); + for (TraceObjectValue val : obj.getValues()) { + System.err.println(" " + val.getEntryKey() + " = " + val.getValue()); + } + } + + protected void dumpValues(TraceObjectInterface obj) { + dumpValues(obj.getObject()); + } + + protected void dumpObjects() { + System.err.println("All objects:"); + for (TraceObject object : objects.getAllObjects()) { + System.err.println(" " + object); + } + } + + @Test + public void testRecordBaseSession() throws Throwable { + startRecording(); + + waitForPass(noExc(() -> { + waitOn(recorder.flushTransactions()); + assertEquals(5, objects.getAllObjects().size()); + })); + } + + @Test + public void testCloseModelStopsRecording() throws Throwable { + startRecording(); + waitForPass(() -> assertTrue(recorder.isRecording())); + + waitOn(mb.testModel.close()); + waitForPass(() -> assertFalse(recorder.isRecording())); + } + + @Test + public void testRecordEvents() throws Throwable { + startRecording(); + + waitForPass(() -> assertEquals(0, recorder.getSnap())); + mb.testModel.fire() + .event(mb.testModel.session, null, TargetEventType.RUNNING, "Test RUNNING", + List.of()); + mb.testModel.fire() + .event(mb.testModel.session, null, TargetEventType.STOPPED, "Test STOPPED", + List.of()); + waitForPass(() -> { + assertEquals(1, recorder.getSnap()); + TraceSnapshot snapshot = time.getSnapshot(1, false); + assertEquals("Test STOPPED", snapshot.getDescription()); + }); + + mb.createTestProcessesAndThreads(); + mb.testModel.fire() + .event(mb.testModel.session, mb.testThread1, TargetEventType.STEP_COMPLETED, + "Test STEP", List.of()); + waitForPass(() -> { + assertEquals(2, recorder.getSnap()); + TraceSnapshot snapshot = time.getSnapshot(2, false); + assertEquals("Test STEP", snapshot.getDescription()); + TraceThread thread = recorder.getTraceThread(mb.testThread1); + assertEquals(thread, snapshot.getEventThread()); + }); + } + + @Test + public void testRecordThreads() throws Throwable { + startRecording(); + mb.createTestProcessesAndThreads(); + + waitForPass(noExc(() -> { + waitOn(recorder.flushTransactions()); + assertEquals(4, threads.getAllThreads().size()); + + TraceThread thread = recorder.getTraceThread(mb.testThread1); + assertEquals(mb.testThread1, recorder.getTargetThread(thread)); + + assertEquals(TargetExecutionState.STOPPED, + recorder.getTargetThreadState(mb.testThread1)); + assertEquals(TargetExecutionState.STOPPED, recorder.getTargetThreadState(thread)); + })); + + assertEquals(Set.of(mb.testThread1, mb.testThread2, mb.testThread3, mb.testThread4), + recorder.getLiveTargetThreads()); + } + + @Test + public void testRecordThreadNameReuse() throws Throwable { + startRecording(); + mb.createTestProcessesAndThreads(); + TraceThread thread1a = waitForValue(() -> recorder.getTraceThread(mb.testThread1)); + + recorder.forceSnapshot(); + mb.testProcess1.threads.removeThreads(mb.testThread1); + + waitForPass(() -> assertEquals(Range.singleton(0L), thread1a.getLifespan())); + assertNull(recorder.getTraceThread(mb.testThread1)); + + recorder.forceSnapshot(); + mb.testThread1 = mb.testProcess1.addThread(1); + TraceThread thread1b = waitForValue(() -> recorder.getTraceThread(mb.testThread1)); + + assertNotSame(thread1a, thread1b); + } + + @Test + public void testRecordMemoryRegion() throws Throwable { + startRecording(); + mb.createTestProcessesAndThreads(); + TestTargetMemoryRegion text = + mb.testProcess1.memory.addRegion("exe:.text", mb.rng(0x00400000, 0x00400fff), "wrx"); + + waitForPass(() -> { + TraceMemoryRegion region = Unique.assertOne(memory.getAllRegions()); + assertEquals("[exe:.text]", region.getName()); + assertEquals("Processes[1].Memory[exe:.text]", region.getPath()); + assertEquals(tb.range(0x00400000, 0x00400fff), region.getRange()); + assertEquals( + Set.of(TraceMemoryFlag.READ, TraceMemoryFlag.WRITE, TraceMemoryFlag.EXECUTE), + region.getFlags()); + + assertEquals(region, recorder.getTraceMemoryRegion(text)); + assertEquals(text, recorder.getTargetMemoryRegion(region)); + }); + } + + @Test + public void testRecordMemoryAddressSet() throws Throwable { + startRecording(); + mb.createTestProcessesAndThreads(); + + mb.testProcess1.memory.addRegion("exe:.text", mb.rng(0x00400000, 0x00400fff), "rx"); + waitForPass(() -> { + assertEquals(tb.set(tb.range(0x00400000, 0x00400fff)), recorder.getAccessibleMemory()); + }); + + TestTargetMemoryRegion data = + mb.testProcess1.memory.addRegion("exe:.data", mb.rng(0x00401000, 0x00401fff), "wr"); + waitForPass(() -> { + assertEquals(tb.set(tb.range(0x00400000, 0x00401fff)), recorder.getAccessibleMemory()); + }); + + mb.testProcess1.memory.removeRegion(data); + waitForPass(() -> { + assertEquals(tb.set(tb.range(0x00400000, 0x00400fff)), recorder.getAccessibleMemory()); + }); + + mb.testModel.removeProcess(mb.testProcess1); + waitForPass(() -> { + assertEquals(tb.set(), recorder.getAccessibleMemory()); + }); + } + + @Test + public void testRecordMemoryBytes() throws Throwable { + startRecording(); + mb.createTestProcessesAndThreads(); + mb.testProcess1.memory.addRegion("exe:.text", mb.rng(0x00400000, 0x00400fff), "wrx"); + mb.testProcess1.memory.writeMemory(mb.addr(0x00400123), mb.arr(1, 2, 3, 4, 5, 6, 7, 8, 9)); + + byte[] data = new byte[9]; + waitForPass(() -> { + memory.getBytes(recorder.getSnap(), tb.addr(0x00400123), ByteBuffer.wrap(data)); + assertArrayEquals(tb.arr(1, 2, 3, 4, 5, 6, 7, 8, 9), data); + }); + } + + protected void flushAndWait() throws Throwable { + waitOn(mb.testModel.flushEvents()); + waitOn(recorder.flushTransactions()); + waitForDomainObject(tb.trace); + } + + @Test + public void testRecordMemoryInvalidateCacheRequested() throws Throwable { + startRecording(); + mb.createTestProcessesAndThreads(); + + mb.testProcess1.memory.addRegion("exe:.text", mb.rng(0x00400000, 0x00400fff), "rwx"); + + waitOn(mb.testProcess1.memory.writeMemory(mb.addr(0x00400123), + mb.arr(1, 2, 3, 4, 5, 6, 7, 8, 9))); + flushAndWait(); + assertEquals(TraceMemoryState.KNOWN, + memory.getState(recorder.getSnap(), tb.addr(0x00400123))); + + mb.testModel.fire().invalidateCacheRequested(mb.testProcess1.memory); + flushAndWait(); + assertEquals(TraceMemoryState.UNKNOWN, + memory.getState(recorder.getSnap(), tb.addr(0x00400123))); + + waitOn(mb.testProcess1.memory.writeMemory(mb.addr(0x00400123), + mb.arr(1, 2, 3, 4, 5, 6, 7, 8, 9))); + flushAndWait(); + assertEquals(TraceMemoryState.KNOWN, + memory.getState(recorder.getSnap(), tb.addr(0x00400123))); + + mb.testModel.fire() + .event(mb.testModel.session, null, TargetEventType.RUNNING, "Test RUNNING", + List.of()); + mb.testModel.fire().invalidateCacheRequested(mb.testProcess1.memory); + flushAndWait(); + assertEquals(TraceMemoryState.KNOWN, + memory.getState(recorder.getSnap(), tb.addr(0x00400123))); + + mb.testModel.fire() + .event(mb.testModel.session, null, TargetEventType.STOPPED, "Test STOPPED", + List.of()); + flushAndWait(); + assertEquals(TraceMemoryState.UNKNOWN, + memory.getState(recorder.getSnap(), tb.addr(0x00400123))); + } + + @Test + public void testReadMemory() throws Throwable { + startRecording(); + mb.createTestProcessesAndThreads(); + + mb.testProcess1.memory.addRegion("exe:.text", mb.rng(0x00400000, 0x00400fff), "rwx"); + mb.testProcess1.memory.setMemory(tb.addr(0x00400123), mb.arr(1, 2, 3, 4, 5, 6, 7, 8, 9)); + flushAndWait(); + assertThat(memory.getState(recorder.getSnap(), tb.addr(0x00400123)), + isOneOf(null, TraceMemoryState.UNKNOWN)); + + byte[] data = new byte[10]; + waitOn(recorder.readMemory(tb.addr(0x00400123), 10)); + flushAndWait(); + assertEquals(TraceMemoryState.KNOWN, + memory.getState(recorder.getSnap(), tb.addr(0x00400123))); + memory.getBytes(recorder.getSnap(), tb.addr(0x00400123), ByteBuffer.wrap(data)); + assertArrayEquals(tb.arr(1, 2, 3, 4, 5, 6, 7, 8, 9, 0), data); + } + + protected Map.Entry stateEntry(long min, long max, + TraceMemoryState state) { + return Map.entry(new ImmutableTraceAddressSnapRange(tb.range(min, max), recorder.getSnap()), + state); + } + + @Test + public void testReadMemoryBlocks() throws Throwable { + startRecording(); + mb.createTestProcessesAndThreads(); + + mb.testProcess1.memory.addRegion("exe:.text", mb.rng(0x00400000, 0x00400fff), "rx"); + mb.testProcess1.memory.addRegion("exe:.data", mb.rng(0x00600000, 0x00602fff), "rw"); + mb.testProcess1.memory.setMemory(tb.addr(0x00400123), mb.arr(1, 2, 3, 4, 5, 6, 7, 8, 9)); + flushAndWait(); + assertThat(memory.getState(recorder.getSnap(), tb.addr(0x00400123)), + isOneOf(null, TraceMemoryState.UNKNOWN)); + + byte[] data = new byte[10]; + assertNull(waitOn(recorder.readMemoryBlocks( + tb.set(tb.range(0x00400123, 0x00400123), tb.range(0x00600ffe, 0x00601000)), + TaskMonitor.DUMMY, false))); + flushAndWait(); + assertEquals(Set.of( + stateEntry(0x00400000, 0x00400fff, TraceMemoryState.KNOWN), + stateEntry(0x00600000, 0x00601fff, TraceMemoryState.KNOWN)), + Set.copyOf(memory.getStates(recorder.getSnap(), tb.range(0x00400000, 0x00602fff)))); + memory.getBytes(recorder.getSnap(), tb.addr(0x00400123), ByteBuffer.wrap(data)); + assertArrayEquals(tb.arr(1, 2, 3, 4, 5, 6, 7, 8, 9, 0), data); + } + + @Test + public void testWriteMemory() throws Throwable { + startRecording(); + mb.createTestProcessesAndThreads(); + + mb.testProcess1.memory.addRegion("exe:.text", mb.rng(0x00400000, 0x00400fff), "rwx"); + flushAndWait(); + assertThat(memory.getState(recorder.getSnap(), tb.addr(0x00400123)), + isOneOf(null, TraceMemoryState.UNKNOWN)); + + byte[] data = new byte[10]; + waitOn(recorder.writeMemory(tb.addr(0x00400123), tb.arr(1, 2, 3, 4, 5, 6, 7, 8, 9))); + flushAndWait(); + assertEquals(TraceMemoryState.KNOWN, + memory.getState(recorder.getSnap(), tb.addr(0x00400123))); + memory.getBytes(recorder.getSnap(), tb.addr(0x00400123), ByteBuffer.wrap(data)); + assertArrayEquals(tb.arr(1, 2, 3, 4, 5, 6, 7, 8, 9, 0), data); + } + + @Test + public void testRecordMemoryError() throws Throwable { + startRecording(); + mb.createTestProcessesAndThreads(); + + mb.testProcess1.memory.addRegion("exe:.text", mb.rng(0x00400000, 0x00400fff), "rx"); + flushAndWait(); + assertEquals(Set.of(), Set.copyOf(memory.getStates(recorder.getSnap(), tb.range(0, -1)))); + + mb.testModel.fire() + .memoryReadError(mb.testProcess1.memory, mb.rng(0x00400123, 0x00400321), + new DebuggerMemoryAccessException("Test error")); + flushAndWait(); + // NB, only the first byte of the reported range is marked. + assertEquals(Set.of(stateEntry(0x00400123, 0x00400123, TraceMemoryState.ERROR)), + Set.copyOf(memory.getStates(recorder.getSnap(), tb.range(0, -1)))); + } + + @Test + public void testRecordMemoryBytes2Processes() throws Throwable { + startRecording(); + mb.createTestProcessesAndThreads(); + Address tgtAddr1 = mb.addr(0x00400123); + Address tgtAddr3 = mb.testModel.ram3.getAddress(0x00400123); + + mb.testProcess1.memory.addRegion("exe:.text", mb.rng(0x00400000, 0x00400fff), "wrx"); + mb.testProcess3.memory.addRegion("exe:.text", new AddressRangeImpl(tgtAddr3, 0x1000), + "rwx"); + + mb.testProcess1.memory.writeMemory(tgtAddr1, mb.arr(1, 2, 3, 4, 5, 6, 7, 8, 9)); + mb.testProcess3.memory.writeMemory(tgtAddr3, mb.arr(11, 12, 13, 14, 15)); + + Address trcAddr1 = recorder.getMemoryMapper().targetToTrace(tgtAddr1); + Address trcAddr3 = recorder.getMemoryMapper().targetToTrace(tgtAddr3); + + byte[] data = new byte[9]; + waitForPass(() -> { + memory.getBytes(recorder.getSnap(), trcAddr1, ByteBuffer.wrap(data)); + assertArrayEquals(tb.arr(1, 2, 3, 4, 5, 6, 7, 8, 9), data); + + memory.getBytes(recorder.getSnap(), trcAddr3, ByteBuffer.wrap(data)); + assertArrayEquals(tb.arr(11, 12, 13, 14, 15, 0, 0, 0, 0), data); + }); + } + + @Test + public void testRecordFocus() throws Throwable { + startRecording(); + mb.createTestProcessesAndThreads(); + + flushAndWait(); + // TODO: Is focus really a concern of the recorder? + assertTrue(recorder.isSupportsFocus()); + assertNull(recorder.getFocus()); + + waitOn(recorder.requestFocus(mb.testThread1)); + flushAndWait(); + assertEquals(mb.testThread1, recorder.getFocus()); + } + + @Test + public void testRecordBreakpoint() throws Throwable { + startRecording(); + mb.createTestProcessesAndThreads(); + flushAndWait(); + + assertEquals(Set.of(TraceBreakpointKind.values()), + Set.copyOf(recorder.getSupportedBreakpointKinds())); + + waitOn(mb.testProcess1.breaks.placeBreakpoint(mb.rng(0x00400123, 0x00400126), + Set.of(TargetBreakpointKind.SW_EXECUTE))); + flushAndWait(); + + TraceBreakpoint loc = Unique.assertOne(breaks.getAllBreakpoints()); + assertEquals(tb.range(0x00400123, 0x00400126), loc.getRange()); + assertEquals(Set.of(TraceBreakpointKind.SW_EXECUTE), loc.getKinds()); + + assertEquals(List.of(), + recorder.collectBreakpointContainers(mb.testThread1)); + assertEquals(List.of(mb.testProcess1.breaks, mb.testProcess3.breaks), + recorder.collectBreakpointContainers(null)); + + TargetBreakpointLocation targetLoc = recorder.getTargetBreakpoint(loc); + assertEquals(loc, recorder.getTraceBreakpoint(targetLoc)); + + // This must *not* show as applicable to thread1 + waitOn(mb.testProcess3.breaks.placeBreakpoint(mb.testModel.ram3.getAddress(0x00400321), + Set.of(TargetBreakpointKind.SW_EXECUTE))); + flushAndWait(); + assertEquals(2, breaks.getAllBreakpoints().size()); + assertEquals(List.of(targetLoc), recorder.collectBreakpoints(mb.testThread1)); + } + + @Test + public void testRecordModuleAndSection() throws Throwable { + startRecording(); + mb.createTestProcessesAndThreads(); + + TestTargetModule targetExe = + mb.testProcess1.modules.addModule("exe", mb.rng(0x00400000, 0x00602fff)); + TestTargetSection targetText = + targetExe.addSection(".text", mb.rng(0x00400000, 0x00400fff)); + flushAndWait(); + + TraceModule exe = Unique.assertOne(modules.getAllModules()); + assertEquals(exe, recorder.getTraceModule(targetExe)); + assertEquals(targetExe, recorder.getTargetModule(exe)); + + assertEquals("exe", exe.getName()); + assertEquals("Processes[1].Modules[exe]", exe.getPath()); + assertEquals(tb.range(0x00400000, 0x00602fff), exe.getRange()); + + TraceSection text = Unique.assertOne(modules.getAllSections()); + assertEquals(text, recorder.getTraceSection(targetText)); + assertEquals(targetText, recorder.getTargetSection(text)); + + assertEquals("[.text]", text.getName()); + assertEquals("Processes[1].Modules[exe].Sections[.text]", text.getPath()); + assertEquals(tb.range(0x00400000, 0x00400fff), text.getRange()); + + assertEquals(exe, text.getModule()); + assertEquals(Set.of(text), Set.copyOf(exe.getSections())); + } + + @Test + public void testRecordRegisters() throws Throwable { + startRecording(); + mb.createTestProcessesAndThreads(); + // TODO: Adjust schema to reflect merging of container and bank + // TODO: Other bank placements. Will need different schemas, though :/ + mb.createTestThreadRegisterBanks(); + + TestTargetRegisterValue targetPC = new TestTargetRegisterValue(mb.testBank1, "pc", true, + BigInteger.valueOf(0x00400123), 8); + TestTargetRegisterValue targetR0 = + new TestTargetRegisterValue(mb.testBank1, "r0", false, BigInteger.ZERO, 8); + mb.testBank1.setElements(Set.of(targetPC, targetR0), "Test registers"); + flushAndWait(); + + TraceObjectThread thread = (TraceObjectThread) recorder.getTraceThread(mb.testThread1); + assertNotNull(thread); + + assertEquals(mb.testBank1, recorder.getTargetRegisterBank(thread, 0)); + assertEquals(thread, recorder.getTraceThreadForSuccessor(mb.testBank1)); + + TraceObject traceBank = thread.getObject() + .querySuccessorsTargetInterface(Range.singleton(recorder.getSnap()), + TargetRegisterBank.class) + .map(p -> p.getLastChild(thread.getObject())) + .findAny() + .orElseThrow(); + + TraceObject pc = traceBank.getElement(recorder.getSnap(), "pc").getChild(); + assertArrayEquals(tb.arr(0, 0, 0, 0, 0, 0x40, 0x01, 0x023), + (byte[]) pc.getAttribute(recorder.getSnap(), TargetObject.VALUE_ATTRIBUTE_NAME) + .getValue()); + TraceObject r0 = traceBank.getElement(recorder.getSnap(), "r0").getChild(); + assertArrayEquals(tb.arr(0, 0, 0, 0, 0, 0, 0, 0), + (byte[]) r0.getAttribute(recorder.getSnap(), TargetObject.VALUE_ATTRIBUTE_NAME) + .getValue()); + // TODO: Test interpretation, once mapping scheme is worked out + // TODO: How to annotate values with types, etc? + // TODO: Perhaps byte-array values are allocated in memory-like byte store? + // TODO: Brings endianness into the picture :/ + } + + @Test + public void testRecordStack() throws Throwable { + startRecording(); + mb.createTestProcessesAndThreads(); + + TestTargetStack targetStack = mb.testThread1.addStack(); + // TODO: These "push" semantics don't seem to work as designed.... + TestTargetStackFrame targetFrame0 = targetStack.pushFrameNoBank(mb.addr(0x00400123)); + TestTargetStackFrame targetFrame1 = targetStack.pushFrameNoBank(mb.addr(0x00400321)); + flushAndWait(); + + TraceObjectThread thread = (TraceObjectThread) recorder.getTraceThread(mb.testThread1); + assertNotNull(thread); + + assertEquals(targetFrame0, recorder.getTargetStackFrame(thread, 0)); + assertEquals(targetFrame1, recorder.getTargetStackFrame(thread, 1)); + assertEquals(thread, recorder.getTraceThreadForSuccessor(targetFrame0)); + assertEquals(thread, recorder.getTraceThreadForSuccessor(targetFrame1)); + + TraceStackFrame frame0 = recorder.getTraceStackFrame(targetFrame0); + TraceStackFrame frame1 = recorder.getTraceStackFrame(targetFrame1); + + // TODO: This can use a bank, once we test the frame-has-bank schema + assertEquals(frame0, recorder.getTraceStackFrameForSuccessor(targetFrame0)); + assertEquals(frame1, recorder.getTraceStackFrameForSuccessor(targetFrame1)); + + assertEquals(0, frame0.getLevel()); + assertEquals(1, frame1.getLevel()); + + assertEquals(tb.addr(0x00400123), frame0.getProgramCounter(Long.MAX_VALUE)); + assertEquals(tb.addr(0x00400321), frame1.getProgramCounter(Long.MAX_VALUE)); + } +} diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/AbstractDebuggerObjectModel.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/AbstractDebuggerObjectModel.java index 7ad4780c55..5a72a305dd 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/AbstractDebuggerObjectModel.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/AbstractDebuggerObjectModel.java @@ -69,7 +69,17 @@ public abstract class AbstractDebuggerObjectModel implements SpiDebuggerObjectMo } protected void objectInvalidated(TargetObject object) { - creationLog.remove(object.getPath()); + synchronized (lock) { + creationLog.remove(object.getPath()); + CompletableFuture.runAsync(() -> { + synchronized (cbLock) { + cbCreationLog.remove(object.getPath()); + } + }, clientExecutor).exceptionally(ex -> { + Msg.error(this, "Error updated objectInvalidated before callback"); + return null; + }); + } } protected void addModelRoot(SpiTargetObject root) { diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetBreakpointSpec.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetBreakpointSpec.java index f4e4fdbe46..f2bc882480 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetBreakpointSpec.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetBreakpointSpec.java @@ -45,6 +45,10 @@ import ghidra.dbg.target.schema.TargetAttributeType; @DebuggerTargetObjectIface("BreakpointSpec") public interface TargetBreakpointSpec extends TargetObject, /*@Transitional*/ TargetTogglable { + /** + * The kinds of breakpoints understood by Ghidra. Note these must match with those in + * {@code TraceBreakpointKind}. + */ public enum TargetBreakpointKind { /** * A read breakpoint, likely implemented in hardware diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetMethod.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetMethod.java index 9d1de33d12..0dd3fc9cb2 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetMethod.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetMethod.java @@ -194,6 +194,12 @@ public interface TargetMethod extends TargetObject { public static TargetParameterMap copyOf(Map> map) { return new ImmutableTargetParameterMap(map); } + + @SafeVarargs + public static TargetParameterMap ofEntries( + Entry>... entries) { + return new ImmutableTargetParameterMap(Map.ofEntries(entries)); + } } /** diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/PathPredicates.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/PathPredicates.java index c03478cde3..f1a5326081 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/PathPredicates.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/PathPredicates.java @@ -17,6 +17,7 @@ package ghidra.dbg.util; import java.util.*; import java.util.concurrent.CompletableFuture; +import java.util.stream.*; import ghidra.async.AsyncFence; import ghidra.dbg.target.TargetObject; @@ -278,24 +279,36 @@ public interface PathPredicates { } /** - * Substitute wildcards from left to right for the given list of indices + * Substitute wildcards from left to right for the given list of keys * *

* Takes each pattern and substitutes its wildcards for the given indices, starting from the * left and working right. This object is unmodified, and the result is returned. * *

- * If there are fewer wildcards in a pattern than given, only the left-most indices are taken. - * If there are fewer indices than wildcards in a pattern, then the right-most wildcards are - * left in the resulting pattern. Note while rare, attribute wildcards are substituted, too. + * If there are fewer wildcards in a pattern than given, only the left-most keys are taken. If + * there are fewer keys than wildcards in a pattern, then the right-most wildcards are left in + * the resulting pattern. * - * @param indices the indices to substitute + * @param keys the keys to substitute * @return the pattern or matcher with the applied substitutions */ - PathPredicates applyKeys(List indices); + PathPredicates applyKeys(List keys); - default PathPredicates applyIndices(String... indices) { - return applyKeys(List.of(indices)); + default PathPredicates applyKeys(Stream keys) { + return applyKeys(keys.collect(Collectors.toList())); + } + + default PathPredicates applyKeys(String... keys) { + return applyKeys(List.of(keys)); + } + + default PathPredicates applyIntKeys(int radix, List keys) { + return applyKeys(keys.stream().map(k -> Integer.toString(k, radix))); + } + + default PathPredicates applyIntKeys(int... keys) { + return applyKeys(IntStream.of(keys).mapToObj(k -> Integer.toString(k))); } /** diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/EmptyDebuggerObjectModel.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/EmptyDebuggerObjectModel.java index 8060b36761..1a294b44e5 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/EmptyDebuggerObjectModel.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/EmptyDebuggerObjectModel.java @@ -20,8 +20,11 @@ import ghidra.dbg.agent.SpiTargetObject; import ghidra.program.model.address.*; public class EmptyDebuggerObjectModel extends AbstractDebuggerObjectModel { - protected final AddressSpace ram = new GenericAddressSpace("ram", 64, AddressSpace.TYPE_RAM, 0); - protected final AddressFactory factory = new DefaultAddressFactory(new AddressSpace[] { ram }); + public final AddressSpace ram = new GenericAddressSpace("ram", 64, AddressSpace.TYPE_RAM, 0); + public final AddressSpace ram3 = + new GenericAddressSpace("ram3", 64, AddressSpace.TYPE_RAM, 0); + protected final AddressFactory factory = + new DefaultAddressFactory(new AddressSpace[] { ram, ram3 }); @Override public AddressFactory getAddressFactory() { diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestDebuggerModelBuilder.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestDebuggerModelBuilder.java index 26df74bb01..340ed0a666 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestDebuggerModelBuilder.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestDebuggerModelBuilder.java @@ -73,7 +73,7 @@ public class TestDebuggerModelBuilder { testThread1 = testProcess1.addThread(1); testThread2 = testProcess1.addThread(2); - testProcess3 = testModel.addProcess(3); + testProcess3 = testModel.addProcess(3, testModel.ram3); testThread3 = testProcess3.addThread(3); testThread4 = testProcess3.addThread(4); } diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestDebuggerObjectModel.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestDebuggerObjectModel.java index b44003cced..8fc5cab2b4 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestDebuggerObjectModel.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestDebuggerObjectModel.java @@ -20,9 +20,11 @@ import java.util.concurrent.*; import org.jdom.JDOMException; +import ghidra.dbg.DebuggerModelListener; import ghidra.dbg.target.TargetObject; import ghidra.dbg.target.schema.TargetObjectSchema; import ghidra.dbg.target.schema.XmlSchemaContext; +import ghidra.program.model.address.AddressSpace; // TODO: Refactor with other Fake and Test model stuff. public class TestDebuggerObjectModel extends EmptyDebuggerObjectModel { @@ -89,7 +91,15 @@ public class TestDebuggerObjectModel extends EmptyDebuggerObjectModel { } public TestTargetProcess addProcess(int pid) { - return session.addProcess(pid); + return addProcess(pid, ram); + } + + public TestTargetProcess addProcess(int pid, AddressSpace space) { + return session.addProcess(pid, space); + } + + public void removeProcess(TestTargetProcess process) { + session.removeProcess(process); } public CompletableFuture future(T t) { @@ -110,4 +120,8 @@ public class TestDebuggerObjectModel extends EmptyDebuggerObjectModel { invalidateCachesCount = 0; return result; } + + public DebuggerModelListener fire() { + return listeners.fire; + } } diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestMimickJavaLauncher.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestMimickJavaLauncher.java index f275646a70..73446a1e1f 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestMimickJavaLauncher.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestMimickJavaLauncher.java @@ -32,16 +32,24 @@ public class TestMimickJavaLauncher public TestMimickJavaLauncher(TestTargetObject parent) { super(parent, "Java Launcher", "Launcher"); - setAttributes(List.of(), Map.of(TargetMethod.PARAMETERS_ATTRIBUTE_NAME, Map.of("Suspend", - ParameterDescription.create(Boolean.class, "Suspend", false, true, "Suspend", ""), - "Quote", ParameterDescription.create(String.class, "Quote", false, "\"", "Quote", ""), - "Launcher", - ParameterDescription.create(String.class, "Launcher", false, "java", "Launcher", ""), - "Options", - ParameterDescription.create(String.class, "Options", false, "", "Options", ""), "Main", - ParameterDescription.create(String.class, "Main", false, "hw.HelloWorld", "Main", ""), - "Home", ParameterDescription.create(String.class, "Home", false, - "/opt/java-11-amazon-corretto", "Home", ""))), + setAttributes( + List.of(), Map.of(TargetMethod.PARAMETERS_ATTRIBUTE_NAME, TargetParameterMap.ofEntries( + Map.entry("Suspend", + ParameterDescription.create(Boolean.class, "Suspend", false, true, "Suspend", + "")), + Map.entry("Quote", + ParameterDescription.create(String.class, "Quote", false, "\"", "Quote", "")), + Map.entry("Launcher", + ParameterDescription.create(String.class, "Launcher", false, "java", "Launcher", + "")), + Map.entry("Options", + ParameterDescription.create(String.class, "Options", false, "", "Options", "")), + Map.entry("Main", + ParameterDescription.create(String.class, "Main", false, "hw.HelloWorld", + "Main", "")), + Map.entry("Home", + ParameterDescription.create(String.class, "Home", false, + "/opt/java-11-amazon-corretto", "Home", "")))), "Initialized"); } @@ -52,6 +60,6 @@ public class TestMimickJavaLauncher @Override public CompletableFuture launch(Map args) { - return AsyncUtils.NIL; // TODO: Queue and allow to test complete it? + return AsyncUtils.NIL; // TODO: Queue and allow test to complete it? } } diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetBreakpointContainer.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetBreakpointContainer.java index 486172e583..f7f01f60cf 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetBreakpointContainer.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetBreakpointContainer.java @@ -25,8 +25,8 @@ import ghidra.dbg.target.TargetBreakpointSpecContainer; import ghidra.program.model.address.AddressRange; // TODO: Test some other breakpoint conventions: -// A1) 1-1 spec-effective, where spec is effective is breakpoint (DONE) -// A2) 1-n spec-effective, where effective are children of spec +// A1) 1-1 spec-loc, where spec is loc is breakpoint (DONE) +// A2) 1-n spec-loc, where locs are children of spec // B1) container per process (DONE) // B2) container per session diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetMemory.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetMemory.java index 8105d9de87..0febf2201e 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetMemory.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetMemory.java @@ -24,24 +24,25 @@ import java.util.concurrent.CompletableFuture; import ghidra.dbg.target.TargetAccessConditioned; import ghidra.dbg.target.TargetMemory; import ghidra.generic.util.datastruct.SemisparseByteArray; -import ghidra.program.model.address.Address; -import ghidra.program.model.address.AddressRange; +import ghidra.program.model.address.*; public class TestTargetMemory extends DefaultTestTargetObject implements TargetMemory, TargetAccessConditioned { protected final SemisparseByteArray memory = new SemisparseByteArray(); + protected final AddressSpace space; - public TestTargetMemory(TestTargetProcess parent) { + public TestTargetMemory(TestTargetProcess parent, AddressSpace space) { super(parent, "Memory", "Memory"); + this.space = space; changeAttributes(List.of(), Map.of( ACCESSIBLE_ATTRIBUTE_NAME, true // ), "Initialized"); } public void getMemory(Address address, byte[] data) { - assertEquals(getModel().ram, address.getAddressSpace()); + assertEquals(space, address.getAddressSpace()); memory.getData(address.getOffset(), data); } @@ -57,7 +58,7 @@ public class TestTargetMemory } public void setMemory(Address address, byte[] data) { - assertEquals(getModel().ram, address.getAddressSpace()); + assertEquals(space, address.getAddressSpace()); memory.putData(address.getOffset(), data); } @@ -77,6 +78,11 @@ public class TestTargetMemory return region; } + public void removeRegion(TestTargetMemoryRegion region) { + changeElements(List.of(region.getIndex()), List.of(), + "Remove test region: " + region.getRange()); + } + public boolean setAccessible(boolean accessible) { boolean old = isAccessible(); changeAttributes(List.of(), Map.ofEntries( diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetProcess.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetProcess.java index 029243bdbb..953375b9f4 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetProcess.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetProcess.java @@ -22,6 +22,7 @@ import ghidra.dbg.target.TargetAggregate; import ghidra.dbg.target.TargetProcess; import ghidra.dbg.util.PathUtils; import ghidra.program.model.address.AddressRange; +import ghidra.program.model.address.AddressSpace; public class TestTargetProcess extends DefaultTestTargetObject @@ -32,10 +33,10 @@ public class TestTargetProcess extends public final TestTargetRegisterContainer regs; public final TestTargetThreadContainer threads; - public TestTargetProcess(DefaultTestTargetObject parent, int pid) { + public TestTargetProcess(DefaultTestTargetObject parent, int pid, AddressSpace space) { super(parent, PathUtils.makeKey(PathUtils.makeIndex(pid)), "Process"); breaks = new TestTargetBreakpointContainer(this); - memory = new TestTargetMemory(this); + memory = new TestTargetMemory(this, space); modules = new TestTargetModuleContainer(this); regs = new TestTargetRegisterContainer(this); threads = new TestTargetThreadContainer(this); diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetProcessContainer.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetProcessContainer.java index 4bd4872a64..2ef0ed1ec0 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetProcessContainer.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetProcessContainer.java @@ -18,15 +18,22 @@ package ghidra.dbg.model; import java.util.List; import java.util.Map; +import ghidra.program.model.address.AddressSpace; + public class TestTargetProcessContainer extends DefaultTestTargetObject { public TestTargetProcessContainer(TestTargetSession parent) { super(parent, "Processes", "Processes"); } - public TestTargetProcess addProcess(int pid) { - TestTargetProcess proc = new TestTargetProcess(this, pid); + public TestTargetProcess addProcess(int pid, AddressSpace space) { + TestTargetProcess proc = new TestTargetProcess(this, pid, space); changeElements(List.of(), List.of(proc), Map.of(), "Test Process Added"); return proc; } + + public void removeProcess(TestTargetProcess process) { + changeElements(List.of(process.getIndex()), List.of(), + "Test Process Removed: " + process.getPid()); + } } diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetRegisterValue.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetRegisterValue.java new file mode 100644 index 0000000000..dd2b1d03a9 --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetRegisterValue.java @@ -0,0 +1,65 @@ +/* ### + * 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.dbg.model; + +import java.math.BigInteger; +import java.util.List; +import java.util.Map; + +import ghidra.dbg.target.TargetRegister; +import ghidra.dbg.util.PathUtils; +import ghidra.pcode.utils.Utils; +import ghidra.program.model.lang.Register; +import ghidra.program.model.lang.RegisterValue; + +public class TestTargetRegisterValue + extends DefaultTestTargetObject> + implements TargetRegister { + + public static TestTargetRegisterValue fromRegisterValue( + AbstractTestTargetRegisterBank parent, RegisterValue rv) { + Register register = rv.getRegister(); + return new TestTargetRegisterValue(parent, PathUtils.makeKey(register.getName()), + register.isProgramCounter(), rv.getUnsignedValue(), register.getBitLength() + 7 / 8); + } + + protected final int byteLength; + protected final boolean isPC; + + public TestTargetRegisterValue(AbstractTestTargetRegisterBank parent, String name, + boolean isPC, BigInteger value, int byteLength) { + this(parent, name, isPC, Utils.bigIntegerToBytes(value, byteLength, true)); + } + + public TestTargetRegisterValue(AbstractTestTargetRegisterBank parent, String name, + boolean isPC, byte[] value) { + super(parent, name, "Register"); + this.byteLength = value.length; + this.isPC = isPC; + + changeAttributes(List.of(), Map.of( + CONTAINER_ATTRIBUTE_NAME, parent, + LENGTH_ATTRIBUTE_NAME, byteLength, + VALUE_ATTRIBUTE_NAME, value // + ), "Initialized"); + } + + public void setValue(BigInteger value) { + changeAttributes(List.of(), Map.of( + VALUE_ATTRIBUTE_NAME, Utils.bigIntegerToBytes(value, byteLength, true) // + ), "Set value"); + } +} diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetSectionContainer.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetSectionContainer.java index d1ab9af030..df6c2c04eb 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetSectionContainer.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetSectionContainer.java @@ -17,10 +17,12 @@ package ghidra.dbg.model; import java.util.List; +import ghidra.dbg.target.TargetSectionContainer; import ghidra.program.model.address.AddressRange; public class TestTargetSectionContainer - extends DefaultTestTargetObject { + extends DefaultTestTargetObject + implements TargetSectionContainer { public TestTargetSectionContainer(TestTargetModule parent) { super(parent, "Sections", "SectionContainer"); diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetSession.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetSession.java index 879cdab4d1..cfb72ab3ed 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetSession.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetSession.java @@ -25,6 +25,7 @@ import ghidra.dbg.target.*; import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; import ghidra.dbg.target.schema.EnumerableTargetObjectSchema; import ghidra.dbg.target.schema.TargetObjectSchema; +import ghidra.program.model.address.AddressSpace; public class TestTargetSession extends DefaultTargetModelRoot implements TestTargetObject, TargetFocusScope, TargetEventScope, TargetLauncher { @@ -51,8 +52,12 @@ public class TestTargetSession extends DefaultTargetModelRoot "Initialized"); } - public TestTargetProcess addProcess(int pid) { - return processes.addProcess(pid); + public TestTargetProcess addProcess(int pid, AddressSpace space) { + return processes.addProcess(pid, space); + } + + public void removeProcess(TestTargetProcess process) { + processes.removeProcess(process); } @Override diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetStack.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetStack.java index c7d6dc9abd..94b36e26fc 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetStack.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetStack.java @@ -38,6 +38,10 @@ public class TestTargetStack extends DefaultTestTargetObject implements TestTargetStackFrame { + protected Address pc; + + public TestTargetStackFrameNoRegisterBank(TestTargetStack parent, int level, Address pc) { + super(parent, PathUtils.makeKey(PathUtils.makeIndex(level)), "Frame"); + + changeAttributes(List.of(), Map.of( + PC_ATTRIBUTE_NAME, this.pc = pc // + ), "Initialized"); + } + + @Override + public void setFromFrame(TestTargetStackFrame frame) { + TestTargetStackFrameNoRegisterBank that = (TestTargetStackFrameNoRegisterBank) frame; + this.pc = that.pc; + changeAttributes(List.of(), Map.of( + PC_ATTRIBUTE_NAME, this.pc // + ), "Copied frame"); + } + + public void setPC(Address pc) { + this.pc = pc; + changeAttributes(List.of(), Map.of( + PC_ATTRIBUTE_NAME, this.pc // + ), "PC Updated"); + } +} diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetThreadContainer.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetThreadContainer.java index faab247349..c450f572ba 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetThreadContainer.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetThreadContainer.java @@ -34,7 +34,7 @@ public class TestTargetThreadContainer return thread; } - public void removeThreads(TestTargetThread[] threads) { + public void removeThreads(TestTargetThread... threads) { List indices = Stream.of(threads).map(TargetObject::getIndex).collect(Collectors.toList()); changeElements(indices, List.of(), Map.of(), "Test Threads Removed"); diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractModelHost.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractModelHost.java index 2408b94c69..1bd440078b 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractModelHost.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractModelHost.java @@ -211,7 +211,7 @@ public abstract class AbstractModelHost implements ModelHost, DebuggerModelTestU PathPredicates matcher = model.getRootSchema() .getSuccessorSchema(seedPath) .searchFor(cls, seedPath, true) - .applyIndices(index); + .applyKeys(index); if (matcher.isEmpty()) { return null; } diff --git a/Ghidra/Debug/Framework-Debugging/src/test/resources/ghidra/dbg/model/test_schema.xml b/Ghidra/Debug/Framework-Debugging/src/test/resources/ghidra/dbg/model/test_schema.xml index 33411e24be..eef0f4617f 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/resources/ghidra/dbg/model/test_schema.xml +++ b/Ghidra/Debug/Framework-Debugging/src/test/resources/ghidra/dbg/model/test_schema.xml @@ -10,17 +10,16 @@

+ * Between the {@link #CREATED} and {@link #INSERTED} events, an object is considered + * "incomplete," because it is likely missing its attributes. Thus, a trace client must take + * care to ensure all attributes, especially fixed attributes, are added to the object + * before it is inserted at its canonical path. Listeners may use + * {@link TraceObject#getCanonicalParent(long)} to check if an object is complete for a + * given snapshot. */ public static final TraceObjectChangeType CREATED = new TraceObjectChangeType<>(); + /** + * An object was inserted at its canonical path. + */ + public static final TraceObjectChangeType INSERTED = + new TraceObjectChangeType<>(); /** * An object's lifespan changed. */ @@ -75,9 +88,9 @@ public interface Trace extends DataTypeManagerDomainObject { * An object's value changed. * *

- * If the old value is uniform for the new value's lifespan, that value is passed as the old - * value. Otherwise, {@code null} is passed for the old value. If the value was cleared, - * {@code null} is passed for the new value. + * If the old value was equal for the entirety of the new value's lifespan, that old value + * is passed as the old value. Otherwise, {@code null} is passed for the old value. If the + * value was cleared, {@code null} is passed for the new value. */ public static final TraceObjectChangeType VALUE_CHANGED = new TraceObjectChangeType<>(); @@ -307,7 +320,12 @@ public interface Trace extends DataTypeManagerDomainObject { public static final class TraceStackChangeType extends DefaultTraceChangeType { public static final TraceStackChangeType ADDED = new TraceStackChangeType<>(); - public static final TraceStackChangeType CHANGED = new TraceStackChangeType<>(); + /** + * NOTE: The "new value" is the (min) snap where the change occurred. For StackFrame, it's + * Stack.getSnap(), for ObjectStackFrame, the min snap of the value entry. The "old value" + * is always 0. + */ + public static final TraceStackChangeType CHANGED = new TraceStackChangeType<>(); public static final TraceStackChangeType DELETED = new TraceStackChangeType<>(); } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/stack/TraceStackFrame.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/stack/TraceStackFrame.java index dd6082ba14..59cdaae658 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/stack/TraceStackFrame.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/stack/TraceStackFrame.java @@ -15,6 +15,8 @@ */ package ghidra.trace.model.stack; +import com.google.common.collect.Range; + import ghidra.program.model.address.Address; public interface TraceStackFrame { @@ -22,9 +24,9 @@ public interface TraceStackFrame { int getLevel(); - Address getProgramCounter(); + Address getProgramCounter(long snap); - void setProgramCounter(Address pc); + void setProgramCounter(Range span, Address pc); String getComment(); diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/target/TraceObject.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/target/TraceObject.java index bfc75fb49a..64f404a2e4 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/target/TraceObject.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/target/TraceObject.java @@ -72,6 +72,20 @@ public interface TraceObject extends TraceUniqueObject { */ void insert(ConflictResolution resolution); + /** + * Get the parent value along this object's canonical path for a given snapshot + * + *

+ * To be the canonical parent value at a given snapshot, three things must be true: 1) The + * parent object must have this object's path with the last key removed. 2) The parent value's + * entry key must be equal to the final key of this object's path. 3) The value's lifespan must + * contain the given snapshot. If no value satisfies these, null is returned. + * + * @param snap the snapshot key + * @return the canonical parent value, or null + */ + TraceObjectValue getCanonicalParent(long snap); + /** * Check if this object is the root * @@ -309,10 +323,15 @@ public interface TraceObject extends TraceUniqueObject { /** * Set a value for the given lifespan, truncating existing entries * + *

+ * Setting a value of {@code null} effectively deletes the value for the given lifespan and + * returns {@code null}. Values of the same key intersecting the given lifespan or either + * truncated or deleted. + * * @param lifespan the lifespan of the value * @param key the key to set * @param value the new value - * @return the created value entry + * @return the created value entry, or null */ TraceObjectValue setValue(Range lifespan, String key, Object value); @@ -361,6 +380,16 @@ public interface TraceObject extends TraceUniqueObject { */ TargetObjectSchema getTargetSchema(); + /** + * Search for ancestors having the given target interface + * + * @param span the span which the found objects must intersect + * @param targetIf the interface class + * @return the stream of found paths to values + */ + Stream queryAncestorsTargetInterface(Range span, + Class targetIf); + /** * Search for ancestors providing the given interface and retrieve those interfaces * @@ -372,6 +401,19 @@ public interface TraceObject extends TraceUniqueObject { Stream queryAncestorsInterface(Range span, Class ifClass); + /** + * Search for ancestors on the canonical path having the given target interface + * + *

+ * The object may not yet be inserted at its canonical path + * + * @param span the span which the found objects must intersect + * @param targetIf the interface class + * @return the stream of objects + */ + Stream queryCanonicalAncestorsTargetInterface(Range span, + Class targetIf); + /** * Search for ancestors on the canonical path providing the given interface * @@ -386,6 +428,9 @@ public interface TraceObject extends TraceUniqueObject { Stream queryCanonicalAncestorsInterface( Range span, Class ifClass); + Stream querySuccessorsTargetInterface(Range span, + Class targetIf); + /** * Search for successors providing the given interface and retrieve those interfaces * 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 index f4e2273626..4dec7b42e3 100644 --- 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 @@ -28,7 +28,8 @@ 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.trace.model.time.TraceSnapshot; +import ghidra.trace.model.time.TraceTimeManager; import ghidra.trace.model.time.schedule.TraceSchedule; import ghidra.util.*; import ghidra.util.datastruct.ListenerSet; 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 index c306412aa6..14bebcfc91 100644 --- 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 @@ -143,7 +143,7 @@ public class TracePcodeEmulatorTest extends AbstractGhidraHeadlessIntegrationTes tb.trace.getStackManager() .getStack(thread, 1, false) .getFrame(0, false) - .getProgramCounter()); + .getProgramCounter(1)); } } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/database/stack/DBTraceStackManagerTest.java b/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/database/stack/DBTraceStackManagerTest.java index 0181ac1442..888b6cdf33 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/database/stack/DBTraceStackManagerTest.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/database/stack/DBTraceStackManagerTest.java @@ -24,6 +24,8 @@ import java.util.stream.StreamSupport; import org.junit.*; +import com.google.common.collect.Range; + import ghidra.test.AbstractGhidraHeadlessIntegrationTest; import ghidra.trace.database.ToyDBTraceBuilder; import ghidra.trace.model.stack.TraceStack; @@ -137,13 +139,13 @@ public class DBTraceStackManagerTest extends AbstractGhidraHeadlessIntegrationTe TraceStack stack1 = stackManager.getStack(thread, 0, true); stack1.setDepth(2, true); - (frame1a = stack1.getFrame(0, false)).setProgramCounter(b.addr(0x0040100)); - (frame1b = stack1.getFrame(1, false)).setProgramCounter(b.addr(0x0040300)); + (frame1a = stack1.getFrame(0, false)).setProgramCounter(Range.all(), b.addr(0x0040100)); + (frame1b = stack1.getFrame(1, false)).setProgramCounter(Range.all(), b.addr(0x0040300)); TraceStack stack2 = stackManager.getStack(thread, 1, true); stack2.setDepth(2, true); - (frame2a = stack2.getFrame(0, false)).setProgramCounter(b.addr(0x0040200)); - (frame2b = stack2.getFrame(1, false)).setProgramCounter(b.addr(0x0040400)); + (frame2a = stack2.getFrame(0, false)).setProgramCounter(Range.all(), b.addr(0x0040200)); + (frame2b = stack2.getFrame(1, false)).setProgramCounter(Range.all(), b.addr(0x0040400)); } assertEquals(Set.of(frame1a, frame2a, frame1b, frame2b), toSet(stackManager @@ -271,11 +273,11 @@ public class DBTraceStackManagerTest extends AbstractGhidraHeadlessIntegrationTe stack.setDepth(1, true); frame = stack.getFrame(0, false); - assertNull(frame.getProgramCounter()); - frame.setProgramCounter(b.addr(0x00400123)); + assertNull(frame.getProgramCounter(Long.MAX_VALUE)); + frame.setProgramCounter(Range.all(), b.addr(0x00400123)); } - assertEquals(b.addr(0x00400123), frame.getProgramCounter()); + assertEquals(b.addr(0x00400123), frame.getProgramCounter(0)); } @Test @@ -289,7 +291,7 @@ public class DBTraceStackManagerTest extends AbstractGhidraHeadlessIntegrationTe stack.setDepth(1, true); frame = stack.getFrame(0, false); // NB. Object-mode sets comment at pc in listing, not on frame itself - frame.setProgramCounter(b.addr(0x00400123)); + frame.setProgramCounter(Range.all(), b.addr(0x00400123)); assertNull(frame.getComment()); frame.setComment("Hello, World!"); diff --git a/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/database/target/DBTraceObjectManagerTest.java b/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/database/target/DBTraceObjectManagerTest.java index 6ddc63c266..68c30cac59 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/database/target/DBTraceObjectManagerTest.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/database/target/DBTraceObjectManagerTest.java @@ -507,6 +507,8 @@ public class DBTraceObjectManagerTest extends AbstractGhidraHeadlessIntegrationT public void testGetSuccessors() { populateModel(3); + assertEquals(1, root.getSuccessors(Range.all(), PathPredicates.parse("")).count()); + assertEquals(1, root.getSuccessors(Range.all(), PathPredicates.parse("Targets")).count()); assertEquals(1, @@ -531,6 +533,29 @@ public class DBTraceObjectManagerTest extends AbstractGhidraHeadlessIntegrationT root.getSuccessors(Range.all(), PathPredicates.parse("anAttribute.nope")).count()); } + @Test + public void testGetOrderedSuccessors() { + populateModel(3); + + assertEquals(List.of(root), + root.getOrderedSuccessors(Range.all(), TraceObjectKeyPath.parse(""), true) + .map(p -> p.getLastChild(root)) + .collect(Collectors.toList())); + assertEquals(List.of(root), + root.getOrderedSuccessors(Range.all(), TraceObjectKeyPath.parse(""), false) + .map(p -> p.getLastChild(root)) + .collect(Collectors.toList())); + + assertEquals(List.of(targets.get(0), targets.get(1), targets.get(2)), + root.getOrderedSuccessors(Range.all(), TraceObjectKeyPath.parse("curTarget"), true) + .map(p -> p.getLastChild(root)) + .collect(Collectors.toList())); + assertEquals(List.of(targets.get(2), targets.get(1), targets.get(0)), + root.getOrderedSuccessors(Range.all(), TraceObjectKeyPath.parse("curTarget"), false) + .map(p -> p.getLastChild(root)) + .collect(Collectors.toList())); + } + @Test public void testSetValue_TruncatesOrDeletes() { try (UndoableTransaction tid = b.startTransaction()) { @@ -794,6 +819,38 @@ public class DBTraceObjectManagerTest extends AbstractGhidraHeadlessIntegrationT } } + @Test + public void testSetValue_NullContainedTruncates() { + try (UndoableTransaction tid = b.startTransaction()) { + root = manager.createRootObject(ctx.getSchema(new SchemaName("Session"))).getChild(); + assertNull(root.setValue(Range.closed(0L, 9L), "a", null)); + assertEquals(0, root.getValues().size()); + + assertNotNull(root.setValue(Range.closed(0L, 9L), "a", 1)); + assertEquals(1, root.getValues().size()); + + assertNull(root.setValue(Range.singleton(5L), "a", null)); + assertEquals(2, root.getValues().size()); + + assertEquals(List.of(Range.closed(0L, 4L), Range.closed(6L, 9L)), + root.getOrderedValues(Range.all(), "a", true) + .map(v -> v.getLifespan()) + .collect(Collectors.toList())); + } + } + + @Test + public void testSetValue_NullSameDeletes() { + try (UndoableTransaction tid = b.startTransaction()) { + root = manager.createRootObject(ctx.getSchema(new SchemaName("Session"))).getChild(); + assertNotNull(root.setValue(Range.closed(0L, 9L), "a", 1)); + assertEquals(1, root.getValues().size()); + + assertNull(root.setValue(Range.closed(0L, 9L), "a", null)); + assertEquals(0, root.getValues().size()); + } + } + @Test public void testSetAttribute() { try (UndoableTransaction tid = b.startTransaction()) { @@ -815,7 +872,7 @@ public class DBTraceObjectManagerTest extends AbstractGhidraHeadlessIntegrationT } @Test - public void testObjectDelete() { + public void testObjectDelete() throws Exception { populateModel(3); // Delete a leaf @@ -844,6 +901,12 @@ public class DBTraceObjectManagerTest extends AbstractGhidraHeadlessIntegrationT assertEquals(2, manager.getObjectsByPath(Range.all(), TraceObjectKeyPath.parse("Targets[]")).count()); assertTrue(t0.getParents().stream().anyMatch(v -> v.getParent() == targetContainer)); + assertEquals(2, targetContainer.getValues().size()); + + b.trace.undo(); + b.trace.redo(); + + assertEquals(2, targetContainer.getValues().size()); try (UndoableTransaction tid = b.startTransaction()) { targetContainer.delete(); @@ -876,7 +939,7 @@ public class DBTraceObjectManagerTest extends AbstractGhidraHeadlessIntegrationT } @Test - public void testObjectTruncateOrDelete() { + public void testRootObjectTruncateOrDelete() { try (UndoableTransaction tid = b.startTransaction()) { root = manager.createRootObject(ctx.getSchema(new SchemaName("Session"))).getChild(); diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/database/DBCachedObjectStoreFactory.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/database/DBCachedObjectStoreFactory.java index dab9b4f9b2..44fa8c77f3 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/database/DBCachedObjectStoreFactory.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/database/DBCachedObjectStoreFactory.java @@ -597,15 +597,21 @@ public class DBCachedObjectStoreFactory { .stream() .collect(Collectors.toMap(c -> c.getValueClass(), c -> c)); - @SuppressWarnings("unchecked") static PrimitiveCodec getCodec(Class cls) { - return (PrimitiveCodec) Objects.requireNonNull(CODECS_BY_CLASS.get(cls), - "No variant codec for class " + cls); + @SuppressWarnings("unchecked") + PrimitiveCodec obj = (PrimitiveCodec) CODECS_BY_CLASS.get(cls); + if (obj == null) { + throw new IllegalArgumentException("No variant codec for class " + cls); + } + return obj; } static PrimitiveCodec getCodec(byte sel) { - return Objects.requireNonNull(CODECS_BY_SELECTOR.get(sel), - "No variant codec with selector " + sel); + PrimitiveCodec obj = CODECS_BY_SELECTOR.get(sel); + if (obj == null) { + throw new IllegalArgumentException("No variant codec with selector " + sel); + } + return obj; } } diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/utilities/util/IDHashed.java b/Ghidra/Debug/ProposedUtils/src/main/java/utilities/util/IDHashed.java index 3b500d98ba..6deca47345 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/utilities/util/IDHashed.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/utilities/util/IDHashed.java @@ -17,9 +17,11 @@ package utilities.util; public class IDHashed { public final T obj; + public final int hashCode; public IDHashed(T obj) { this.obj = obj; + this.hashCode = System.identityHashCode(obj); } @Override @@ -29,7 +31,7 @@ public class IDHashed { @Override public int hashCode() { - return System.identityHashCode(obj); + return hashCode; } @Override diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/utilities/util/IDKeyed.java b/Ghidra/Debug/ProposedUtils/src/main/java/utilities/util/IDKeyed.java new file mode 100644 index 0000000000..f1b4f7ba2b --- /dev/null +++ b/Ghidra/Debug/ProposedUtils/src/main/java/utilities/util/IDKeyed.java @@ -0,0 +1,31 @@ +/* ### + * 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 utilities.util; + +public class IDKeyed extends IDHashed { + public IDKeyed(T obj) { + super(obj); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof IDHashed)) { + return false; + } + IDHashed that = (IDHashed) o; + return this.obj == that.obj; + } +}