diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/BytesTracePcodeExecutorStatePiece.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/BytesTracePcodeExecutorStatePiece.java index 1d1710bd72..c4e08694c5 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/BytesTracePcodeExecutorStatePiece.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/BytesTracePcodeExecutorStatePiece.java @@ -76,6 +76,10 @@ public class BytesTracePcodeExecutorStatePiece } } + protected AddressSetView intersectViewKnown(AddressSetView set) { + return backing.intersectViewKnown(set, true); + } + @Override protected ULongSpanSet readUninitializedFromBacking(ULongSpanSet uninitialized) { if (uninitialized.isEmpty()) { @@ -84,7 +88,7 @@ public class BytesTracePcodeExecutorStatePiece // TODO: Warn or bail when reading UNKNOWN bytes // NOTE: Read without regard to gaps // NOTE: Cannot write those gaps, though!!! - AddressSetView knownButUninit = backing.intersectViewKnown(addrSet(uninitialized)); + AddressSetView knownButUninit = intersectViewKnown(addrSet(uninitialized)); if (knownButUninit.isEmpty()) { return uninitialized; } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/RequireHasKnownTraceCachedWriteBytesPcodeExecutorStatePiece.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/RequireHasKnownTraceCachedWriteBytesPcodeExecutorStatePiece.java index 990ebbb2f2..46e7ee3474 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/RequireHasKnownTraceCachedWriteBytesPcodeExecutorStatePiece.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/RequireHasKnownTraceCachedWriteBytesPcodeExecutorStatePiece.java @@ -52,8 +52,8 @@ public class RequireHasKnownTraceCachedWriteBytesPcodeExecutorStatePiece } @Override - protected AddressSetView getKnown(PcodeTraceDataAccess backing) { - return backing.getKnownBefore(); + protected AddressSetView getKnown(PcodeTraceDataAccess backing, AddressSetView set) { + return backing.intersectViewKnown(set, true); } @Override diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/RequireIsKnownTraceCachedWriteBytesPcodeExecutorStatePiece.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/RequireIsKnownTraceCachedWriteBytesPcodeExecutorStatePiece.java index e487911a18..e33df8cc8b 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/RequireIsKnownTraceCachedWriteBytesPcodeExecutorStatePiece.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/RequireIsKnownTraceCachedWriteBytesPcodeExecutorStatePiece.java @@ -50,8 +50,8 @@ public class RequireIsKnownTraceCachedWriteBytesPcodeExecutorStatePiece spaceMap.fork()); } - protected AddressSetView getKnown(PcodeTraceDataAccess backing) { - return backing.getKnownNow(); + protected AddressSetView getKnown(PcodeTraceDataAccess backing, AddressSetView set) { + return backing.intersectViewKnown(set, false); } protected AccessPcodeExecutionException excFor(AddressSetView unknown) { @@ -68,7 +68,7 @@ public class RequireIsKnownTraceCachedWriteBytesPcodeExecutorStatePiece throw excFor(uninitialized); } // TODO: Could find first instead? - AddressSetView unknown = uninitialized.subtract(getKnown(backing)); + AddressSetView unknown = uninitialized.subtract(getKnown(backing, uninitialized)); if (unknown.isEmpty()) { return size; } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/data/AbstractPcodeTraceDataAccess.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/data/AbstractPcodeTraceDataAccess.java index ff0c8a9452..532ea340e5 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/data/AbstractPcodeTraceDataAccess.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/data/AbstractPcodeTraceDataAccess.java @@ -23,6 +23,7 @@ import ghidra.trace.model.Lifespan; import ghidra.trace.model.TraceTimeViewport; import ghidra.trace.model.guest.TracePlatform; import ghidra.trace.model.memory.*; +import ghidra.trace.util.TraceRegisterUtils; /** * An abstract data-access shim, for either memory or registers @@ -130,27 +131,8 @@ public abstract class AbstractPcodeTraceDataAccess implements InternalPcodeTrace return hostSet.isEmpty() ? TraceMemoryState.KNOWN : TraceMemoryState.UNKNOWN; } - protected AddressSetView doGetKnown(Lifespan span) { - TraceMemoryOperations ops = getMemoryOps(false); - if (ops == null) { - return new AddressSet(); - } - return platform.mapHostToGuest(ops.getAddressesWithState(span, - s -> s == TraceMemoryState.KNOWN)); - } - @Override - public AddressSetView getKnownNow() { - return doGetKnown(Lifespan.at(snap)); - } - - @Override - public AddressSetView getKnownBefore() { - return doGetKnown(Lifespan.since(snap)); - } - - @Override - public AddressSetView intersectViewKnown(AddressSetView guestView) { + public AddressSetView intersectViewKnown(AddressSetView guestView, boolean useFullSpans) { TraceMemoryOperations ops = getMemoryOps(false); if (ops == null) { return new AddressSet(); @@ -158,11 +140,20 @@ public abstract class AbstractPcodeTraceDataAccess implements InternalPcodeTrace AddressSetView hostView = toOverlay(platform.mapGuestToHost(guestView)); AddressSet hostKnown = new AddressSet(); - for (long sn : viewport.getOrderedSnaps()) { - hostKnown.add(ops.getAddressesWithState(sn, hostView, - st -> st != null && st != TraceMemoryState.UNKNOWN)); + if (useFullSpans) { + for (Lifespan span : viewport.getOrderedSpans()) { + hostKnown.add(ops.getAddressesWithState(span, hostView, + st -> st != null && st != TraceMemoryState.UNKNOWN)); + } } - AddressSetView hostResult = hostView.intersect(hostKnown); + else { + for (long snap : viewport.getOrderedSnaps()) { + hostKnown.add(ops.getAddressesWithState(snap, hostView, + st -> st != null && st != TraceMemoryState.UNKNOWN)); + } + } + AddressSetView hostResult = + TraceRegisterUtils.getPhysicalSet(hostView.intersect(hostKnown)); return platform.mapHostToGuest(hostResult); } @@ -176,7 +167,7 @@ public abstract class AbstractPcodeTraceDataAccess implements InternalPcodeTrace AddressSetView hostView = toOverlay(platform.mapGuestToHost(guestView)); AddressSetView hostKnown = ops.getAddressesWithState(snap, hostView, s -> s != null && s != TraceMemoryState.UNKNOWN); - AddressSetView hostResult = hostView.subtract(hostKnown); + AddressSetView hostResult = TraceRegisterUtils.getPhysicalSet(hostView.subtract(hostKnown)); return platform.mapHostToGuest(hostResult); } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/data/DefaultPcodeTraceThreadAccess.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/data/DefaultPcodeTraceThreadAccess.java index 2456b8027f..d6eaf8bb51 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/data/DefaultPcodeTraceThreadAccess.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/data/DefaultPcodeTraceThreadAccess.java @@ -71,18 +71,9 @@ public class DefaultPcodeTraceThreadAccess } @Override - public AddressSetView getKnownNow() { - return memory.getKnownNow().union(registers.getKnownNow()); - } - - @Override - public AddressSetView getKnownBefore() { - return memory.getKnownBefore().union(registers.getKnownBefore()); - } - - @Override - public AddressSetView intersectViewKnown(AddressSetView view) { - return memory.intersectViewKnown(view).union(registers.intersectViewKnown(view)); + public AddressSetView intersectViewKnown(AddressSetView view, boolean useFullSpans) { + return memory.intersectViewKnown(view, useFullSpans) + .union(registers.intersectViewKnown(view, useFullSpans)); } @Override diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/data/PcodeTraceDataAccess.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/data/PcodeTraceDataAccess.java index 71f68a4d49..3d4e6d66f4 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/data/PcodeTraceDataAccess.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/data/PcodeTraceDataAccess.java @@ -63,43 +63,15 @@ public interface PcodeTraceDataAccess { */ TraceMemoryState getViewportState(AddressRange range); - /** - * Get the address set of {@link TraceMemoryState#KNOWN} memory in the source snapshot - * - *

- * Note, this does not consider the snapshot's viewport. - * - * @implNote This can be an expensive operation when the platform is a guest, since what would - * ordinarily be a lazy address set must be computed and translated to the guest - * address spaces. - * - * @return the address set - */ - AddressSetView getKnownNow(); - - /** - * Get the address set of {@link TraceMemoryState#KNOWN} memory among all snapshots from 0 to - * the source snapshot - * - *

- * Note, this does not consider the snapshot's viewport. - * - * @implNote This can be an expensive operation when the platform is a guest, since what would - * ordinarily be a lazy address set must be computed and translated to the guest - * address spaces. - * - * @return the address set - */ - AddressSetView getKnownBefore(); - /** * Compute the intersection of the given address set and the set of * {@link TraceMemoryState#KNOWN} or (@link {@link TraceMemoryState#ERROR} memory * * @param view the address set + * @param useFullSpans how to treat the viewport; true for ever known, false for known now. * @return the intersection */ - AddressSetView intersectViewKnown(AddressSetView view); + AddressSetView intersectViewKnown(AddressSetView view, boolean useFullSpans); /** * Compute the intersection of the given address set and the set of diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceMemoryManager.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceMemoryManager.java index 94e7483997..59599946b3 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceMemoryManager.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceMemoryManager.java @@ -290,7 +290,7 @@ public class DBTraceMemoryManager extends AbstractDBTraceSpaceBasedManager predicate) { return delegateAddressSet(getActiveMemorySpaces(), m -> m.getAddressesWithState(snap, set, predicate)); @@ -311,6 +311,11 @@ public class DBTraceMemoryManager extends AbstractDBTraceSpaceBasedManager> doGetStates(Lifespan span, + AddressRange range) { + return delegateRead(range.getAddressSpace(), m -> m.doGetStates(span, range)); + } + @Override public Collection> getStates(long snap, AddressRange range) { diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceMemorySpace.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceMemorySpace.java index ab13bedd1c..72a269060e 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceMemorySpace.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceMemorySpace.java @@ -434,7 +434,7 @@ public class DBTraceMemorySpace } @Override - public AddressSetView getAddressesWithState(long snap, AddressSetView set, + public AddressSetView getAddressesWithState(Lifespan span, AddressSetView set, Predicate predicate) { try (LockHold hold = LockHold.lock(lock.readLock())) { AddressSet remains = new AddressSet(set); @@ -442,7 +442,7 @@ public class DBTraceMemorySpace while (!remains.isEmpty()) { AddressRange range = remains.getFirstRange(); remains.delete(range); - for (Entry entry : doGetStates(snap, + for (Entry entry : doGetStates(span, range)) { AddressRange foundRange = entry.getKey().getRange(); remains.delete(foundRange); @@ -455,21 +455,20 @@ public class DBTraceMemorySpace } } - protected Collection> doGetStates(long snap, + protected Collection> doGetStates(Lifespan span, AddressRange range) { // TODO: A better way to handle memory-mapped registers? if (getAddressSpace().isRegisterSpace() && !range.getAddressSpace().isRegisterSpace()) { - return trace.getMemoryManager().getStates(snap, range); + return trace.getMemoryManager().doGetStates(span, range); } - return stateMapSpace.reduce(TraceAddressSnapRangeQuery.intersecting(range.getMinAddress(), - range.getMaxAddress(), snap, snap)).entries(); + return stateMapSpace.reduce(TraceAddressSnapRangeQuery.intersecting(range, span)).entries(); } @Override public Collection> getStates(long snap, AddressRange range) { assertInSpace(range); - return doGetStates(snap, range); + return doGetStates(Lifespan.at(snap), range); } @Override diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/memory/TraceMemoryOperations.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/memory/TraceMemoryOperations.java index 78d3b1d8cc..8e1adcb571 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/memory/TraceMemoryOperations.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/memory/TraceMemoryOperations.java @@ -268,6 +268,20 @@ public interface TraceMemoryOperations { Entry getViewMostRecentStateEntry(long snap, Address address); + /** + * Get at least the subset of addresses having state satisfying the given predicate + * + * @param snap the time + * @param set the set to examine + * @param predicate a predicate on state to search for + * @return the address set + * @see #getAddressesWithState(Lifespan, AddressSetView, Predicate) + */ + default AddressSetView getAddressesWithState(long snap, AddressSetView set, + Predicate predicate) { + return getAddressesWithState(Lifespan.at(snap), set, predicate); + } + /** * Get at least the subset of addresses having state satisfying the given predicate * @@ -288,7 +302,7 @@ public interface TraceMemoryOperations { * @param predicate a predicate on state to search for * @return the address set */ - AddressSetView getAddressesWithState(long snap, AddressSetView set, + AddressSetView getAddressesWithState(Lifespan span, AddressSetView set, Predicate predicate); /** diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/TraceRegisterUtils.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/TraceRegisterUtils.java index 5ac2a3bb25..5c19a936a6 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/TraceRegisterUtils.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/TraceRegisterUtils.java @@ -60,6 +60,34 @@ public enum TraceRegisterUtils { return result; } + public static AddressRange getPhysicalRange(AddressRange range) { + AddressSpace space = range.getAddressSpace(); + AddressSpace physical = space.getPhysicalSpace(); + if (space == physical) { + return range; + } + return new AddressRangeImpl( + physical.getAddress(range.getMinAddress().getOffset()), + physical.getAddress(range.getMaxAddress().getOffset())); + } + + /** + * Convert a set in an overlay space to the corresponding set in its physical space + * + * @param set a set contained entirely in one space + * @return the physical set + */ + public static AddressSetView getPhysicalSet(AddressSetView set) { + if (set.isEmpty() || !set.getMinAddress().getAddressSpace().isOverlaySpace()) { + return set; + } + AddressSet result = new AddressSet(); + for (AddressRange rng : set) { + result.add(getPhysicalRange(rng)); + } + return result; + } + public static byte[] padOrTruncate(byte[] arr, int length) { if (arr.length == length) { return arr; diff --git a/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/pcode/exec/trace/BytesTracePcodeEmulatorTest.java b/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/pcode/exec/trace/BytesTracePcodeEmulatorTest.java index 349ca08731..3ff4363ed4 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/pcode/exec/trace/BytesTracePcodeEmulatorTest.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/pcode/exec/trace/BytesTracePcodeEmulatorTest.java @@ -942,6 +942,23 @@ public class BytesTracePcodeEmulatorTest extends AbstractTracePcodeEmulatorTest } } + @Test(expected = DecodePcodeExecutionException.class) + public void testUninitialized() throws Throwable { + try (ToyDBTraceBuilder tb = new ToyDBTraceBuilder("Test", "Toy:BE:64:default")) { + assertEquals(Register.NO_CONTEXT, tb.language.getContextBaseRegister()); + + TraceThread thread = initTrace(tb, """ + pc = 0x00400000; + sp = 0x00110000; + """, + List.of()); // An empty, uninitialized program + + BytesTracePcodeEmulator emu = new BytesTracePcodeEmulator(tb.host, 0); + PcodeThread emuThread = emu.newThread(thread.getPath()); + emuThread.stepInstruction(); + } + } + @Test public void testMov_w_mW1_W0() throws Throwable { try (ToyDBTraceBuilder tb = new ToyDBTraceBuilder("Test", "dsPIC33F:LE:24:default")) { diff --git a/Ghidra/Debug/TaintAnalysis/src/test/java/ghidra/pcode/emu/taint/full/TaintDebuggerPcodeEmulatorTest.java b/Ghidra/Debug/TaintAnalysis/src/test/java/ghidra/pcode/emu/taint/full/TaintDebuggerPcodeEmulatorTest.java index 2138061a17..a9c781946a 100644 --- a/Ghidra/Debug/TaintAnalysis/src/test/java/ghidra/pcode/emu/taint/full/TaintDebuggerPcodeEmulatorTest.java +++ b/Ghidra/Debug/TaintAnalysis/src/test/java/ghidra/pcode/emu/taint/full/TaintDebuggerPcodeEmulatorTest.java @@ -15,7 +15,8 @@ */ package ghidra.pcode.emu.taint.full; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import java.util.Set; @@ -30,18 +31,18 @@ import ghidra.app.plugin.core.debug.service.emulation.DebuggerEmulationServicePl import ghidra.app.plugin.core.debug.service.emulation.DebuggerPcodeMachine; import ghidra.app.plugin.core.debug.service.modules.DebuggerStaticMappingServicePlugin; import ghidra.app.services.DebuggerEmulationService; +import ghidra.app.services.DebuggerEmulationService.EmulationResult; import ghidra.app.services.DebuggerStaticMappingService; import ghidra.pcode.emu.taint.trace.TaintTracePcodeEmulatorTest; import ghidra.pcode.emu.taint.trace.TaintTracePcodeExecutorStatePiece; import ghidra.program.model.address.AddressSpace; import ghidra.program.model.util.StringPropertyMap; import ghidra.program.util.ProgramLocation; -import ghidra.trace.model.DefaultTraceLocation; -import ghidra.trace.model.Lifespan; +import ghidra.trace.model.*; import ghidra.trace.model.property.TracePropertyMap; import ghidra.trace.model.property.TracePropertyMapSpace; import ghidra.trace.model.thread.TraceThread; -import ghidra.trace.model.time.schedule.TraceSchedule; +import ghidra.trace.model.time.schedule.*; import ghidra.util.task.TaskMonitor; public class TaintDebuggerPcodeEmulatorTest extends AbstractGhidraHeadedDebuggerGUITest { @@ -75,11 +76,19 @@ public class TaintDebuggerPcodeEmulatorTest extends AbstractGhidraHeadedDebugger traceManager.activateTrace(tb.trace); - TraceSchedule time = TraceSchedule.parse("0:t0-1"); - emuService.emulate(tb.trace, time, TaskMonitor.DUMMY); - traceManager.activateTime(time); + EmulationResult result = + emuService.run(tb.host, TraceSchedule.snap(0), monitor, new Scheduler() { + int calls = 0; - DebuggerPcodeMachine emu = emuService.getCachedEmulator(tb.trace, time); + @Override + public TickStep nextSlice(Trace trace) { + // Expect decode of uninitialized memory immediately + assertEquals(0, calls++); + return new TickStep(0, 1); + } + }); + + DebuggerPcodeMachine emu = emuService.getCachedEmulator(tb.trace, result.schedule()); assertTrue(emu instanceof TaintDebuggerPcodeEmulator); } diff --git a/Ghidra/Framework/Emulation/src/main/java/ghidra/pcode/exec/BytesPcodeExecutorStateSpace.java b/Ghidra/Framework/Emulation/src/main/java/ghidra/pcode/exec/BytesPcodeExecutorStateSpace.java index e34e3d826a..757de237a5 100644 --- a/Ghidra/Framework/Emulation/src/main/java/ghidra/pcode/exec/BytesPcodeExecutorStateSpace.java +++ b/Ghidra/Framework/Emulation/src/main/java/ghidra/pcode/exec/BytesPcodeExecutorStateSpace.java @@ -32,6 +32,7 @@ import ghidra.util.Msg; * @param if this space is a cache, the type of object backing this space */ public class BytesPcodeExecutorStateSpace { + protected final static byte[] EMPTY = new byte[] {}; protected final SemisparseByteArray bytes; protected final Language language; // for logging diagnostics protected final AddressSpace space; @@ -197,8 +198,13 @@ public class BytesPcodeExecutorStateSpace { warnUninit(uninitialized); } else if (reason == Reason.EXECUTE_DECODE) { - throw new DecodePcodeExecutionException("Cannot decode uninitialized memory", - space.getAddress(offset)); + /** + * The callers may be reading ahead, so it's not appropriate to throw an exception here. + * Instead, communicate there's no more. If the buffer's empty on their end, they'll + * handle the error as appropriate. If it's in the emulator, the instruction decoder + * should eventually throw the decode exception. + */ + return EMPTY; } return readBytes(offset, size, reason); } diff --git a/Ghidra/Framework/Emulation/src/main/java/ghidra/pcode/exec/DecodePcodeExecutionException.java b/Ghidra/Framework/Emulation/src/main/java/ghidra/pcode/exec/DecodePcodeExecutionException.java index 2be36d8aa6..0d996c5b6f 100644 --- a/Ghidra/Framework/Emulation/src/main/java/ghidra/pcode/exec/DecodePcodeExecutionException.java +++ b/Ghidra/Framework/Emulation/src/main/java/ghidra/pcode/exec/DecodePcodeExecutionException.java @@ -21,7 +21,7 @@ public class DecodePcodeExecutionException extends PcodeExecutionException { private final Address pc; public DecodePcodeExecutionException(String message, Address pc) { - super(message + ", PC=" + pc); + super(message.contains("PC=") ? message : "%s (PC=%s)".formatted(message, pc)); this.pc = pc; }