GP-7222: Add experimental debugger developer scripts

This commit is contained in:
ghidra_red
2026-09-04 15:12:28 +00:00
parent bad07f4b8a
commit a0ad5c740e
2 changed files with 690 additions and 0 deletions
@@ -0,0 +1,412 @@
/* ###
* 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.
*/
//EXPERIMENTAL script that tries to track memory allocations and frees in a trace.
//@category Debugger
//@keybinding
//@menupath
//@toolbar
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.*;
import ghidra.app.script.GhidraScript;
import ghidra.app.services.DebuggerStaticMappingService;
import ghidra.app.services.ProgramManager;
import ghidra.debug.api.modules.MappedAddressRange;
import ghidra.debug.flatapi.FlatDebuggerAPI;
import ghidra.program.model.address.*;
import ghidra.program.model.lang.CompilerSpec;
import ghidra.program.model.lang.RegisterValue;
import ghidra.program.model.listing.*;
import ghidra.program.model.symbol.RefType;
import ghidra.trace.model.*;
import ghidra.trace.model.stack.TraceStackFrame;
import ghidra.trace.model.target.TraceObjectValue;
import ghidra.trace.model.thread.TraceThread;
public class AllocationTracker extends GhidraScript implements FlatDebuggerAPI {
public record AllocFreeEvent(long snap, long size, Address allocedAddress,
Address freedAddress,
Function function, long returnStatus) {
}
public record EntryExitEvent(long snap, AllocOrFreeFunc allocOrFreeFunc, boolean entry) {
}
public record AllocatedBuffer(Lifespan lifespan, long size, Address startAddress) {
}
public static class AllocFreeEventBuilder {
private long snap;
private long size;
private Address allocatedAddress;
private Address freedAddress;
private Function function;
private boolean clear = true;
private long returnStatus;
public AllocFreeEventBuilder setSnap(long snap) {
clear = false;
this.snap = snap;
return this;
}
public AllocFreeEventBuilder setSize(long size) {
clear = false;
this.size = size;
return this;
}
public AllocFreeEventBuilder setAllocatedAddress(Address allocatedAddress) {
clear = false;
this.allocatedAddress = allocatedAddress;
return this;
}
public AllocFreeEventBuilder setFreedAddress(Address freedAddress) {
clear = false;
this.freedAddress = freedAddress;
return this;
}
public AllocFreeEventBuilder setReturnStatus(long returnStatus) {
clear = false;
this.returnStatus = returnStatus;
return this;
}
public AllocFreeEventBuilder setFunction(Function function) {
clear = false;
this.function = function;
return this;
}
public AllocFreeEvent createAllocFreeEvent() {
AllocFreeEvent event =
new AllocFreeEvent(snap, size, allocatedAddress, freedAddress, function,
returnStatus);
snap = 0;
size = 0;
returnStatus = 0;
allocatedAddress = null;
freedAddress = null;
function = null;
clear = true;
return event;
}
public boolean isClear() {
return clear;
}
}
public class AllocFreePairings {
private final List<AllocOrFreeFunc> funcs;
AllocFreePairings(List<AllocOrFreeFunc> funcs) {
this.funcs = funcs;
}
List<AllocFreeEvent> getAllEvents() {
AddressSet addressSet = new AddressSet();
funcs.forEach(e -> {
addressSet.add(e.dynFunctionStart);
addressSet.add(e.dynFunctionReturns);
});
return getCurrentTrace().getObjectManager()
.getRootSchema()
.getContext()
.getAllSchemas()
.stream()
.filter(e -> e.getInterfaces().contains(TraceStackFrame.class))
.map(s -> s.checkAliasedAttribute(TraceStackFrame.KEY_PC))
.flatMap(path -> funcs.stream()
.flatMap(func -> func.dynFunctionStartsAndReturns.stream()
.flatMap(addrRange -> getCurrentTrace().getObjectManager()
.getValuesIntersecting(Lifespan.ALL, addrRange, path)
.stream()
.sorted(Comparator.comparingLong(
TraceObjectValue::getMinSnap))
.map(tov -> {
if (func.dynFunctionStart.intersects(addrRange)) {
return new EntryExitEvent(tov.getMinSnap(),
func, true);
}
else {
return new EntryExitEvent(tov.getMinSnap(),
func, false);
}
}))))
.sorted(Comparator.comparingLong(EntryExitEvent::snap))
.map(e -> {
if (e.entry) {
e.allocOrFreeFunc.onEntry(e.snap());
return null;
}
else {
return e.allocOrFreeFunc.onExit(e.snap());
}
})
.filter(Objects::nonNull)
.toList();
}
}
class AllocOrFreeFunc {
Function function;
AddressSet functionStarts;
AddressSet functionReturns;
AddressSet dynFunctionStart;
AddressSet dynFunctionReturns;
AddressSet dynFunctionStartsAndReturns;
AllocFreeEventBuilder allocFreeEventBuilder;
AllocOrFreeFunc(Function function) {
this.function = function;
functionStarts = new AddressSet(function.getEntryPoint());
functionReturns = new AddressSet();
for (Instruction instruction : function.getProgram()
.getListing()
.getInstructions(function.getBody(), true)) {
if (instruction.getFlowType().equals(RefType.TERMINATOR)) {
functionReturns.add(instruction.getAddress());
}
}
dynFunctionStart = getDynamicAddresses(functionStarts, function.getProgram());
dynFunctionReturns = getDynamicAddresses(functionReturns, function.getProgram());
dynFunctionStartsAndReturns = new AddressSet();
dynFunctionStartsAndReturns.add(dynFunctionStart);
dynFunctionStartsAndReturns.add(dynFunctionReturns);
allocFreeEventBuilder = new AllocFreeEventBuilder();
}
AddressSet getDynamicAddresses(AddressSet staticSet, Program program) {
final DebuggerStaticMappingService staticMappingService =
state.getTool().getService(DebuggerStaticMappingService.class);
final AddressSet dynamicSet = new AddressSet();
for (AddressRange address : staticSet) {
Map<TraceSpan, Collection<MappedAddressRange>> openMappedViews =
staticMappingService.getOpenMappedViews(program, new AddressSet(address));
for (Map.Entry<TraceSpan, Collection<MappedAddressRange>> entry :
openMappedViews.entrySet()) {
if (entry.getKey().getTrace() != getCurrentTrace()) {
continue;
}
for (MappedAddressRange addressRange : entry.getValue()) {
dynamicSet.add(addressRange.getDestinationAddressRange().getMinAddress(),
addressRange.getDestinationAddressRange().getMaxAddress());
}
}
}
return dynamicSet;
}
BigInteger getVariableValue(Variable variable, long snap) {
Trace trace = getCurrentTrace();
Address address = variable.getMinAddress();
final ByteBuffer buf = ByteBuffer.allocate(variable.getLength());
TraceThread eventThread =
trace.getTimeManager().getSnapshot(snap, false).getEventThread();
if (address.isStackAddress()) {
final CompilerSpec spec = trace.getBaseCompilerSpec();
final RegisterValue value = trace.getMemoryManager()
.getMemoryRegisterSpace(eventThread, false)
.getValue(snap, spec.getStackPointer());
final Address stackPointer =
spec.getStackBaseSpace().getAddress(value.getUnsignedValue().longValue());
address = stackPointer.add(address.getOffset());
trace.getMemoryManager().getBytes(snap, address, buf);
}
else if (address.isMemoryAddress()) {
trace.getMemoryManager().getBytes(snap, address, buf);
}
else if (address.isRegisterAddress()) {
final RegisterValue regVal = trace.getMemoryManager()
.getMemoryRegisterSpace(eventThread, false)
.getValue(snap, variable.getRegister());
return regVal.getUnsignedValue();
}
byte[] bytes = new byte[buf.remaining()];
buf.get(bytes);
return new BigInteger(bytes);
}
void onEntry(long snap) {
if (!allocFreeEventBuilder.isClear()) {
printerr("Seems like we missed a function exit for %s".formatted(function));
}
}
AllocFreeEvent onExit(long snap) {
return allocFreeEventBuilder.createAllocFreeEvent();
}
}
class malloc extends AllocOrFreeFunc {
malloc(Function function) {
super(function);
}
@Override
void onEntry(long snap) {
super.onEntry(snap);
allocFreeEventBuilder.setSize(
getVariableValue(function.getParameter(0), snap).longValue())
.setFunction(function)
.setSnap(snap);
}
@Override
AllocFreeEvent onExit(long snap) {
allocFreeEventBuilder.setAllocatedAddress(
toAddr(getVariableValue(function.getReturn(), snap).longValue()))
.setFunction(function);
return super.onExit(snap);
}
}
class calloc extends AllocOrFreeFunc {
calloc(Function function) {
super(function);
}
@Override
void onEntry(long snap) {
super.onEntry(snap);
allocFreeEventBuilder.setSize(
getVariableValue(function.getParameter(0), snap).longValue() *
getVariableValue(function.getParameter(1), snap).longValue())
.setFunction(function)
.setSnap(snap);
}
@Override
AllocFreeEvent onExit(long snap) {
allocFreeEventBuilder.setAllocatedAddress(
toAddr(getVariableValue(function.getReturn(), snap).longValue()))
.setFunction(function);
return super.onExit(snap);
}
}
class realloc extends AllocOrFreeFunc {
realloc(Function function) {
super(function);
}
@Override
void onEntry(long snap) {
super.onEntry(snap);
allocFreeEventBuilder.setFreedAddress(
toAddr(getVariableValue(function.getParameter(0), snap).longValue()))
.setSize(getVariableValue(function.getParameter(1), snap).longValue())
.setFunction(function)
.setSnap(snap);
}
@Override
AllocFreeEvent onExit(long snap) {
allocFreeEventBuilder.setAllocatedAddress(
toAddr(getVariableValue(function.getReturn(), snap).longValue()))
.setFunction(function);
return super.onExit(snap);
}
}
class free extends AllocOrFreeFunc {
free(Function function) {
super(function);
}
@Override
void onEntry(long snap) {
super.onEntry(snap);
allocFreeEventBuilder.setFreedAddress(
toAddr(getVariableValue(function.getParameter(0), snap).longValue()))
.setFunction(function)
.setSnap(snap);
}
// @Override
// AllocFreeEvent onExit(long snap) {
// allocFreeEventBuilder.setReturnStatus(
// getVariableValue(function.getReturn(), snap).longValue());
// return super.onExit(snap);
// }
}
@Override
protected void run() throws Exception {
AllocFreePairings pairing = new AllocFreePairings(
List.of(new malloc(getMatchingFunction("malloc")),
new calloc(getMatchingFunction("calloc")),
new realloc(getMatchingFunction("realloc")),
new free(getMatchingFunction("free"))));
Map<Address, AllocFreeEvent> allocationTracker = new HashMap<>();
List<AllocatedBuffer> allocatedBuffers = new ArrayList<>();
for (AllocFreeEvent event : pairing.getAllEvents()) {
if (event.freedAddress != null) {
if (!allocationTracker.containsKey(event.freedAddress)) {
println("Double free?? %x".formatted(event.freedAddress.getUnsignedOffset()));
}
else {
AllocFreeEvent allocEvent = allocationTracker.get(event.freedAddress);
allocatedBuffers.add(
new AllocatedBuffer(Lifespan.span(allocEvent.snap, event.snap),
allocEvent.size(), allocEvent.allocedAddress));
allocationTracker.remove(event.freedAddress);
}
}
if (event.allocedAddress != null) {
if (allocationTracker.containsKey(event.allocedAddress)) {
println("Did we miss a free?? %x".formatted(
event.allocedAddress.getUnsignedOffset()));
}
else {
allocationTracker.put(event.allocedAddress, event);
}
}
}
println("Unfreed memory: %s".formatted(allocationTracker.keySet()));
println("Allocated buffers: %s".formatted(allocatedBuffers));
}
public Function getMatchingFunction(String funcName) {
for (Program program : state.getTool()
.getService(ProgramManager.class)
.getAllOpenPrograms()) {
for (Function f : program.getFunctionManager().getFunctions(true)) {
if (!f.isThunk() && !f.isExternal() && funcName.equals(f.getName().toLowerCase())) {
return f;
}
}
}
return null;
}
}
@@ -0,0 +1,278 @@
/* ###
* 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.
*/
//EXPERIMENTAL script that emulates instructions between snapshots in a sparse trace
//@category Debugger
//@keybinding
//@menupath
//@toolbar
import java.nio.ByteBuffer;
import java.util.*;
import db.Transaction;
import ghidra.app.plugin.core.debug.service.emulation.DebuggerEmulationIntegration;
import ghidra.app.plugin.core.debug.service.emulation.Mode;
import ghidra.app.plugin.core.debug.service.emulation.data.DefaultPcodeDebuggerAccess;
import ghidra.app.script.GhidraScript;
import ghidra.debug.flatapi.FlatDebuggerAPI;
import ghidra.pcode.emu.*;
import ghidra.pcode.exec.BytesPcodeArithmetic;
import ghidra.pcode.exec.PcodeArithmetic;
import ghidra.pcode.exec.trace.TraceEmulationIntegration;
import ghidra.pcode.exec.trace.data.PcodeTraceAccess;
import ghidra.program.model.address.*;
import ghidra.program.model.lang.*;
import ghidra.program.model.pcode.PcodeOp;
import ghidra.program.model.symbol.RefType;
import ghidra.program.model.symbol.SourceType;
import ghidra.trace.database.DBTrace;
import ghidra.trace.model.Lifespan;
import ghidra.trace.model.Trace;
import ghidra.trace.model.memory.TraceMemorySpace;
import ghidra.trace.model.memory.TraceMemoryState;
import ghidra.trace.model.stack.TraceStackFrame;
import ghidra.trace.model.target.TraceObject;
import ghidra.trace.model.target.TraceObjectManager;
import ghidra.trace.model.thread.TraceThread;
import ghidra.trace.model.time.TraceSnapshot;
import ghidra.trace.model.time.schedule.TraceSchedule;
import ghidra.util.Msg;
import ghidra.util.exception.CancelledException;
import static ghidra.pcode.exec.BytesPcodeArithmetic.forLanguage;
public class EmulateGapsInTrace extends GhidraScript implements FlatDebuggerAPI {
private static class CustomWriter extends TraceEmulationIntegration.TraceWriter {
final Map<Address, Integer> readMap = new HashMap<>();
final Map<Address, Integer> writeMap = new HashMap<>();
final BytesPcodeArithmetic arithmetic;
SleighInstructionDecoder decoder = null;
public CustomWriter(PcodeTraceAccess access, Language language) {
super(access);
arithmetic = forLanguage(language);
}
@Override
public void beforeLoad(PcodeThread<Object> thread, PcodeOp op, AddressSpace space,
Object offset, int size) {
if (offset instanceof byte[] off) {
readMap.put(arithmetic.toAddress(off, space, PcodeArithmetic.Purpose.INSPECT),
size);
}
super.beforeLoad(thread, op, space, offset, size);
}
@Override
public void beforeStore(PcodeThread<Object> thread, PcodeOp op, AddressSpace space,
Object offset, int size, Object value) {
if (offset instanceof byte[] off) {
writeMap.put(arithmetic.toAddress(off, space, PcodeArithmetic.Purpose.INSPECT),
size);
}
super.beforeStore(thread, op, space, offset, size, value);
}
}
@Override
protected void run() throws Exception {
final Trace fromTrace = requireCurrentTrace();
final TraceThread fromThread = requireCurrentThread();
Iterator<? extends TraceSnapshot> iterator = fromTrace.getTimeManager()
.getSnapshots(getCurrentSnap(), true, fromTrace.getTimeManager().getMaxSnap(),
true)
.iterator();
TraceSnapshot cur = iterator.next();
Trace toTrace = new DBTrace("%s Emulated Gaps".formatted(fromTrace.getName()),
fromTrace.getBaseCompilerSpec(), this);
final TraceObjectManager om = toTrace.getObjectManager();
try (Transaction ignored = toTrace.openTransaction("Emulate the gaps");
TraceObjectManager.BypassWriteCache ignored1 = om.withoutWriteCache()) {
om.createRootObject(fromTrace.getObjectManager().getRootSchema());
monitor.initialize(fromTrace.getTimeManager().getMaxSnap() - getCurrentSnap());
while (iterator.hasNext()) {
monitor.increment();
TraceSnapshot next = iterator.next();
TraceThread toThread = copySnapshotToTrace(cur, toTrace);
if (emulateToNextSnapshot(cur, fromThread, fromTrace, toTrace, toThread, next)) {
break;
}
cur = next;
}
activateTrace(toTrace);
}
catch (Exception e) {
activateTrace(toTrace);
throw e;
}
}
private TraceThread copySnapshotToTrace(TraceSnapshot fromSnapshot, Trace toTrace) {
TraceSnapshot snapshot =
toTrace.getTimeManager().createSnapshot(fromSnapshot.getDescription());
Trace fromTrace = fromSnapshot.getTrace();
TraceThread fromThread = fromSnapshot.getEventThread();
fromTrace.getObjectManager()
.getAllObjects()
.filter(o -> o.getLife().contains(fromSnapshot.getKey()) && !o.isRoot())
.forEachOrdered(fromTraceObject -> {
TraceObject toTraceObject = toTrace.getObjectManager()
.createObject(fromTraceObject.getCanonicalPath());
toTraceObject.insert(Lifespan.nowOn(snapshot.getKey()),
TraceObject.ConflictResolution.DENY);
fromTraceObject.getValues(Lifespan.at(fromSnapshot.getKey()))
.forEach(fromTraceObjectValue -> {
if (!(fromTraceObjectValue.getValue() instanceof TraceObject)) {
toTraceObject.setValue(Lifespan.nowOn(snapshot.getKey()),
fromTraceObjectValue.getEntryKey(),
fromTraceObjectValue.getValue());
}
});
});
TraceThread thread = toTrace.getThreadManager().getThread(fromThread.getKey());
snapshot.setEventThread(thread);
for (AddressRange addressRange : fromTrace.getMemoryManager()
.getAddressesWithState(fromSnapshot.getKey(),
s -> s != TraceMemoryState.UNKNOWN)) {
ByteBuffer buffer = ByteBuffer.allocate((int) addressRange.getLength());
fromTrace.getMemoryManager()
.getBytes(fromSnapshot.getKey(), addressRange.getMinAddress(), buffer);
buffer.flip();
if (addressRange.getMinAddress().isRegisterAddress()) {
Address regAddress = toTrace.getMemoryManager()
.getMemoryRegisterSpace(thread, true)
.getAddressSpace()
.getAddress(addressRange.getMinAddress().getOffset());
toTrace.getMemoryManager()
.getMemoryRegisterSpace(thread, true)
.putBytes(snapshot.getKey(), regAddress, buffer);
}
else {
toTrace.getMemoryManager()
.putBytes(snapshot.getKey(), toTrace.getBaseAddressFactory()
.getDefaultAddressSpace()
.getAddress(addressRange.getMinAddress().getOffset()), buffer);
}
}
return thread;
}
private boolean emulateToNextSnapshot(TraceSnapshot curFromSnapshot, TraceThread fromThread,
Trace fromTrace, Trace toTrace, TraceThread toThread, TraceSnapshot nextFromSnapshot) {
Register programCounter = fromTrace.getBaseLanguage().getProgramCounter();
TraceMemorySpace registerSpace =
fromTrace.getMemoryManager().getMemoryRegisterSpace(fromThread, false);
RegisterValue endPCValue =
registerSpace.getViewValue(nextFromSnapshot.getKey(), programCounter);
int tickCount = 1;
TraceSchedule schedule = TraceSchedule.snap(curFromSnapshot.getKey())
.steppedForward(fromThread, tickCount++);
// TraceEmulationIntegration.Writer writer =
// DebuggerEmulationIntegration.bytesDelayedWriteTrace(
// new DefaultPcodeDebuggerAccess(getState().getTool(), null,
// fromTrace.getPlatformManager().getHostPlatform(),
// curFromSnapshot.getKey()));
var writer = new CustomWriter(new DefaultPcodeDebuggerAccess(getState().getTool(), null,
fromTrace.getPlatformManager().getHostPlatform(), curFromSnapshot.getKey()),
fromTrace.getBaseLanguage());
writer.putHandler(new DebuggerEmulationIntegration.TargetBytesPieceHandler(Mode.RO));
PcodeEmulator emulator = new PcodeEmulator(fromTrace.getBaseLanguage(), writer.callbacks());
PcodeThread<byte[]> pcodeThread = emulator.getThread(fromThread.getPath(), true);
// ThreadPcodeExecutorState<byte[]> state = pcodeThread.getState();
// for (Register register : fromTrace.getBaseLanguage().getRegisters()) {
// if (!register.isBaseRegister()) {
// continue;
// }
// RegisterValue value = fromTrace.getMemoryManager()
// .getMemoryRegisterSpace(fromThread, true)
// .getValue(curFromSnapshot.getKey(), register);
// state.setRegisterValue(register, value);
// }
try {
while (true) {
monitor.checkCancelled();
pcodeThread.stepInstruction();
if (pcodeThread.getCounter().getOffset() ==
endPCValue.getUnsignedValue().longValue()) {
break;
}
TraceSnapshot snapshot =
toTrace.getTimeManager().createSnapshot("Emulated " + schedule);
Msg.info(this, "Snapshot %d".formatted(snapshot.getKey()));
snapshot.setSchedule(schedule);
snapshot.setEventThread(toThread);
TraceStackFrame frame = toTrace.getStackManager()
.getStack(toThread, snapshot.getKey(), true)
.getFrame(snapshot.getKey(), 0, true);
frame.setProgramCounter(Lifespan.nowOn(snapshot.getKey()),
pcodeThread.getCounter());
writer.writeDown(new DefaultPcodeDebuggerAccess(getState().getTool(), null,
toTrace.getPlatformManager().getHostPlatform(), snapshot.getKey()));
for (Map.Entry<Address, Integer> entry : writer.readMap.entrySet()) {
AddressRangeImpl range = new AddressRangeImpl(entry.getKey(),
entry.getValue());
toTrace.getReferenceManager()
.addMemoryReference(Lifespan.at(snapshot.getKey() - 1),
addr(toTrace, pcodeThread.getCounter().getOffset()), range,
RefType.READ, SourceType.USER_DEFINED, -1);
}
for (Map.Entry<Address, Integer> entry : writer.writeMap.entrySet()) {
AddressRangeImpl range = new AddressRangeImpl(entry.getKey(),
entry.getValue());
toTrace.getReferenceManager()
.addMemoryReference(Lifespan.at(snapshot.getKey() - 1),
addr(toTrace, pcodeThread.getCounter().getOffset()), range,
RefType.WRITE, SourceType.USER_DEFINED, -1);
}
writer.readMap.clear();
writer.writeMap.clear();
schedule = TraceSchedule.snap(curFromSnapshot.getKey())
.steppedForward(fromThread, tickCount++);
}
}
catch (CancelledException _) {
return true;
}
catch (Exception e) {
printerr(e.getMessage());
}
return false;
}
Address addr(Trace trace, long addr) {
return trace.getBaseAddressFactory().getDefaultAddressSpace().getAddress(addr);
}
}