Merge remote-tracking branch 'origin/debugger'

This commit is contained in:
ghidra1 2021-04-23 10:45:33 -04:00
commit ac85e4efbb
200 changed files with 5446 additions and 1424 deletions

View file

@ -742,9 +742,8 @@ public enum DebugModelConventions {
this.name = name;
this.obj = obj;
obj.addListener(this);
obj.fetchAttribute(name).thenAccept(t -> {
set((T) t, null);
}).exceptionally(ex -> {
set((T) obj.getCachedAttribute(name), null);
obj.fetchAttribute(name).exceptionally(ex -> {
Msg.error(this, "Could not get initial value of " + name + " for " + obj, ex);
return null;
});

View file

@ -342,22 +342,6 @@ public interface DebuggerObjectModel {
* are refreshed; and {@code A}'s, {@code B[1]}'s, and {@code C[2]}'s attribute caches are
* refreshed.
*
* @implNote The returned value cannot be a {@link TargetObjectRef} unless the value represents
* a link. In other words, if the path refers to an object, the model must return the
* object, not a ref. When the value is a link, the implementation may optionally
* resolve the object, but should only do so if it doesn't incur a significant cost.
* Furthermore, such links cannot be resolved -- though they can be substituted for
* the target object at the linked path. In other words, the path of the returned ref
* (or object) must represent the link's target. Suppose {@code A[1]} is a link to
* {@code B[1]}, which is in turn a link to {@code C[1]} -- honestly, linked links
* ought to be a rare occurrence -- then fetching {@code A[1]} must return a ref to
* {@code B[1]}. It must not return {@code C[1]} nor a ref to it. The reason deals
* with caching and updates. If a request for {@code A[1]} were to return
* {@code C[1]}, a client may cache that result. Suppose that client then observes a
* change causing {@code B[1]} to link to {@code C[2]}. This implies that {@code A[1]}
* now resolves to {@code C[2]}; however, the client has not received enough
* information to update or invalidate its cache.
*
* @param path the path
* @param refresh true to refresh caches
* @return the found value, or {@code null} if it does not exist

View file

@ -1,44 +0,0 @@
/* ###
* 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;
import ghidra.util.classfinder.ExtensionPointProperties;
/**
* A factory for a local debugger model
*
* <p>
* These factories are searched when attempting to create a new default debug model targeting the
* local environment.
*/
public interface LocalDebuggerModelFactory extends DebuggerModelFactory {
/**
* Get the priority of this factory
*
* <p>
* In the event multiple compatible factories are discovered, the one with the highest priority
* is selected, breaking ties arbitrarily.
*
* <p>
* The default implementation returns the priority given by {@link ExtensionPointProperties}. If
* the priority must be determined dynamically, then override this implementation.
*
* @return the priority, where lower values indicate higher priority.
*/
default int getPriority() {
return ExtensionPointProperties.Util.getPriority(getClass());
}
}

View file

@ -218,7 +218,7 @@ public abstract class AbstractDebuggerObjectModel implements SpiDebuggerObjectMo
public void removeExisting(List<String> path) {
TargetObject existing = getModelObject(path);
// It had better be. This also checks for null
// It's best if the implementation has already removed it, but just in case....
if (existing == null) {
return;
}
@ -230,15 +230,27 @@ public abstract class AbstractDebuggerObjectModel implements SpiDebuggerObjectMo
if (!path.equals(existing.getPath())) {
return; // Is a link
}
if (parent instanceof DefaultTargetObject<?, ?>) { // It had better be
DefaultTargetObject<?, ?> dtoParent = (DefaultTargetObject<?, ?>) parent;
if (PathUtils.isIndex(path)) {
dtoParent.changeElements(List.of(PathUtils.getIndex(path)), List.of(), "Replaced");
}
else {
assert PathUtils.isName(path);
dtoParent.changeAttributes(List.of(PathUtils.getKey(path)), Map.of(), "Replaced");
}
if (!(parent instanceof SpiTargetObject)) { // It had better be
Msg.error(this, "Could not remove existing object " + existing +
", because parent is not an SpiTargetObject");
return;
}
SpiTargetObject spiParent = (SpiTargetObject) parent;
SpiTargetObject delegate = spiParent.getDelegate();
if (!(delegate instanceof DefaultTargetObject<?, ?>)) { // It had better be :)
Msg.error(this, "Could not remove existing object " + existing +
", because its parent's delegate is not a DefaultTargetObject");
return;
}
DefaultTargetObject<?, ?> dtoParent = (DefaultTargetObject<?, ?>) delegate;
if (PathUtils.isIndex(path)) {
dtoParent.changeElements(List.of(PathUtils.getIndex(path)), List.of(),
"Replaced");
}
else {
assert PathUtils.isName(path);
dtoParent.changeAttributes(List.of(PathUtils.getKey(path)), Map.of(),
"Replaced");
}
}

View file

@ -319,9 +319,9 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
Delta<E, E> delta;
synchronized (model.lock) {
delta = Delta.computeAndSet(this.elements, elements, Delta.SAME);
getSchema().validateElementDelta(getPath(), delta, enforcesStrictSchema());
doInvalidateElements(delta.removed, reason);
}
getSchema().validateElementDelta(getPath(), delta, enforcesStrictSchema());
doInvalidateElements(delta.removed, reason);
if (!delta.isEmpty()) {
updateCallbackElements(delta);
listeners.fire.elementsChanged(getProxy(), delta.getKeysRemoved(), delta.added);
@ -361,9 +361,9 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
Delta<E, E> delta;
synchronized (model.lock) {
delta = Delta.apply(this.elements, remove, add, Delta.SAME);
getSchema().validateElementDelta(getPath(), delta, enforcesStrictSchema());
doInvalidateElements(delta.removed, reason);
}
getSchema().validateElementDelta(getPath(), delta, enforcesStrictSchema());
doInvalidateElements(delta.removed, reason);
if (!delta.isEmpty()) {
updateCallbackElements(delta);
listeners.fire.elementsChanged(getProxy(), delta.getKeysRemoved(), delta.added);
@ -497,9 +497,9 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
Delta<Object, ?> delta;
synchronized (model.lock) {
delta = Delta.computeAndSet(this.attributes, attributes, Delta.EQUAL);
getSchema().validateAttributeDelta(getPath(), delta, enforcesStrictSchema());
doInvalidateAttributes(delta.removed, reason);
}
getSchema().validateAttributeDelta(getPath(), delta, enforcesStrictSchema());
doInvalidateAttributes(delta.removed, reason);
if (!delta.isEmpty()) {
updateCallbackAttributes(delta);
listeners.fire.attributesChanged(getProxy(), delta.getKeysRemoved(), delta.added);
@ -556,9 +556,9 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
Delta<Object, ?> delta;
synchronized (model.lock) {
delta = Delta.apply(this.attributes, remove, add, Delta.EQUAL);
getSchema().validateAttributeDelta(getPath(), delta, enforcesStrictSchema());
doInvalidateAttributes(delta.removed, reason);
}
getSchema().validateAttributeDelta(getPath(), delta, enforcesStrictSchema());
doInvalidateAttributes(delta.removed, reason);
if (!delta.isEmpty()/* && !reason.equals("Default")*/) {
updateCallbackAttributes(delta);
listeners.fire.attributesChanged(getProxy(), delta.getKeysRemoved(), delta.added);

View file

@ -26,4 +26,13 @@ public interface SpiTargetObject extends TargetObject, InvalidatableTargetObject
//Map<String, ? extends SpiTargetObject> getCachedElements();
boolean enforcesStrictSchema();
/**
* If this internal implementation is a proxy, get its delegate
*
* @return the delegate, or this same object
*/
default SpiTargetObject getDelegate() {
return this;
}
}

View file

@ -22,6 +22,7 @@ import ghidra.dbg.DebuggerTargetObjectIface;
/**
* An object made active
*
* <p>
* "Active" here describes which object in a given class the target should operate on
*/
@DebuggerTargetObjectIface("ActiveScope")

View file

@ -98,6 +98,7 @@ public interface TargetLauncher extends TargetObject {
public CmdLineParser(Reader r) {
super(r);
resetSyntax();
wordChars(0, 255);
whitespaceChars(' ', ' ');
quoteChar('"');

View file

@ -194,4 +194,40 @@ public class PathPattern implements PathPredicates {
}
return new PathPattern(result);
}
/**
* If the given path matches, extract indices where matched by wildcards
*
* <p>
* This is essentially the inverse of {@link #applyIndices(List)}, but can only be asked of one
* pattern. The keys are returned from left to right, in the order matched by the pattern. Only
* those keys matched by a wildcard are included in the result. Indices are extracted with the
* brackets {@code []} removed.
*
* @param path the path to match
* @return the list of matched indices or {@code null} if not matched
*/
public List<String> matchIndices(List<String> path) {
int length = pattern.size();
if (length != path.size()) {
return null;
}
List<String> result = new ArrayList<>();
for (int i = 0; i < length; i++) {
String pat = pattern.get(i);
String key = path.get(i);
if (!keyMatches(pat, key)) {
return null;
}
if (isWildcard(pat)) {
if (PathUtils.isIndex(pat)) {
result.add(PathUtils.parseIndex(key));
}
else {
result.add(key);
}
}
}
return result;
}
}

View file

@ -1,44 +0,0 @@
/* ###
* 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.util.Deque;
import java.util.LinkedList;
import java.util.concurrent.CompletableFuture;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.LocalDebuggerModelFactory;
import ghidra.dbg.util.ConfigurableFactory.FactoryDescription;
@FactoryDescription(brief = "Mocked Local Client", htmlDetails = TestDebuggerModelFactory.FAKE_DETAILS)
public class TestLocalDebuggerModelFactory implements LocalDebuggerModelFactory {
protected final Deque<CompletableFuture<DebuggerObjectModel>> buildQueue =
new LinkedList<>();
public TestLocalDebuggerModelFactory() {
}
@Override
public CompletableFuture<? extends DebuggerObjectModel> build() {
CompletableFuture<DebuggerObjectModel> future = new CompletableFuture<>();
buildQueue.offer(future);
return future;
}
public CompletableFuture<DebuggerObjectModel> pollBuild() {
return buildQueue.poll();
}
}

View file

@ -0,0 +1,32 @@
/* ###
* 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.target;
import static org.junit.Assert.*;
import java.util.List;
import org.junit.Test;
import ghidra.dbg.target.TargetLauncher.CmdLineParser;
public class TargetLauncherTest {
@Test
public void testCmdLineParser() {
assertEquals(List.of("C:\\Windows", "10\\notepad.exe"),
CmdLineParser.tokenize("C:\\Windows 10\\notepad.exe"));
}
}

View file

@ -0,0 +1,198 @@
/* ###
* 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.test;
import static org.junit.Assert.*;
import static org.junit.Assume.*;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.junit.Test;
import generic.Unique;
import ghidra.dbg.target.*;
import ghidra.dbg.util.PathUtils;
/**
* Test model object activation and focus
*
* <p>
* Activation and focus are related but separate concepts. Focus is a little looser, and is allowed
* by the model to exactly match the client's notion of focus, usually indicating the object of the
* user's interest. Activation, however, commands the model to make the given object the "current"
* object. This implies any commands issued to the CLI will affect the active object. The model
* reflects the active object back to the client via focus. This allows the model and client to
* synchronize their "active" objects, while reducing the likelihood of event feedback loops.
* Furthermore, not every object can be activated. For example, activating a register will likely
* result in the containing thread or frame becoming active instead. Or, activating a thread may
* result in its innermost frame becoming active as well.
*/
public abstract class AbstractDebuggerModelActivationTest extends AbstractDebuggerModelTest {
/**
* Use the interpreter to activate the given object
*
* @param obj the object to activate
* @param interpreter the interpreter to use
* @throws Throwable if anything goes wrong
*/
protected void activateViaInterpreter(TargetObject obj, TargetInterpreter interpreter)
throws Throwable {
fail("Unless hasInterpreter is false, the test must implement this method");
}
/**
* Use the interpreter to verify the given object is active/current
*
* <p>
* Note, it may be necessary to run and capture several commands, depending on what's being
* verified and what sort of commands the interpreter makes available. For example, to verify a
* frame is active, the test should check that the containing thread and process are active,
* too.
*
* @param expected the expected active or current object
* @param interpreter the interpreter to use
* @throws Throwable if anything goes wrong
*/
protected void assertActiveViaInterpreter(TargetObject expected,
TargetInterpreter interpreter) throws Throwable {
fail("Unless hasInterpreter is false, the test must implement this method");
}
/**
* Get (possibly generate) things for this focus test to try out
*
* @throws Throwable if anything goes wrong
*/
protected abstract Set<TargetObject> getActivatableThings() throws Throwable;
/**
* Governs whether assertions permit the actual object to be a successor of the expected object
*
* @return true to permit successors, false to require exact
*/
protected boolean permitSuccessor() {
return true;
}
protected void assertSuccessorOrExact(TargetObject expected, TargetObject actual) {
assertNotNull(actual);
if (permitSuccessor()) {
assertTrue("Expected successor of '" + expected.getJoinedPath(".") +
"' got '" + actual.getJoinedPath(".") + "'",
PathUtils.isAncestor(expected.getPath(), actual.getPath()));
}
else {
assertSame(expected, actual);
}
}
/**
* If the default focus is one of the activatable things (after generation), assert its path
*
* @return the path of the expected default focus, or {@code null} for no assertion
*/
protected List<String> getExpectedDefaultActivePath() {
return null;
}
@Test
public void testDefaultFocusIsAsExpected() throws Throwable {
List<String> expectedDefaultFocus = getExpectedDefaultActivePath();
assumeNotNull(expectedDefaultFocus);
m.build();
TargetFocusScope focusScope = findFocusScope();
Set<TargetObject> activatable = getActivatableThings();
// The default must be one of the activatable objects
TargetObject obj = Unique.assertOne(activatable.stream()
.filter(f -> PathUtils.isAncestor(f.getPath(), expectedDefaultFocus))
.collect(Collectors.toList()));
retryVoid(() -> {
assertEquals(expectedDefaultFocus, focusScope.getFocus().getPath());
}, List.of(AssertionError.class));
if (m.hasInterpreter()) {
TargetInterpreter interpreter = findInterpreter();
assertActiveViaInterpreter(obj, interpreter);
}
}
@Test
public void testActivateEachOnce() throws Throwable {
m.build();
TargetFocusScope focusScope = findFocusScope();
TargetActiveScope activeScope = findActiveScope();
Set<TargetObject> activatable = getActivatableThings();
for (TargetObject obj : activatable) {
waitOn(activeScope.requestActivation(obj));
retryVoid(() -> {
assertSuccessorOrExact(obj, focusScope.getFocus());
}, List.of(AssertionError.class));
if (m.hasInterpreter()) {
TargetInterpreter interpreter = findInterpreter();
assertActiveViaInterpreter(obj, interpreter);
}
}
}
@Test
public void testActivateEachTwice() throws Throwable {
m.build();
TargetFocusScope focusScope = findFocusScope();
TargetActiveScope activeScope = findActiveScope();
Set<TargetObject> activatable = getActivatableThings();
for (TargetObject obj : activatable) {
waitOn(activeScope.requestActivation(obj));
retryVoid(() -> {
assertSuccessorOrExact(obj, focusScope.getFocus());
}, List.of(AssertionError.class));
if (m.hasInterpreter()) {
TargetInterpreter interpreter = findInterpreter();
assertActiveViaInterpreter(obj, interpreter);
}
waitOn(activeScope.requestActivation(obj));
retryVoid(() -> {
assertSuccessorOrExact(obj, focusScope.getFocus());
}, List.of(AssertionError.class));
if (m.hasInterpreter()) {
TargetInterpreter interpreter = findInterpreter();
assertActiveViaInterpreter(obj, interpreter);
}
}
}
@Test
public void testActivateEachViaInterpreter() throws Throwable {
assumeTrue(m.hasInterpreter());
m.build();
TargetFocusScope focusScope = findFocusScope();
Set<TargetObject> activatable = getActivatableThings();
TargetInterpreter interpreter = findInterpreter();
for (TargetObject obj : activatable) {
activateViaInterpreter(obj, interpreter);
retryVoid(() -> {
assertSuccessorOrExact(obj, focusScope.getFocus());
}, List.of(AssertionError.class));
assertActiveViaInterpreter(obj, interpreter);
}
}
}

View file

@ -16,8 +16,7 @@
package ghidra.dbg.test;
import static org.junit.Assert.*;
import static org.junit.Assume.assumeNotNull;
import static org.junit.Assume.assumeTrue;
import static org.junit.Assume.*;
import java.util.*;
import java.util.Map.Entry;
@ -162,10 +161,97 @@ public abstract class AbstractDebuggerModelBreakpointsTest extends AbstractDebug
return null;
}
protected void runTestPlaceBreakpoint(TargetBreakpointKind kind) throws Throwable {
assumeTrue(getExpectedSupportedKinds().contains(kind));
m.build();
/**
* Verify that the given breakpoint location covers the required range and kind, using the
* interpreter
*
* @param range the requested range of the breakpoint
* @param kind the requested kind of the breakpoint
* @param loc the location object
* @param interpreter the interpreter
* @throws Throwable if anything goes wrong
*/
protected void assertLocCoversViaInterpreter(AddressRange range,
TargetBreakpointKind kind, TargetBreakpointLocation loc,
TargetInterpreter interpreter) throws Throwable {
fail("Unless hasInterpreter is false, the test must implement this method");
}
/**
* Verify that the given spec and/or location is in the given state, using the interpreter
*
* @param t the spec or location
* @param enabled the expected state: true for enabled, false for disabled
* @param interpreter the interpreter
* @throws Throwable if anything goes wrong
*/
protected void assertEnabledViaInterpreter(TargetTogglable t, boolean enabled,
TargetInterpreter interpreter) throws Throwable {
fail("Unless hasInterpreter is false, the test must implement this method");
}
/**
* Verify that the given spec and/or location no longer exists, using the interpreter
*
* @param d the spec or location
* @param interpreter the interpreter
* @throws Throwable if anything goes wrong
*/
protected void assertDeletedViaInterpreter(TargetDeletable d, TargetInterpreter interpreter)
throws Throwable {
fail("Unless hasInterpreter is false, the test must implement this method");
}
/**
* Place the given breakpoint using the interpreter
*
* @param range the requested range
* @param kind the requested kind
* @param interpreter the interpreter
* @throws Throwable if anything goes wrong
*/
protected void placeBreakpointViaInterpreter(AddressRange range, TargetBreakpointKind kind,
TargetInterpreter interpreter) throws Throwable {
fail("Unless hasInterpreter is false, the test must implement this method");
}
/**
* Disable the given spec and/or location using the interpreter
*
* @param t the spec and/or location
* @param interpreter the interpreter
* @throws Throwable if anything goes wrong
*/
protected void disableViaInterpreter(TargetTogglable t, TargetInterpreter interpreter)
throws Throwable {
fail("Unless hasInterpreter is false, the test must implement this method");
}
/**
* Enable the given spec and/or location using the interpreter
*
* @param t the spec and/or location
* @param interpreter the interpreter
* @throws Throwable if anything goes wrong
*/
protected void enableViaInterpreter(TargetTogglable t, TargetInterpreter interpreter)
throws Throwable {
fail("Unless hasInterpreter is false, the test must implement this method");
}
/**
* Delete the given spec and/or location using the interpreter
*
* @param d the spec and/or location
* @param interpreter the interpreter
* @throws Throwable if anything goes wrong
*/
protected void deleteViaInterpreter(TargetDeletable d, TargetInterpreter interpreter)
throws Throwable {
fail("Unless hasInterpreter is false, the test must implement this method");
}
protected void addMonitor() {
var monitor = new DebuggerModelListener() {
DebuggerCallbackReorderer reorderer = new DebuggerCallbackReorderer(this);
@ -217,25 +303,56 @@ public abstract class AbstractDebuggerModelBreakpointsTest extends AbstractDebug
}
};
m.getModel().addModelListener(monitor.reorderer, true);
}
protected void runTestPlaceBreakpoint(TargetBreakpointKind kind) throws Throwable {
assumeTrue(getExpectedSupportedKinds().contains(kind));
m.build();
addMonitor();
TargetObject target = obtainTarget();
TargetBreakpointSpecContainer container = findBreakpointSpecContainer(target.getPath());
AddressRange range = getSuitableRangeForBreakpoint(target, kind);
waitOn(container.placeBreakpoint(range, Set.of(kind)));
retryVoid(() -> {
TargetBreakpointLocation loc = retry(() -> {
Collection<? extends TargetBreakpointLocation> found =
m.findAll(TargetBreakpointLocation.class, target.getPath(), true).values();
assertAtLeastOneLocCovers(found, range, kind);
return assertAtLeastOneLocCovers(found, range, kind);
}, List.of(AssertionError.class));
if (m.hasInterpreter()) {
TargetInterpreter interpreter = findInterpreter();
assertLocCoversViaInterpreter(range, kind, loc, interpreter);
}
}
protected void runTestPlaceBreakpointViaInterpreter(TargetBreakpointKind kind)
throws Throwable {
assumeTrue(getExpectedSupportedKinds().contains(kind));
assumeTrue(m.hasInterpreter());
m.build();
addMonitor();
TargetObject target = obtainTarget();
TargetInterpreter interpreter = findInterpreter();
AddressRange range = getSuitableRangeForBreakpoint(target, kind);
placeBreakpointViaInterpreter(range, kind, interpreter);
TargetBreakpointLocation loc = retry(() -> {
Collection<? extends TargetBreakpointLocation> found =
m.findAll(TargetBreakpointLocation.class, target.getPath(), true).values();
return assertAtLeastOneLocCovers(found, range, kind);
}, List.of(AssertionError.class));
assertLocCoversViaInterpreter(range, kind, loc, interpreter);
}
@Test
public void testPlaceSoftwareBreakpoint() throws Throwable {
public void testPlaceSoftwareExecuteBreakpoint() throws Throwable {
runTestPlaceBreakpoint(TargetBreakpointKind.SW_EXECUTE);
}
@Test
public void testPlaceHardwareBreakpoint() throws Throwable {
public void testPlaceHardwareExecuteBreakpoint() throws Throwable {
runTestPlaceBreakpoint(TargetBreakpointKind.HW_EXECUTE);
}
@ -249,6 +366,26 @@ public abstract class AbstractDebuggerModelBreakpointsTest extends AbstractDebug
runTestPlaceBreakpoint(TargetBreakpointKind.WRITE);
}
@Test
public void testPlaceSoftwareExecuteBreakpointViaInterpreter() throws Throwable {
runTestPlaceBreakpointViaInterpreter(TargetBreakpointKind.SW_EXECUTE);
}
@Test
public void testPlaceHardwareExecuteBreakpointViaInterpreter() throws Throwable {
runTestPlaceBreakpointViaInterpreter(TargetBreakpointKind.HW_EXECUTE);
}
@Test
public void testPlaceReadBreakpointViaInterpreter() throws Throwable {
runTestPlaceBreakpointViaInterpreter(TargetBreakpointKind.READ);
}
@Test
public void testPlaceWriteBreakpointViaInterpreter() throws Throwable {
runTestPlaceBreakpointViaInterpreter(TargetBreakpointKind.WRITE);
}
protected Set<TargetBreakpointLocation> createLocations() throws Throwable {
// TODO: Test with multiple targets?
TargetObject target = obtainTarget();
@ -278,6 +415,10 @@ public abstract class AbstractDebuggerModelBreakpointsTest extends AbstractDebug
retryVoid(() -> {
assertFalse(t.isEnabled());
}, List.of(AssertionError.class));
if (m.hasInterpreter()) {
TargetInterpreter interpreter = findInterpreter();
assertEnabledViaInterpreter(t, false, interpreter);
}
}
// Repeat it for fun. Should have no effect
for (TargetTogglable t : order) {
@ -285,6 +426,10 @@ public abstract class AbstractDebuggerModelBreakpointsTest extends AbstractDebug
retryVoid(() -> {
assertFalse(t.isEnabled());
}, List.of(AssertionError.class));
if (m.hasInterpreter()) {
TargetInterpreter interpreter = findInterpreter();
assertEnabledViaInterpreter(t, false, interpreter);
}
}
// Enable each
@ -293,6 +438,10 @@ public abstract class AbstractDebuggerModelBreakpointsTest extends AbstractDebug
retryVoid(() -> {
assertTrue(t.isEnabled());
}, List.of(AssertionError.class));
if (m.hasInterpreter()) {
TargetInterpreter interpreter = findInterpreter();
assertEnabledViaInterpreter(t, true, interpreter);
}
}
// Repeat it for fun. Should have no effect
for (TargetTogglable t : order) {
@ -300,6 +449,49 @@ public abstract class AbstractDebuggerModelBreakpointsTest extends AbstractDebug
retryVoid(() -> {
assertTrue(t.isEnabled());
}, List.of(AssertionError.class));
if (m.hasInterpreter()) {
TargetInterpreter interpreter = findInterpreter();
assertEnabledViaInterpreter(t, true, interpreter);
}
}
}
protected void runToggleTestViaInterpreter(Set<TargetTogglable> set,
TargetInterpreter interpreter) throws Throwable {
List<TargetTogglable> order = new ArrayList<>(set);
Collections.shuffle(order);
// Disable each
for (TargetTogglable t : order) {
disableViaInterpreter(t, interpreter);
retryVoid(() -> {
assertFalse(t.isEnabled());
}, List.of(AssertionError.class));
assertEnabledViaInterpreter(t, false, interpreter);
}
// Repeat it for fun. Should have no effect
for (TargetTogglable t : order) {
disableViaInterpreter(t, interpreter);
retryVoid(() -> {
assertFalse(t.isEnabled());
}, List.of(AssertionError.class));
assertEnabledViaInterpreter(t, false, interpreter);
}
// Enable each
for (TargetTogglable t : order) {
enableViaInterpreter(t, interpreter);
retryVoid(() -> {
assertTrue(t.isEnabled());
}, List.of(AssertionError.class));
assertEnabledViaInterpreter(t, true, interpreter);
}
// Repeat it for fun. Should have no effect
for (TargetTogglable t : order) {
enableViaInterpreter(t, interpreter);
retryVoid(() -> {
assertTrue(t.isEnabled());
}, List.of(AssertionError.class));
assertEnabledViaInterpreter(t, true, interpreter);
}
}
@ -313,6 +505,19 @@ public abstract class AbstractDebuggerModelBreakpointsTest extends AbstractDebug
.collect(Collectors.toSet()));
}
@Test
public void testToggleBreakpointsViaInterpreter() throws Throwable {
assumeTrue(m.hasInterpreter());
m.build();
Set<TargetBreakpointLocation> locs = createLocations();
TargetInterpreter interpreter = findInterpreter();
runToggleTestViaInterpreter(locs.stream()
.map(l -> l.getSpecification().as(TargetTogglable.class))
.collect(Collectors.toSet()),
interpreter);
}
@Test
public void testToggleBreakpointLocations() throws Throwable {
assumeTrue(isSupportsTogglableLocations());
@ -323,15 +528,46 @@ public abstract class AbstractDebuggerModelBreakpointsTest extends AbstractDebug
locs.stream().map(l -> l.as(TargetTogglable.class)).collect(Collectors.toSet()));
}
@Test
public void testToggleBreakpointLocationsViaInterpreter() throws Throwable {
assumeTrue(isSupportsTogglableLocations());
assumeTrue(m.hasInterpreter());
m.build();
Set<TargetBreakpointLocation> locs = createLocations();
TargetInterpreter interpreter = findInterpreter();
runToggleTestViaInterpreter(
locs.stream().map(l -> l.as(TargetTogglable.class)).collect(Collectors.toSet()),
interpreter);
}
protected void runDeleteTest(Set<TargetDeletable> set) throws Throwable {
List<TargetDeletable> order = new ArrayList<>(set);
Collections.shuffle(order);
// Disable each
// Delete each
for (TargetDeletable d : order) {
waitOn(d.delete());
retryVoid(() -> {
assertFalse(d.isValid());
}, List.of(AssertionError.class));
if (m.hasInterpreter()) {
TargetInterpreter interpreter = findInterpreter();
assertDeletedViaInterpreter(d, interpreter);
}
}
}
protected void runDeleteTestViaInterpreter(Set<TargetDeletable> set,
TargetInterpreter interpreter) throws Throwable {
List<TargetDeletable> order = new ArrayList<>(set);
Collections.shuffle(order);
// Delete each
for (TargetDeletable d : order) {
deleteViaInterpreter(d, interpreter);
retryVoid(() -> {
assertFalse(d.isValid());
}, List.of(AssertionError.class));
assertDeletedViaInterpreter(d, interpreter);
}
}
@ -345,6 +581,19 @@ public abstract class AbstractDebuggerModelBreakpointsTest extends AbstractDebug
.collect(Collectors.toSet()));
}
@Test
public void testDeleteBreakpointsViaInterpreter() throws Throwable {
assumeTrue(m.hasInterpreter());
m.build();
Set<TargetBreakpointLocation> locs = createLocations();
TargetInterpreter interpreter = findInterpreter();
runDeleteTestViaInterpreter(locs.stream()
.map(l -> l.getSpecification().as(TargetDeletable.class))
.collect(Collectors.toSet()),
interpreter);
}
@Test
public void testDeleteBreakpointLocations() throws Throwable {
assumeTrue(isSupportsDeletableLocations());
@ -354,4 +603,17 @@ public abstract class AbstractDebuggerModelBreakpointsTest extends AbstractDebug
runDeleteTest(
locs.stream().map(l -> l.as(TargetDeletable.class)).collect(Collectors.toSet()));
}
@Test
public void testDeleteBreakpointLocationsViaInterpreter() throws Throwable {
assumeTrue(isSupportsDeletableLocations());
assumeTrue(m.hasInterpreter());
m.build();
TargetInterpreter interpreter = findInterpreter();
Set<TargetBreakpointLocation> locs = createLocations();
runDeleteTestViaInterpreter(
locs.stream().map(l -> l.as(TargetDeletable.class)).collect(Collectors.toSet()),
interpreter);
}
}

View file

@ -1,120 +0,0 @@
/* ###
* 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.test;
import static org.junit.Assert.*;
import static org.junit.Assume.assumeNotNull;
import java.util.List;
import java.util.Set;
import org.junit.Test;
import ghidra.dbg.target.TargetFocusScope;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.util.PathUtils;
/**
* TODO: Since activation and focus are separate concepts, we need to fix the terminology here and
* ensure we're testing the right things.
*/
public abstract class AbstractDebuggerModelFocusTest extends AbstractDebuggerModelTest {
/**
* Get (possibly generate) things for this focus test to try out
*
* @throws Throwable if anything goes wrong
*/
protected abstract Set<TargetObject> getFocusableThings() throws Throwable;
/**
* Governs whether assertions permit the actual object to be a successor of the expected object
*
* @return true to permit successors, false to require exact
*/
protected boolean permitSuccessor() {
return true;
}
protected void assertSuccessorOrExact(TargetObject expected, TargetObject actual) {
assertNotNull(actual);
if (permitSuccessor()) {
assertTrue("Expected successor of '" + expected.getJoinedPath(".") +
"' got '" + actual.getJoinedPath(".") + "'",
PathUtils.isAncestor(expected.getPath(), actual.getPath()));
}
else {
assertSame(expected, actual);
}
}
/**
* If the default focus is one of the focusable things (after generation), assert its path
*
* @return the path of the expected default focus, or {@code null} for no assertion
*/
protected List<String> getExpectedDefaultFocus() {
return null;
}
@Test
public void testDefaultFocusIsAsExpected() throws Throwable {
List<String> expectedDefaultFocus = getExpectedDefaultFocus();
assumeNotNull(expectedDefaultFocus);
m.build();
TargetFocusScope scope = findFocusScope();
Set<TargetObject> focusable = getFocusableThings();
// The default must be one of the focusable objects
assertTrue(focusable.stream()
.anyMatch(f -> PathUtils.isAncestor(f.getPath(), expectedDefaultFocus)));
retryVoid(() -> {
assertEquals(expectedDefaultFocus, scope.getFocus().getPath());
}, List.of(AssertionError.class));
}
@Test
public void testFocusEachOnce() throws Throwable {
m.build();
TargetFocusScope scope = findFocusScope();
Set<TargetObject> focusable = getFocusableThings();
for (TargetObject obj : focusable) {
waitOn(scope.requestFocus(obj));
retryVoid(() -> {
assertSuccessorOrExact(obj, scope.getFocus());
}, List.of(AssertionError.class));
}
}
@Test
public void testFocusEachTwice() throws Throwable {
m.build();
TargetFocusScope scope = findFocusScope();
Set<TargetObject> focusable = getFocusableThings();
for (TargetObject obj : focusable) {
waitOn(scope.requestFocus(obj));
retryVoid(() -> {
assertSuccessorOrExact(obj, scope.getFocus());
}, List.of(AssertionError.class));
waitOn(scope.requestFocus(obj));
retryVoid(() -> {
assertSuccessorOrExact(obj, scope.getFocus());
}, List.of(AssertionError.class));
}
}
}

View file

@ -15,7 +15,6 @@
*/
package ghidra.dbg.test;
import static ghidra.lifecycle.Unfinished.TODO;
import static org.junit.Assert.*;
import static org.junit.Assume.assumeNotNull;
import static org.junit.Assume.assumeTrue;
@ -24,7 +23,6 @@ import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.Ignore;
import org.junit.Test;
import ghidra.async.AsyncReference;
@ -94,13 +92,26 @@ public abstract class AbstractDebuggerModelInterpreterTest extends AbstractDebug
*/
protected abstract String getKillCommand(TargetProcess process);
/**
* Perform an pre-test actions to ensure an interpreter exists where expected
*
* <p>
* The model will have been built already. This method is invoked immediately preceding
* {@link #findInterpreter()}
*
* @throws Throwable if anything goes wrong
*/
protected void ensureInterpreterAvailable() throws Throwable {
}
@Test
public void testInterpreterIsWhereExpected() throws Throwable {
List<String> expectedInterpreterPath = getExpectedInterpreterPath();
assumeNotNull(expectedInterpreterPath);
m.build();
TargetInterpreter interpreter = m.find(TargetInterpreter.class, List.of());
ensureInterpreterAvailable();
TargetInterpreter interpreter = findInterpreter();
assertEquals(expectedInterpreterPath, interpreter.getPath());
}
@ -128,7 +139,8 @@ public abstract class AbstractDebuggerModelInterpreterTest extends AbstractDebug
assumeNotNull(cmd);
m.build();
TargetInterpreter interpreter = m.find(TargetInterpreter.class, List.of());
ensureInterpreterAvailable();
TargetInterpreter interpreter = findInterpreter();
runTestExecute(interpreter, cmd);
}
@ -163,7 +175,8 @@ public abstract class AbstractDebuggerModelInterpreterTest extends AbstractDebug
assumeNotNull(cmd);
m.build();
TargetInterpreter interpreter = m.find(TargetInterpreter.class, List.of());
ensureInterpreterAvailable();
TargetInterpreter interpreter = findInterpreter();
runTestExecuteCapture(interpreter, cmd);
}
@ -178,25 +191,11 @@ public abstract class AbstractDebuggerModelInterpreterTest extends AbstractDebug
assumeNotNull(cmd);
m.build();
TargetInterpreter interpreter = m.find(TargetInterpreter.class, List.of());
ensureInterpreterAvailable();
TargetInterpreter interpreter = findInterpreter();
runTestExecute(interpreter, cmd);
}
@Test
@Ignore
public void testFocusIsSynced() throws Throwable {
TODO();
}
@Test
@Ignore
public void testBreakpointsAreSynced() throws Throwable {
TODO();
// TODO: Place different kinds
// TODO: Enable/disable
// TODO: Delete (spec vs. loc?)
}
protected TargetProcess runTestLaunchViaInterpreterShowsInProcessContainer(
TargetInterpreter interpreter) throws Throwable {
DebuggerTestSpecimen specimen = getLaunchSpecimen();
@ -220,6 +219,7 @@ public abstract class AbstractDebuggerModelInterpreterTest extends AbstractDebug
assumeTrue(m.hasProcessContainer());
m.build();
ensureInterpreterAvailable();
TargetInterpreter interpreter = findInterpreter();
TargetProcess process = runTestLaunchViaInterpreterShowsInProcessContainer(interpreter);
@ -250,6 +250,7 @@ public abstract class AbstractDebuggerModelInterpreterTest extends AbstractDebug
m.build();
dummy = specimen.runDummy();
ensureInterpreterAvailable();
TargetInterpreter interpreter = findInterpreter();
TargetProcess process = runTestAttachViaInterpreterShowsInProcessContainer(interpreter);

View file

@ -16,7 +16,7 @@
package ghidra.dbg.test;
import static org.junit.Assert.*;
import static org.junit.Assume.assumeNotNull;
import static org.junit.Assume.*;
import java.util.*;
import java.util.concurrent.CompletableFuture;
@ -109,7 +109,7 @@ public abstract class AbstractDebuggerModelSteppableTest extends AbstractDebugge
* @return the window in milliseconds
*/
protected long getDebounceWindowMs() {
return 1000;
return 5000;
}
enum CallbackType {

View file

@ -31,7 +31,7 @@ import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind;
import ghidra.dbg.target.TargetEventScope.TargetEventType;
import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState;
import ghidra.dbg.testutil.*;
import ghidra.test.AbstractGhidraHeadlessIntegrationTest;
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
import ghidra.util.Msg;
/**
@ -41,7 +41,7 @@ import ghidra.util.Msg;
* <li>TODO: ensure registersUpdated(RegisterBank) immediately upon created(RegisterBank) ?</li>
* </ul>
*/
public abstract class AbstractDebuggerModelTest extends AbstractGhidraHeadlessIntegrationTest
public abstract class AbstractDebuggerModelTest extends AbstractGhidraHeadedIntegrationTest
implements TestDebuggerModelProvider, DebuggerModelTestUtils {
protected DummyProc dummy;
@ -56,6 +56,10 @@ public abstract class AbstractDebuggerModelTest extends AbstractGhidraHeadlessIn
return List.of();
}
protected TargetActiveScope findActiveScope() throws Throwable {
return m.find(TargetActiveScope.class, seedPath());
}
protected TargetObject findAttachableContainer() throws Throwable {
return m.findContainer(TargetAttachable.class, seedPath());
}

View file

@ -124,6 +124,11 @@ public abstract class AbstractModelHost implements ModelHost, DebuggerModelTestU
return true;
}
@Override
public boolean hasInterpreter() {
return true;
}
@Override
public boolean hasInterruptibleProcesses() {
return true;

View file

@ -47,7 +47,7 @@ public class CallbackValidator implements DebuggerModelListener, AutoCloseable {
public final DebuggerObjectModel model;
public Thread thread = null;
public Set<TargetObject> valid = new HashSet<>();
public Map<TargetObject, TargetObject> valid = new HashMap<>();
// Knobs
// TODO: Make these methods instead?
@ -85,12 +85,8 @@ public class CallbackValidator implements DebuggerModelListener, AutoCloseable {
thread = Thread.currentThread();
}
if (requireSameThread) {
if (thread != Thread.currentThread()) {
throw new AssertionError("Completion came from an unexpected thread expected:" +
thread + " but got " + Thread.currentThread());
}
assertEquals("Completion came from an unexpected thread", thread,
Thread.currentThread());
assertEquals("Completion came from an unexpected thread. Probably forgot gateFuture()",
thread, Thread.currentThread());
}
}
@ -113,14 +109,14 @@ public class CallbackValidator implements DebuggerModelListener, AutoCloseable {
public void validateObjectValid(String callback, TargetObject obj) {
if (requireValid) {
assertTrue("Object " + obj.getJoinedPath(".") + " invalid during callback " + callback,
valid.contains(obj));
valid.containsKey(obj));
}
}
public void validateObjectInvalid(String callback, TargetObject obj) {
if (requireValid) {
assertFalse("Object " + obj.getJoinedPath(".") + " valid during callback " + callback +
", but should have been invalid", valid.contains(obj));
", but should have been invalid", valid.containsKey(obj));
}
}
@ -260,8 +256,16 @@ public class CallbackValidator implements DebuggerModelListener, AutoCloseable {
if (log) {
Msg.info(this, "created(object=" + object + ")");
}
valid.add(object);
TargetObject exists = valid.put(object, object);
off.catching(() -> {
if (exists != null) {
if (exists == object) {
fail("created twice (same object): " + object.getJoinedPath("."));
}
else {
fail("replaced before invalidation. old= " + exists + ", new=" + object);
}
}
validateCallbackThread("created");
validateObject("created", object);
});

View file

@ -23,15 +23,19 @@ import java.util.Map.Entry;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import ghidra.async.AsyncReference;
import ghidra.async.AsyncTestUtils;
import ghidra.dbg.DebugModelConventions;
import ghidra.async.*;
import ghidra.dbg.*;
import ghidra.dbg.DebugModelConventions.AsyncAccess;
import ghidra.dbg.error.DebuggerMemoryAccessException;
import ghidra.dbg.target.*;
import ghidra.dbg.target.TargetConsole.Channel;
import ghidra.dbg.target.TargetEventScope.TargetEventType;
import ghidra.dbg.target.TargetSteppable.TargetStepKind;
import ghidra.dbg.test.AbstractDebuggerModelTest;
import ghidra.dbg.test.AbstractDebuggerModelTest.DebuggerTestSpecimen;
import ghidra.dbg.util.PathUtils;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressRange;
import ghidra.util.NumericUtilities;
public interface DebuggerModelTestUtils extends AsyncTestUtils {
@ -204,4 +208,78 @@ public interface DebuggerModelTestUtils extends AsyncTestUtils {
return process;
}, List.of(AssertionError.class));
}
default void waitSettled(DebuggerObjectModel model, int ms) throws Throwable {
AsyncDebouncer<Void> debouncer = new AsyncDebouncer<>(AsyncTimer.DEFAULT_TIMER, ms);
var listener = new DebuggerModelListener() {
@Override
public void attributesChanged(TargetObject object, Collection<String> removed,
Map<String, ?> added) {
debouncer.contact(null);
}
@Override
public void breakpointHit(TargetObject container, TargetObject trapped,
TargetStackFrame frame, TargetBreakpointSpec spec,
TargetBreakpointLocation breakpoint) {
debouncer.contact(null);
}
@Override
public void consoleOutput(TargetObject console, Channel channel, byte[] data) {
debouncer.contact(null);
}
@Override
public void created(TargetObject object) {
debouncer.contact(null);
}
@Override
public void elementsChanged(TargetObject object, Collection<String> removed,
Map<String, ? extends TargetObject> added) {
debouncer.contact(null);
}
@Override
public void event(TargetObject object, TargetThread eventThread, TargetEventType type,
String description, List<Object> parameters) {
debouncer.contact(null);
}
@Override
public void invalidateCacheRequested(TargetObject object) {
debouncer.contact(null);
}
@Override
public void invalidated(TargetObject object, TargetObject branch, String reason) {
debouncer.contact(null);
}
@Override
public void memoryReadError(TargetObject memory, AddressRange range,
DebuggerMemoryAccessException e) {
debouncer.contact(null);
}
@Override
public void memoryUpdated(TargetObject memory, Address address, byte[] data) {
debouncer.contact(null);
}
@Override
public void registersUpdated(TargetObject bank, Map<String, byte[]> updates) {
debouncer.contact(null);
}
@Override
public void rootAdded(TargetObject root) {
debouncer.contact(null);
}
};
model.addModelListener(listener);
debouncer.contact(null);
waitOnNoValidate(debouncer.settled());
}
}

View file

@ -39,6 +39,8 @@ public interface TestDebuggerModelProvider {
boolean hasDetachableProcesses();
boolean hasInterpreter();
boolean hasInterruptibleProcesses();
boolean hasKillableProcesses();