diff --git a/Ghidra/Debug/Debugger/ghidra_scripts/SearchBytesAcrossTime.java b/Ghidra/Debug/Debugger/ghidra_scripts/SearchBytesAcrossTime.java new file mode 100644 index 0000000000..755eaab374 --- /dev/null +++ b/Ghidra/Debug/Debugger/ghidra_scripts/SearchBytesAcrossTime.java @@ -0,0 +1,137 @@ +/* ### + * 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. + */ +// Search the entire trace, across all time, for a user-specified byte pattern. +//@category Debugger + +import javax.swing.*; +import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.ArrayList; +import java.util.List; + +import docking.ComponentProvider; +import docking.widgets.table.*; +import ghidra.app.script.GhidraScript; +import ghidra.app.services.DebuggerListingService; +import ghidra.app.services.DebuggerTraceManagerService; +import ghidra.app.services.DebuggerTraceManagerService.ActivationCause; +import ghidra.async.AsyncUtils; +import ghidra.debug.api.tracemgr.DebuggerCoordinates; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.address.*; +import ghidra.trace.model.*; +import ghidra.trace.model.memory.TraceMemoryManager; + +public class SearchBytesAcrossTime extends GhidraScript { + public record Row(Address address, long startSnap, long endSnap) { + Row(TraceAddressSnapRange match) { + this(match.getRange().getMinAddress(), match.getLifespan().lmin(), + match.getLifespan().lmax()); + } + } + + private static class ResultsProvider extends ComponentProvider { + private final JComponent component; + + ResultsProvider(PluginTool tool, String title, List rows) { + super(tool, title, ResultsProvider.class.getSimpleName()); + + AnyObjectTableModel model = + new AnyObjectTableModel<>("Matches", Row.class, "startSnap", "endSnap", + "address"); + model.setModelData(rows); + GTable table = new GTable(model); + + table.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (e.getClickCount() != 2) { + return; + } + int viewRow = table.getSelectedRow(); + if (viewRow == -1) { + return; + } + int modelRow = table.convertRowIndexToModel(viewRow); + Row row = rows.get(modelRow); + navigateTo(tool, row.startSnap, row.address); + } + }); + + GTableFilterPanel filterPanel = new GTableFilterPanel<>(table, model); + + final JPanel panel = new JPanel(new BorderLayout()); + panel.add(new JScrollPane(table)); + panel.add(filterPanel, BorderLayout.SOUTH); + + component = panel; + setTransient(); + setVisible(true); + } + + private void navigateTo(PluginTool tool, long snap, Address address) { + DebuggerTraceManagerService traceManager = + tool.getService(DebuggerTraceManagerService.class); + DebuggerListingService listingService = tool.getService(DebuggerListingService.class); + + DebuggerCoordinates coords = traceManager.getCurrent().snap(snap); + + traceManager.activateAndNotify(coords, ActivationCause.USER) + .thenRunAsync(() -> listingService.goTo(address, true), + AsyncUtils.SWING_EXECUTOR); + } + + @Override + public JComponent getComponent() { + return component; + } + } + + @Override + protected void run() throws Exception { + DebuggerTraceManagerService traceManager = + state.getTool().getService(DebuggerTraceManagerService.class); + Trace trace = traceManager.getCurrentTrace(); + if (trace == null) { + printerr("No trace is active"); + return; + } + + byte[] pattern = askBytes("Search Across Time", "Enter byte pattern to search for:"); + if (pattern == null || pattern.length == 0) { + printerr("No pattern given"); + return; + } + + TraceMemoryManager mem = trace.getMemoryManager(); + + List rows = new ArrayList<>(); + + // This searches the default address space, if your architecture uses other address spaces + // this needs to be updated. + AddressRange fullRange = new AddressRangeImpl(toAddr(0L), toAddr(0xFFFF_FFFF_FFFF_FFFFL)); + List bytesAcrossLifespan = + mem.findBytesAcrossLifespan(Lifespan.ALL, fullRange, pattern, monitor); + + for (TraceAddressSnapRange traceAddressSnapRange : bytesAcrossLifespan) { + rows.add(new Row(traceAddressSnapRange)); + } + + println("Found " + rows.size() + " match(es)"); + new ResultsProvider(state.getTool(), "Search Across Time Results MEMORY_BLOCKS", rows); + } +} diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/map/DBTraceAddressSnapRangePropertyMapTree.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/map/DBTraceAddressSnapRangePropertyMapTree.java index 8ab5dec911..5a1e47450a 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/map/DBTraceAddressSnapRangePropertyMapTree.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/map/DBTraceAddressSnapRangePropertyMapTree.java @@ -516,7 +516,7 @@ public class DBTraceAddressSnapRangePropertyMapTree> getStates(Lifespan span, + AddressRange range) { + return delegateReadOr(range.getAddressSpace(), m -> m.getStates(span, range), + () -> List.of( + Map.entry(new ImmutableTraceAddressSnapRange(range, span), + TraceMemoryState.UNKNOWN))); + } + + @Override + public List findBytesAcrossLifespan(Lifespan span, AddressRange range, byte[] pattern, + TaskMonitor monitor) { + return delegateReadOr(range.getAddressSpace(), + m -> m.findBytesAcrossLifespan(span, range, pattern, monitor), List::of); + } + @Override public Iterable> getMostRecentStates( TraceAddressSnapRange within) { 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 852ae2a82f..ac30ef1a34 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 @@ -1083,6 +1083,270 @@ public class DBTraceMemorySpace } } + private static List withinBlockMatchOffsets(byte[] buf, byte[] pattern) { + List offsets = new ArrayList<>(); + byte firstByte = pattern[0]; + for (int off = 0; off < buf.length - pattern.length; off++) { + if (buf[off] == firstByte && + Arrays.mismatch(buf, off, off + pattern.length, pattern, 0, pattern.length) == -1) { + offsets.add(off); + } + } + return offsets; + } + + private static List partialUpperBoundaryMatchOffsets(byte[] buf, byte[] pattern) { + List offsets = new ArrayList<>(); + int startOffset = buf.length - Math.min(buf.length, pattern.length) + 1; + byte firstByte = pattern[0]; + for (int off = startOffset; off < buf.length; off++) { + int lengthToCheck = buf.length - off; + if (buf[off] == firstByte && + Arrays.mismatch(buf, off, off + lengthToCheck, pattern, 0, lengthToCheck) == -1) { + offsets.add(off); + } + } + return offsets; + } + + private static List coalesceLifespansAndPruneToExactSearch(int hitLength, + Map> hits, AddressRange targetRange, Lifespan targetSpan) { + hits.values().forEach(l -> l.sort(Comparator.comparing(Lifespan::lmin))); + + List retHits = new ArrayList<>(); + + for (Entry> entry : hits.entrySet()) { + AddressRangeImpl range = + new AddressRangeImpl(entry.getKey(), entry.getKey().add(hitLength)); + if (!targetRange.intersects(range.getMinAddress(), range.getMaxAddress())) { + continue; + } + Lifespan accumulator = entry.getValue().getFirst(); + for (Lifespan lifespan : entry.getValue()) { + if (accumulator != lifespan) { + if (accumulator.intersects(lifespan) || + accumulator.lmax() + 1 == lifespan.lmin()) { + accumulator = accumulator.bound(lifespan); + } + else { + retHits.add(new ImmutableTraceAddressSnapRange(range, accumulator)); + accumulator = lifespan; + } + } + } + ImmutableTraceAddressSnapRange snapRange = + new ImmutableTraceAddressSnapRange(range, accumulator); + if (!retHits.contains(snapRange)) { + retHits.add(snapRange); + } + } + + return retHits.stream().filter(e -> e.getLifespan().intersects(targetSpan)).toList(); + } + + @Override + public Collection> getStates(Lifespan span, + AddressRange range) { + assertInSpace(range); + return doGetStates(span, range); + } + + @Override + public List findBytesAcrossLifespan(Lifespan span, AddressRange range, byte[] pattern, + TaskMonitor monitor) { + assertInSpace(range); + if (pattern.length == 0) { + return List.of(); + } + + Map> hits = searchBytesByMemoryBlocks(range, pattern, monitor); + + return coalesceLifespansAndPruneToExactSearch(pattern.length, hits, range, span); + } + + private Map> searchBytesByMemoryBlocks(AddressRange range, + byte[] pattern, TaskMonitor monitor) { + // Cheat a little to include a lower block in the case that the pattern crosses the lower + // search boundary + long minOffset = Math.max(range.getMinAddress().getUnsignedOffset() - pattern.length, 0); + long maxOffset = range.getMaxAddress().getUnsignedOffset(); + long firstBlock = minOffset & BLOCK_MASK; + long lastBlock = maxOffset & BLOCK_MASK; + + OffsetSnap lo = new OffsetSnap(firstBlock, Long.MIN_VALUE); + OffsetSnap hi = new OffsetSnap(lastBlock, Long.MAX_VALUE); + + Map> hits = new HashMap<>(); + + for (Entry ent : blocksByOffset.sub(lo, true, hi, + true).entries()) { + if (monitor.isCancelled()) { + return Map.of(); + } + + byte[] blockBuf = new byte[BLOCK_SIZE]; + try (var _ = LockHold.lock(lock.readLock())) { + ent.getValue().getBytes(ByteBuffer.wrap(blockBuf), 0, BLOCK_SIZE); + } + catch (IOException e) { + blockStore.dbError(e); + continue; + } + Map lifespanCache = new HashMap<>(); + patternSearchWithinBlock(pattern, ent, blockBuf, hits, lifespanCache); + patternSearchSpanningBlocks(pattern, monitor, ent, blockBuf, hits, lifespanCache); + } + + return hits; + } + + private void patternSearchWithinBlock(byte[] pattern, + Entry ent, byte[] blockBuf, + Map> hits, Map lifespanCache) { + for (int off : withinBlockMatchOffsets(blockBuf, pattern)) { + Address address = toAddress(ent.getValue().getOffset() + off); + hits.computeIfAbsent(address, e -> new ArrayList<>()); + hits.get(address).add(getLifespanOfBlock(ent.getKey(), lifespanCache)); + } + } + + private void patternSearchSpanningBlocks(byte[] pattern, TaskMonitor monitor, + Entry ent, byte[] blockBuf, + Map> hits, Map lifespanCache) { + int adjacentBlocksToCheck = (int) Math.ceil((double) pattern.length / BLOCK_SIZE) + 1; + + int leadingZeros = 0; + for (byte b : pattern) { + if (b != 0) { + break; + } + leadingZeros++; + } + byte[] modifiedPattern = Arrays.copyOfRange(pattern, leadingZeros, pattern.length); + + // Check upper bounds if we hit a partial match + for (int off : partialUpperBoundaryMatchOffsets(blockBuf, modifiedPattern)) { + if (monitor.isCancelled()) { + return; + } + List adjacentBlocks = + getIntersectingLifespansOfAdjacentBlocks(ent.getKey(), adjacentBlocksToCheck, + null, lifespanCache); + for (Lifespan lifespan : adjacentBlocks) { + byte[] buf = new byte[pattern.length]; + getBytes(lifespan.lmin(), + toAddress(ent.getValue().getOffset() + off - leadingZeros), + ByteBuffer.wrap(buf)); + + if (Arrays.mismatch(buf, pattern) == -1) { + Address address = toAddress(ent.getValue().getOffset() + off - leadingZeros); + hits.computeIfAbsent(address, e -> new ArrayList<>()); + hits.get(address).add(lifespan); + } + } + } + + // Check lower bounds since we got leading 0s to deal with + if (leadingZeros > 0 && !lowerMemoryBlockExists(ent.getKey())) { + byte[] buf = new byte[pattern.length]; + getBytes(ent.getKey().snap, toAddress(ent.getValue().getOffset() - leadingZeros), + ByteBuffer.wrap(buf)); + if (Arrays.mismatch(buf, pattern) == -1) { + Address address = toAddress(ent.getValue().getOffset() - leadingZeros); + hits.computeIfAbsent(address, e -> new ArrayList<>()); + hits.get(address).add(getLifespanOfBlock(ent.getKey(), lifespanCache)); + } + } + } + + private boolean lowerMemoryBlockExists(OffsetSnap offsetSnap) { + OffsetSnap start = new OffsetSnap(offsetSnap.offset - BLOCK_SIZE, 0); + OffsetSnap stop = new OffsetSnap(offsetSnap.offset - BLOCK_SIZE, Long.MAX_VALUE); + return blocksByOffset.sub(start, true, stop, true).keys().iterator().hasNext(); + } + + private boolean upperMemoryBlockExists(OffsetSnap offsetSnap) { + OffsetSnap start = new OffsetSnap(offsetSnap.offset + BLOCK_SIZE, 0); + OffsetSnap stop = new OffsetSnap(offsetSnap.offset + BLOCK_SIZE, Long.MAX_VALUE); + return blocksByOffset.sub(start, true, stop, true).keys().iterator().hasNext(); + } + + private Lifespan getLifespanOfBlock(OffsetSnap offsetSnap, Map cache) { + if (cache != null && cache.containsKey(offsetSnap)) { + return cache.get(offsetSnap); + } + Lifespan lifespan; + OffsetSnap nextOffsetSnap = blocksByOffset.higherKey(offsetSnap); + if (nextOffsetSnap == null) { + lifespan = Lifespan.nowOn(offsetSnap.snap); + } + else if (offsetSnap.offset == nextOffsetSnap.offset && + offsetSnap.isScratch() == nextOffsetSnap.isScratch()) { + lifespan = Lifespan.span(offsetSnap.snap, nextOffsetSnap.snap - 1); + } + else if (offsetSnap.isScratch()) { + lifespan = Lifespan.at(offsetSnap.snap); + } + else { + lifespan = Lifespan.nowOn(offsetSnap.snap); + } + if (cache != null) { + cache.put(offsetSnap, lifespan); + } + return lifespan; + } + + private List getIntersectingLifespansOfAdjacentBlocks(OffsetSnap offsetSnap, + int numberOfBlocks, Lifespan accumulatorSpan, + Map lifespanCache) { + if (numberOfBlocks == 1) { + return List.of(accumulatorSpan); + } + List intersectingLifespans = new ArrayList<>(); + Lifespan curLifespan; + if (accumulatorSpan != null) { + curLifespan = accumulatorSpan; + } + else { + curLifespan = getLifespanOfBlock(offsetSnap, lifespanCache); + } + + if (upperMemoryBlockExists(offsetSnap)) { + // Check all lifespans of next block for intersections + OffsetSnap firstHit = null; + OffsetSnap start = new OffsetSnap(offsetSnap.offset + BLOCK_SIZE, 0); + OffsetSnap stop = new OffsetSnap(offsetSnap.offset + BLOCK_SIZE, Long.MAX_VALUE); + for (OffsetSnap adjacentOffsetSnap : blocksByOffset.sub(start, true, stop, true) + .keys()) { + Lifespan intersected = + getLifespanOfBlock(adjacentOffsetSnap, lifespanCache).intersect( + curLifespan); + if (firstHit == null) { + firstHit = adjacentOffsetSnap; + } + if (!intersected.isEmpty()) { + intersectingLifespans.addAll( + getIntersectingLifespansOfAdjacentBlocks(adjacentOffsetSnap, + numberOfBlocks - 1, intersected, lifespanCache)); + } + } + + // Check if lifespan before block was initialized (i.e. when it was all 0s) intersects + assert firstHit != null; + Lifespan intersected = Lifespan.before(firstHit.snap).intersect(curLifespan); + if (!intersected.isEmpty()) { + intersectingLifespans.addAll(getIntersectingLifespansOfAdjacentBlocks( + new OffsetSnap(firstHit.offset, Long.MIN_VALUE), numberOfBlocks - 1, + intersected, lifespanCache)); + } + } + else { + // Upper block was never touched so just return same lifespan + intersectingLifespans.add(curLifespan); + } + return intersectingLifespans; + } + @Override public MemBuffer getBufferAt(long snap, Address start, ByteOrder byteOrder) { return new DBTraceMemBuffer(this, snap, start, byteOrder); 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 98a98bc022..312b10c92f 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 @@ -17,8 +17,7 @@ package ghidra.trace.model.memory; import java.nio.ByteBuffer; import java.nio.ByteOrder; -import java.util.Collection; -import java.util.Iterator; +import java.util.*; import java.util.Map.Entry; import java.util.function.Predicate; @@ -323,6 +322,35 @@ public interface TraceMemoryOperations { Collection> getStates(long snap, AddressRange range); + /** + * Get all the entries covering the given range effective within the given span + *

+ * Note that {@link TraceMemoryState#UNKNOWN} entries will not appear in the result. Gaps in + * the returned entries are implied to be {@link TraceMemoryState#UNKNOWN}. + * + * @param span the time span to examine + * @param range the range to examine + * @return the map of ranges to states + */ + Collection> getStates(Lifespan span, + AddressRange range); + + /** + * Search the given range, across a given lifespan, for a byte pattern. + *

+ * Addresses outside the given range may be returned if the pattern matches at the edges of + * the given range. Full lifespans for each hit are included as long as they intersect the + * lifespan given. + * + * @param span the range of time + * @param range the range to search + * @param pattern the pattern + * @param monitor a monitor for cancellation/progress + * @return the list of matches, each with the span of time during which it holds + */ + List findBytesAcrossLifespan(Lifespan span, AddressRange range, + byte[] pattern, TaskMonitor monitor); + /** * Check if a range addresses are all known * diff --git a/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/database/memory/AbstractDBTraceMemoryManagerMemoryTest.java b/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/database/memory/AbstractDBTraceMemoryManagerMemoryTest.java index 549fbe316e..9500ce804f 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/database/memory/AbstractDBTraceMemoryManagerMemoryTest.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/database/memory/AbstractDBTraceMemoryManagerMemoryTest.java @@ -15,9 +15,6 @@ */ package ghidra.trace.database.memory; -import static org.junit.Assert.*; -import static org.junit.Assume.assumeFalse; - import java.math.BigInteger; import java.nio.ByteBuffer; import java.nio.file.Files; @@ -25,9 +22,6 @@ import java.nio.file.Path; import java.util.*; import java.util.Map.Entry; -import org.junit.Ignore; -import org.junit.Test; - import db.DBHandle; import db.Transaction; import ghidra.framework.data.OpenMode; @@ -35,8 +29,7 @@ import ghidra.program.model.address.*; import ghidra.program.model.lang.Register; import ghidra.program.model.lang.RegisterValue; import ghidra.trace.database.DBTrace; -import ghidra.trace.model.Lifespan; -import ghidra.trace.model.TraceAddressSnapRange; +import ghidra.trace.model.*; import ghidra.trace.model.memory.*; import ghidra.trace.model.memory.TraceMemoryOperations.StatePredicate; import ghidra.trace.model.thread.TraceThread; @@ -44,6 +37,11 @@ import ghidra.trace.model.thread.TraceThreadManager; import ghidra.util.SystemUtilities; import ghidra.util.task.ConsoleTaskMonitor; import ghidra.util.task.TaskMonitor; +import org.junit.Ignore; +import org.junit.Test; + +import static org.junit.Assert.*; +import static org.junit.Assume.assumeFalse; public abstract class AbstractDBTraceMemoryManagerMemoryTest extends AbstractDBTraceMemoryManagerTest { @@ -1010,6 +1008,108 @@ public abstract class AbstractDBTraceMemoryManagerMemoryTest } } + @Test + public void testFindBytesAcrossLifespan() { + try (Transaction tx = b.startTransaction()) { + assertEquals(5, memory.putBytes(2, b.addr(0x4ffb), b.buf(1, 1, 1, 1, 1))); + assertEquals(5, memory.putBytes(3, b.addr(0x4000), b.buf(1, 2, 3, 4, 5))); + // Test lifespan + assertEquals(5, memory.putBytes(4, b.addr(0x4001), b.buf(1, 2, 3, 4, 5))); + assertEquals(5, memory.putBytes(5, b.addr(0x4200), b.buf(1, 2, 3, 4, 5))); + // Test block spanning + assertEquals(5, memory.putBytes(6, b.addr(0x4fff), b.buf(1, 2, 3, 4, 5))); + assertEquals(5, memory.putBytes(7, b.addr(0x4321), b.buf(1, 2, 3, 4, 5))); + assertEquals(5, memory.putBytes(8, b.addr(0x4000), b.buf(1, 2, 3, 4, 5))); + assertEquals(5, memory.putBytes(9, b.addr(0x4000), b.buf(1, 1, 1, 1, 1))); + assertEquals(5, memory.putBytes(10, b.addr(0x5ffb), b.buf(1, 1, 1, 1, 1))); + + } + + ImmutableTraceAddressSnapRange hit1 = + new ImmutableTraceAddressSnapRange(b.range(0x4000, 0x4005), Lifespan.at(3)); + ImmutableTraceAddressSnapRange hit2 = + new ImmutableTraceAddressSnapRange(b.range(0x4001, 0x4006), Lifespan.span(4, 7)); + ImmutableTraceAddressSnapRange hit3 = + new ImmutableTraceAddressSnapRange(b.range(0x4200, 0x4205), Lifespan.nowOn(5)); + ImmutableTraceAddressSnapRange hit4 = + new ImmutableTraceAddressSnapRange(b.range(0x4fff, 0x5004), Lifespan.nowOn(6)); + ImmutableTraceAddressSnapRange hit5 = + new ImmutableTraceAddressSnapRange(b.range(0x4321, 0x4326), Lifespan.nowOn(7)); + ImmutableTraceAddressSnapRange hit6 = + new ImmutableTraceAddressSnapRange(b.range(0x4000, 0x4005), Lifespan.at(8)); + ImmutableTraceAddressSnapRange hit7 = + new ImmutableTraceAddressSnapRange(b.range(0x3fff, 0x4005), Lifespan.nowOn(9)); + ImmutableTraceAddressSnapRange hit8 = + new ImmutableTraceAddressSnapRange(b.range(0x4ffb, 0x5001), Lifespan.span(2, 5)); + ImmutableTraceAddressSnapRange hit9 = + new ImmutableTraceAddressSnapRange(b.range(0x5ffb, 0x6001), Lifespan.nowOn(10)); + + // Lifespan All + List bytesAcrossLifespan = + memory.findBytesAcrossLifespan(Lifespan.ALL, b.range(0, 0xFFFF_FFFF_FFFF_FFFFL), + new byte[] { 1, 2, 3, 4, 5 }, TaskMonitor.DUMMY); + assertContainsExactly(List.of(hit1, hit2, hit3, hit4, hit5, hit6), bytesAcrossLifespan); + + // Narrow lifespan + bytesAcrossLifespan = memory.findBytesAcrossLifespan(Lifespan.before(5), + b.range(0, 0xFFFF_FFFF_FFFF_FFFFL), + new byte[] { 1, 2, 3, 4, 5 }, TaskMonitor.DUMMY); + assertContainsExactly(List.of(hit1, hit2), bytesAcrossLifespan); + + // Narrow Range + bytesAcrossLifespan = memory.findBytesAcrossLifespan(Lifespan.ALL, b.range(0x4200, 0x4500), + new byte[] { 1, 2, 3, 4, 5 }, TaskMonitor.DUMMY); + assertContainsExactly(List.of(hit3, hit5), bytesAcrossLifespan); + + // Narrow lifespan/range + bytesAcrossLifespan = memory.findBytesAcrossLifespan(Lifespan.since(5), + b.range(0x4200, 0x4500), + new byte[] { 1, 2, 3, 4, 5 }, TaskMonitor.DUMMY); + assertContainsExactly(List.of(hit3), bytesAcrossLifespan); + + // Narrow lifespan/range (miss) + bytesAcrossLifespan = memory.findBytesAcrossLifespan(Lifespan.before(5), + b.range(0x4200, 0x4500), + new byte[] { 1, 2, 3, 4, 5 }, TaskMonitor.DUMMY); + assertContainsExactly(List.of(), bytesAcrossLifespan); + + // Empty pattern + bytesAcrossLifespan = + memory.findBytesAcrossLifespan(Lifespan.ALL, b.range(0, 0xFFFF_FFFF_FFFF_FFFFL), new byte[] {}, + TaskMonitor.DUMMY); + assertContainsExactly(List.of(), bytesAcrossLifespan); + + // Leading 0s, with block crossing + bytesAcrossLifespan = memory.findBytesAcrossLifespan(Lifespan.ALL, + b.range(0, 0xFFFF_FFFF_FFFF_FFFFL), + new byte[] { 0, 1, 1, 1, 1, 1 }, TaskMonitor.DUMMY); + // hit7 should work, no memory block means it's all 0s + assertContainsExactly(List.of(hit7), bytesAcrossLifespan); + + // Trailing 0s, with block crossing + bytesAcrossLifespan = memory.findBytesAcrossLifespan(Lifespan.before(6), + b.range(0, 0xFFFF_FFFF_FFFF_FFFFL), + new byte[] { 1, 1, 1, 1, 1, 0 }, TaskMonitor.DUMMY); + // hit8 should work, no memory block means it's all 0s + assertContainsExactly(List.of(hit8), bytesAcrossLifespan); + + // 1 byte intersect on range, start of pattern, with block crossing + bytesAcrossLifespan = memory.findBytesAcrossLifespan(Lifespan.ALL, b.range(0x4fff, 0x4fff), + new byte[] { 1, 2, 3, 4, 5 }, TaskMonitor.DUMMY); + assertContainsExactly(List.of(hit4), bytesAcrossLifespan); + + // 1 byte intersect on range, end of pattern, with block crossing + bytesAcrossLifespan = memory.findBytesAcrossLifespan(Lifespan.ALL, b.range(0x5004, 0x5004), + new byte[] { 1, 2, 3, 4, 5 }, TaskMonitor.DUMMY); + assertContainsExactly(List.of(hit4), bytesAcrossLifespan); + + // Block crossing into never touched memory + bytesAcrossLifespan = memory.findBytesAcrossLifespan(Lifespan.nowOn(6), + b.range(0, 0xFFFF_FFFF_FFFF_FFFFL), + new byte[] { 1, 1, 1, 1, 1, 0 }, TaskMonitor.DUMMY); + assertContainsExactly(List.of(hit9), bytesAcrossLifespan); + } + /** * Based on old issue: https://github.com/NationalSecurityAgency/ghidra/issues/2760 that came up * again in another context.