GP-5523: Allow tool-wide configuration of radix for displaying trace times.

This commit is contained in:
Dan 2025-04-17 18:18:53 +00:00
parent 92e2b6b5d4
commit 004712026b
38 changed files with 751 additions and 246 deletions

View file

@ -27,6 +27,7 @@ import ghidra.trace.model.thread.TraceObjectThread;
import ghidra.trace.model.thread.TraceThread;
import ghidra.trace.model.time.TraceSnapshot;
import ghidra.trace.model.time.schedule.TraceSchedule;
import ghidra.trace.model.time.schedule.TraceSchedule.TimeRadix;
import ghidra.util.LockHold;
import ghidra.util.Msg;
import ghidra.util.database.*;
@ -85,7 +86,7 @@ public class DBTraceSnapshot extends DBAnnotatedObject implements TraceSnapshot
eventThread = manager.threadManager.getThread(threadKey);
if (!"".equals(scheduleStr)) {
try {
schedule = TraceSchedule.parse(scheduleStr);
schedule = TraceSchedule.parse(scheduleStr, TimeRadix.DEC);
}
catch (IllegalArgumentException e) {
Msg.error(this, "Could not parse schedule: " + schedule, e);
@ -205,7 +206,7 @@ public class DBTraceSnapshot extends DBAnnotatedObject implements TraceSnapshot
public void setSchedule(TraceSchedule schedule) {
try (LockHold hold = LockHold.lock(manager.lock.writeLock())) {
this.schedule = schedule;
this.scheduleStr = schedule == null ? "" : schedule.toString();
this.scheduleStr = schedule == null ? "" : schedule.toString(TimeRadix.DEC);
update(SCHEDULE_COLUMN);
manager.notifySnapshotChanged(this);
}

View file

@ -25,10 +25,14 @@ import db.DBHandle;
import ghidra.framework.data.OpenMode;
import ghidra.trace.database.DBTrace;
import ghidra.trace.database.DBTraceManager;
import ghidra.trace.database.target.DBTraceObject;
import ghidra.trace.database.thread.DBTraceThreadManager;
import ghidra.trace.model.Lifespan;
import ghidra.trace.model.target.TraceObjectValue;
import ghidra.trace.model.time.TraceSnapshot;
import ghidra.trace.model.time.TraceTimeManager;
import ghidra.trace.model.time.schedule.TraceSchedule;
import ghidra.trace.model.time.schedule.TraceSchedule.TimeRadix;
import ghidra.trace.util.TraceChangeRecord;
import ghidra.trace.util.TraceEvents;
import ghidra.util.LockHold;
@ -181,4 +185,34 @@ public class DBTraceTimeManager implements TraceTimeManager, DBTraceManager {
notifySnapshotDeleted(snapshot);
}
}
@Override
public void setTimeRadix(TimeRadix radix) {
DBTraceObject root = trace.getObjectManager().getRootObject();
if (root == null) {
throw new IllegalStateException(
"There must be a root object in the ObjectManager before setting the TimeRadix");
}
root.setAttribute(Lifespan.ALL, KEY_TIME_RADIX, switch (radix) {
case DEC -> "dec";
case HEX_UPPER -> "HEX";
case HEX_LOWER -> "hex";
});
}
@Override
public TimeRadix getTimeRadix() {
DBTraceObject root = trace.getObjectManager().getRootObject();
if (root == null) {
return TimeRadix.DEFAULT;
}
TraceObjectValue attribute = root.getAttribute(0, KEY_TIME_RADIX);
if (attribute == null) {
return TimeRadix.DEFAULT;
}
return switch (attribute.getValue()) {
case String s -> TimeRadix.fromStr(s);
default -> TimeRadix.DEFAULT;
};
}
}

View file

@ -281,7 +281,7 @@ public sealed interface Lifespan extends Span<Long, Lifespan>, Iterable<Long> {
@Override
public String toString() {
return doToString();
return toString(DOMAIN::toString);
}
@Override
@ -329,7 +329,7 @@ public sealed interface Lifespan extends Span<Long, Lifespan>, Iterable<Long> {
public record Impl(long lmin, long lmax) implements Lifespan {
@Override
public String toString() {
return doToString();
return toString(DOMAIN::toString);
}
@Override

View file

@ -18,8 +18,12 @@ package ghidra.trace.model.time;
import java.util.Collection;
import ghidra.trace.model.time.schedule.TraceSchedule;
import ghidra.trace.model.time.schedule.TraceSchedule.TimeRadix;
public interface TraceTimeManager {
/** The attribute key for controlling the time radix */
String KEY_TIME_RADIX = "_time_radix";
/**
* Create a new snapshot after the latest
*
@ -32,6 +36,7 @@ public interface TraceTimeManager {
* Get the snapshot with the given key, optionally creating it
*
* @param snap the snapshot key
* @param createIfAbsent create the snapshot if it's missing
* @return the snapshot or {@code null}
*/
TraceSnapshot getSnapshot(long snap, boolean createIfAbsent);
@ -105,4 +110,24 @@ public interface TraceTimeManager {
* @return the count
*/
long getSnapshotCount();
/**
* Set the radix for displaying and parsing time (snapshots and step counts)
*
* <p>
* This only affects the GUI, but storing it in the trace gives the back end a means of
* controlling it.
*
* @param radix the radix
*/
void setTimeRadix(TimeRadix radix);
/**
* Get the radix for displaying and parsing time (snapshots and step counts)
*
* @see #setTimeRadix(TimeRadix)
* @return radix the radix
*/
TimeRadix getTimeRadix();
}

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -18,6 +18,7 @@ package ghidra.trace.model.time.schedule;
import java.util.List;
import ghidra.program.model.lang.Language;
import ghidra.trace.model.time.schedule.TraceSchedule.TimeRadix;
public abstract class AbstractStep implements Step {
protected final long threadKey;
@ -34,16 +35,22 @@ public abstract class AbstractStep implements Step {
/**
* Return the step portion of {@link #toString()}
*
* @param radix the radix
* @return the string
*/
protected abstract String toStringStepPart();
protected abstract String toStringStepPart(TimeRadix radix);
@Override
public String toString() {
return toString(TimeRadix.DEFAULT);
}
@Override
public String toString(TimeRadix radix) {
if (threadKey == -1) {
return toStringStepPart();
return toStringStepPart(radix);
}
return String.format("t%d-", threadKey) + toStringStepPart();
return String.format("t%d-%s", threadKey, toStringStepPart(radix));
}
@Override

View file

@ -34,6 +34,7 @@ import ghidra.program.model.lang.Language;
import ghidra.program.model.lang.Register;
import ghidra.program.model.pcode.PcodeOp;
import ghidra.program.model.pcode.Varnode;
import ghidra.trace.model.time.schedule.TraceSchedule.TimeRadix;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
@ -108,7 +109,7 @@ public class PatchStep implements Step {
protected static void generateSleigh(List<String> result, Language language, Address address,
byte[] data) {
SemisparseByteArray array = new SemisparseByteArray(); // TODO: Seems heavy-handed
SemisparseByteArray array = new SemisparseByteArray(); // Seems heavy-handed
array.putData(address.getOffset(), data);
generateSleigh(result, language, address.getAddressSpace(), array);
}
@ -190,7 +191,7 @@ public class PatchStep implements Step {
}
public static PatchStep parse(long threadKey, String stepSpec) {
// TODO: Can I parse and validate the sleigh here?
// Would be nice to parse and validate the sleigh here, but need a language.
if (!stepSpec.startsWith("{") || !stepSpec.endsWith("}")) {
throw new IllegalArgumentException("Cannot parse step: '" + stepSpec + "'");
}
@ -200,7 +201,7 @@ public class PatchStep implements Step {
public PatchStep(long threadKey, String sleigh) {
this.threadKey = threadKey;
this.sleigh = Objects.requireNonNull(sleigh);
this.hashCode = Objects.hash(threadKey, sleigh); // TODO: May become mutable
this.hashCode = Objects.hash(threadKey, sleigh);
}
private void setSleigh(String sleigh) {
@ -233,6 +234,11 @@ public class PatchStep implements Step {
@Override
public String toString() {
return toString(TimeRadix.DEFAULT);
}
@Override
public String toString(TimeRadix radix) {
if (threadKey == -1) {
return "{" + sleigh + "}";
}
@ -251,7 +257,6 @@ public class PatchStep implements Step {
@Override
public boolean isNop() {
// TODO: If parsing beforehand, base on number of ops
return sleigh.length() == 0;
}
@ -272,7 +277,7 @@ public class PatchStep implements Step {
@Override
public boolean isCompatible(Step step) {
// TODO: Can we combine ops?
// Can we combine ops?
return false; // For now, never combine sleigh steps
}
@ -314,7 +319,7 @@ public class PatchStep implements Step {
return result;
}
// TODO: Compare ops, if/when we pre-compile
// Compare ops, if/when we pre-compile?
result = CompareResult.unrelated(this.sleigh.compareTo(that.sleigh));
if (result != CompareResult.EQUALS) {
return result;

View file

@ -18,14 +18,13 @@ package ghidra.trace.model.time.schedule;
import java.util.*;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import ghidra.pcode.emu.PcodeMachine;
import ghidra.pcode.emu.PcodeThread;
import ghidra.program.model.lang.Language;
import ghidra.trace.model.Trace;
import ghidra.trace.model.thread.TraceThread;
import ghidra.trace.model.thread.TraceThreadManager;
import ghidra.trace.model.time.schedule.TraceSchedule.TimeRadix;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
@ -33,27 +32,28 @@ import ghidra.util.task.TaskMonitor;
* A sequence of thread steps, each repeated some number of times
*/
public class Sequence implements Comparable<Sequence> {
public static final String SEP = ";";
private static final String SEP = ";";
/**
* Parse (and normalize) a sequence of steps
*
* <p>
* This takes a semicolon-separated list of steps in the form specified by
* {@link Step#parse(String)}. Each step may or may not specify a thread, but it's uncommon for
* any but the first step to omit the thread. The sequence is normalized as it is parsed, so any
* step after the first that omits a thread will be combined with the previous step. When the
* first step applies to the "last thread," it typically means the "event thread" of the source
* trace snapshot.
* {@link Step#parse(String, TimeRadix)}. Each step may or may not specify a thread, but it's
* uncommon for any but the first step to omit the thread. The sequence is normalized as it is
* parsed, so any step after the first that omits a thread will be combined with the previous
* step. When the first step applies to the "last thread," it typically means the "event thread"
* of the source trace snapshot.
*
* @param seqSpec the string specification of the sequence
* @param radix the radix
* @return the parsed sequence
* @throws IllegalArgumentException if the specification is of the wrong form
*/
public static Sequence parse(String seqSpec) {
public static Sequence parse(String seqSpec, TimeRadix radix) {
Sequence result = new Sequence();
for (String stepSpec : seqSpec.split(SEP)) {
Step step = Step.parse(stepSpec);
Step step = Step.parse(stepSpec, radix);
result.advance(step);
}
return result;
@ -109,7 +109,11 @@ public class Sequence implements Comparable<Sequence> {
@Override
public String toString() {
return StringUtils.join(steps, SEP);
return toString(TimeRadix.DEFAULT);
}
public String toString(TimeRadix radix) {
return steps.stream().map(s -> s.toString(radix)).collect(Collectors.joining(SEP));
}
/**

View file

@ -16,17 +16,18 @@
package ghidra.trace.model.time.schedule;
import ghidra.pcode.emu.PcodeThread;
import ghidra.trace.model.time.schedule.TraceSchedule.TimeRadix;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
public class SkipStep extends AbstractStep {
public static SkipStep parse(long threadKey, String stepSpec) {
public static SkipStep parse(long threadKey, String stepSpec, TimeRadix radix) {
if (!stepSpec.startsWith("s")) {
throw new IllegalArgumentException("Cannot parse skip step: '" + stepSpec + "'");
}
try {
return new SkipStep(threadKey, Long.parseLong(stepSpec.substring(1)));
return new SkipStep(threadKey, radix.decode(stepSpec.substring(1)));
}
catch (NumberFormatException e) {
throw new IllegalArgumentException("Cannot parse skip step: '" + stepSpec + "'");
@ -54,8 +55,8 @@ public class SkipStep extends AbstractStep {
}
@Override
protected String toStringStepPart() {
return String.format("s%d", tickCount);
protected String toStringStepPart(TimeRadix radix) {
return "s" + radix.format(tickCount);
}
@Override

View file

@ -22,6 +22,7 @@ import ghidra.pcode.emu.PcodeThread;
import ghidra.program.model.lang.Language;
import ghidra.trace.model.thread.TraceThread;
import ghidra.trace.model.thread.TraceThreadManager;
import ghidra.trace.model.time.schedule.TraceSchedule.TimeRadix;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
@ -40,21 +41,22 @@ public interface Step extends Comparable<Step> {
* applies to the last thread or the event thread.
*
* @param stepSpec the string specification
* @param radix the radix
* @return the parsed step
* @throws IllegalArgumentException if the specification is of the wrong form
*/
static Step parse(String stepSpec) {
static Step parse(String stepSpec, TimeRadix radix) {
if ("".equals(stepSpec)) {
return nop();
}
String[] parts = stepSpec.split("-");
if (parts.length == 1) {
return parse(-1, parts[0].trim());
return parse(-1, parts[0].trim(), radix);
}
if (parts.length == 2) {
String tPart = parts[0].trim();
if (tPart.startsWith("t")) {
return parse(Long.parseLong(tPart.substring(1)), parts[1].trim());
return parse(Long.parseLong(tPart.substring(1)), parts[1].trim(), radix);
}
}
throw new IllegalArgumentException("Cannot parse step: '" + stepSpec + "'");
@ -70,23 +72,26 @@ public interface Step extends Comparable<Step> {
*
* @param threadKey the thread to step, or -1 for the last thread or event thread
* @param stepSpec the string specification
* @param radix the radix
* @return the parsed step
* @throws IllegalArgumentException if the specification is of the wrong form
*/
static Step parse(long threadKey, String stepSpec) {
static Step parse(long threadKey, String stepSpec, TimeRadix radix) {
if (stepSpec.startsWith("s")) {
return SkipStep.parse(threadKey, stepSpec);
return SkipStep.parse(threadKey, stepSpec, radix);
}
if (stepSpec.startsWith("{")) {
return PatchStep.parse(threadKey, stepSpec);
}
return TickStep.parse(threadKey, stepSpec);
return TickStep.parse(threadKey, stepSpec, radix);
}
static TickStep nop() {
return new TickStep(-1, 0);
}
String toString(TimeRadix radix);
StepType getType();
default int getTypeOrder() {

View file

@ -16,6 +16,7 @@
package ghidra.trace.model.time.schedule;
import ghidra.pcode.emu.PcodeThread;
import ghidra.trace.model.time.schedule.TraceSchedule.TimeRadix;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
@ -24,9 +25,9 @@ import ghidra.util.task.TaskMonitor;
*/
public class TickStep extends AbstractStep {
public static TickStep parse(long threadKey, String stepSpec) {
public static TickStep parse(long threadKey, String stepSpec, TimeRadix radix) {
try {
return new TickStep(threadKey, Long.parseLong(stepSpec));
return new TickStep(threadKey, radix.decode(stepSpec));
}
catch (NumberFormatException e) {
throw new IllegalArgumentException("Cannot parse tick step: '" + stepSpec + "'");
@ -54,8 +55,8 @@ public class TickStep extends AbstractStep {
}
@Override
protected String toStringStepPart() {
return Long.toString(tickCount);
protected String toStringStepPart(TimeRadix radix) {
return radix.format(tickCount);
}
@Override

View file

@ -35,6 +35,62 @@ public class TraceSchedule implements Comparable<TraceSchedule> {
*/
public static final TraceSchedule ZERO = TraceSchedule.snap(0);
/**
* Format for rendering and parsing snaps and step counts
*/
public enum TimeRadix {
/** Use decimal (default) */
DEC("dec", 10, "%d"),
/** Use upper-case hexadecimal */
HEX_UPPER("HEX", 16, "%X"),
/** Use lower-case hexadecimal */
HEX_LOWER("hex", 16, "%x");
/** The default radix (decimal) */
public static final TimeRadix DEFAULT = DEC;
/**
* Get the radix specified by the given string
*
* @param s the name of the specified radix
* @return the radix
*/
public static TimeRadix fromStr(String s) {
return switch (s) {
case "dec" -> DEC;
case "HEX" -> HEX_UPPER;
case "hex" -> HEX_LOWER;
default -> DEFAULT;
};
}
public final String name;
public final int n;
public final String fmt;
private TimeRadix(String name, int n, String fmt) {
this.name = name;
this.n = n;
this.fmt = fmt;
}
public String format(long time) {
return fmt.formatted(time);
}
public long decode(String nm) {
if (nm.startsWith("0x") || nm.startsWith("0X") ||
nm.startsWith("-0x") || nm.startsWith("-0X")) {
return Long.parseLong(nm, 16);
}
if (nm.startsWith("0n") || nm.startsWith("0N") ||
nm.startsWith("-0n") || nm.startsWith("-0N")) {
return Long.parseLong(nm, 10);
}
return Long.parseLong(nm, n);
}
}
/**
* Specifies forms of a stepping schedule.
*
@ -196,14 +252,15 @@ public class TraceSchedule implements Comparable<TraceSchedule> {
* <p>
* A schedule consists of a snap, a optional {@link Sequence} of thread instruction-level steps,
* and optional p-code-level steps (pSteps). The form of {@code steps} and {@code pSteps} is
* specified by {@link Sequence#parse(String)}. Each sequence consists of stepping selected
* threads forward, and/or patching machine state.
* specified by {@link Sequence#parse(String, TimeRadix)}. Each sequence consists of stepping
* selected threads forward, and/or patching machine state.
*
* @param spec the string specification
* @param source the presumed source of the schedule
* @param radix the radix
* @return the parsed schedule
*/
public static TraceSchedule parse(String spec, Source source) {
public static TraceSchedule parse(String spec, Source source, TimeRadix radix) {
String[] parts = spec.split(":", 2);
if (parts.length > 2) {
throw new AssertionError();
@ -212,7 +269,7 @@ public class TraceSchedule implements Comparable<TraceSchedule> {
final Sequence ticks;
final Sequence pTicks;
try {
snap = Long.decode(parts[0]);
snap = radix.decode(parts[0]);
}
catch (NumberFormatException e) {
throw new IllegalArgumentException(PARSE_ERR_MSG, e);
@ -220,7 +277,7 @@ public class TraceSchedule implements Comparable<TraceSchedule> {
if (parts.length > 1) {
String[] subs = parts[1].split("\\.");
try {
ticks = Sequence.parse(subs[0]);
ticks = Sequence.parse(subs[0], radix);
}
catch (IllegalArgumentException e) {
throw new IllegalArgumentException(PARSE_ERR_MSG, e);
@ -230,7 +287,7 @@ public class TraceSchedule implements Comparable<TraceSchedule> {
}
else if (subs.length == 2) {
try {
pTicks = Sequence.parse(subs[1]);
pTicks = Sequence.parse(subs[1], radix);
}
catch (IllegalArgumentException e) {
throw new IllegalArgumentException(PARSE_ERR_MSG, e);
@ -248,13 +305,24 @@ public class TraceSchedule implements Comparable<TraceSchedule> {
}
/**
* As in {@link #parse(String, Source)}, but assumed abnormal
* As in {@link #parse(String, Source, TimeRadix)}, but assumed abnormal
*
* @param spec the string specification
* @param radix the radix
* @return the parsed schedule
*/
public static TraceSchedule parse(String spec, TimeRadix radix) {
return parse(spec, Source.INPUT, radix);
}
/**
* As in {@link #parse(String, TimeRadix)}, but with the {@link TimeRadix#DEFAULT} radix.
*
* @param spec the string specification
* @return the parse sequence
*/
public static TraceSchedule parse(String spec) {
return parse(spec, Source.INPUT);
return parse(spec, TimeRadix.DEFAULT);
}
public enum Source {
@ -318,13 +386,18 @@ public class TraceSchedule implements Comparable<TraceSchedule> {
@Override
public String toString() {
return toString(TimeRadix.DEFAULT);
}
public String toString(TimeRadix radix) {
if (pSteps.isNop()) {
if (steps.isNop()) {
return Long.toString(snap);
return radix.format(snap);
}
return String.format("%d:%s", snap, steps);
return String.format("%s:%s", radix.format(snap), steps.toString(radix));
}
return String.format("%d:%s.%s", snap, steps, pSteps);
return String.format("%s:%s.%s", radix.format(snap), steps.toString(radix),
pSteps.toString(radix));
}
/**

View file

@ -30,6 +30,7 @@ import ghidra.test.AbstractGhidraHeadlessIntegrationTest;
import ghidra.test.ToyProgramBuilder;
import ghidra.trace.database.ToyDBTraceBuilder;
import ghidra.trace.model.thread.TraceThread;
import ghidra.trace.model.time.schedule.TraceSchedule.TimeRadix;
import ghidra.util.task.TaskMonitor;
public class TraceScheduleTest extends AbstractGhidraHeadlessIntegrationTest {
@ -161,7 +162,7 @@ public class TraceScheduleTest extends AbstractGhidraHeadlessIntegrationTest {
@Test
public void testRewind() {
Sequence seq = Sequence.parse("10;t1-20;t2-30");
Sequence seq = Sequence.parse("10;t1-20;t2-30", TimeRadix.DEC);
assertEquals(0, seq.rewind(5));
assertEquals("10;t1-20;t2-25", seq.toString());
@ -181,7 +182,7 @@ public class TraceScheduleTest extends AbstractGhidraHeadlessIntegrationTest {
@Test(expected = IllegalArgumentException.class)
public void testRewindNegativeErr() {
Sequence seq = Sequence.parse("10;t1-20;t2-30");
Sequence seq = Sequence.parse("10;t1-20;t2-30", TimeRadix.DEC);
seq.rewind(-1);
}
@ -251,7 +252,8 @@ public class TraceScheduleTest extends AbstractGhidraHeadlessIntegrationTest {
}
public String strRelativize(String fromSpec, String toSpec) {
Sequence seq = Sequence.parse(toSpec).relativize(Sequence.parse(fromSpec));
Sequence seq = Sequence.parse(toSpec, TimeRadix.DEC)
.relativize(Sequence.parse(fromSpec, TimeRadix.DEC));
return seq == null ? null : seq.toString();
}
@ -503,4 +505,13 @@ public class TraceScheduleTest extends AbstractGhidraHeadlessIntegrationTest {
assertFalse(
TraceSchedule.parse("1:1.1;{r0=1}").differsOnlyByPatch(TraceSchedule.parse("1:1.1")));
}
@Test
public void testTimeRadix() throws Exception {
TraceSchedule schedule = TraceSchedule.parse("A:t10-B.C", TimeRadix.HEX_UPPER);
assertEquals("10:t10-11.12", schedule.toString(TimeRadix.DEC));
assertEquals("A:t10-B.C", schedule.toString(TimeRadix.HEX_UPPER));
assertEquals("a:t10-b.c", schedule.toString(TimeRadix.HEX_LOWER));
}
}