Merge remote-tracking branch 'origin/patch'

Conflicts:
	Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/DebuggerObjectsProvider.java
This commit is contained in:
Ryan Kurtz 2023-05-22 06:50:32 -04:00
commit ce8fadf7a8
16 changed files with 163 additions and 112 deletions

View file

@ -117,14 +117,16 @@ public interface GdbInferior extends GdbConsoleOperations, GdbMemoryOperations {
* List GDB's modules in this inferior (process, thread group)
*
* <p>
* This is equivalent to the CLI command: {@code maintenance info sections ALLOBJ}. This command
* is more thorough than {@code info shared} as it contains the executable module, shared
* libraries, system-supplied objects, and enumerates all sections thereof, not just
* {@code .text}.
* This is equivalent to the CLI command: {@code maintenance info sections -all-objects}. This
* command is more thorough than {@code info shared} as it contains the executable module,
* shared libraries, system-supplied objects, and enumerates all sections thereof, not just
* {@code .text}. By default, the manager will only refresh this list on the first call or the
* next call since a module-loaded event. Otherwise, it will just return its cached list.
*
* @param refresh force the manager to refresh its modules and sections lists
* @return a future that completes with a map of module names to module handles
*/
CompletableFuture<Map<String, GdbModule>> listModules();
CompletableFuture<Map<String, GdbModule>> listModules(boolean refresh);
/**
* Enumerate the memory mappings known to the manager to belong to this inferior's process

View file

@ -23,18 +23,13 @@ import agent.gdb.manager.impl.GdbMinimalSymbol;
public interface GdbModule {
String getName();
CompletableFuture<Long> computeBase();
Long getBase();
CompletableFuture<Long> computeMax();
Long getMax();
Long getKnownBase();
Long getKnownMax();
CompletableFuture<Map<String, GdbModuleSection>> listSections();
CompletableFuture<Map<String, GdbModuleSection>> listSections(boolean refresh);
Map<String, GdbModuleSection> getKnownSections();
CompletableFuture<Map<String, GdbMinimalSymbol>> listMinimalSymbols();
}

View file

@ -19,7 +19,6 @@ import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.CompletableFuture;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -30,6 +29,7 @@ import agent.gdb.manager.GdbManager.StepCmd;
import agent.gdb.manager.impl.cmd.*;
import agent.gdb.manager.impl.cmd.GdbConsoleExecCommand.CompletesWithRunning;
import generic.ULongSpan.ULongSpanSet;
import ghidra.async.AsyncLazyValue;
import ghidra.lifecycle.Internal;
import ghidra.util.Msg;
@ -69,6 +69,8 @@ public class GdbInferiorImpl implements GdbInferior {
private final Map<String, GdbModuleImpl> modules = new LinkedHashMap<>();
private final Map<String, GdbModule> unmodifiableModules = Collections.unmodifiableMap(modules);
protected final AsyncLazyValue<Map<String, GdbModule>> listModules =
new AsyncLazyValue<>(this::doListModules);
private final NavigableMap<BigInteger, GdbMemoryMapping> mappings = new TreeMap<>();
private final NavigableMap<BigInteger, GdbMemoryMapping> unmodifiableMappings =
@ -235,20 +237,29 @@ public class GdbInferiorImpl implements GdbInferior {
}
@Override
public CompletableFuture<Map<String, GdbModule>> listModules() {
return manager.execMaintInfoSectionsAllObjects(this).thenApply(lines -> {
return parseModuleNames(lines);
});
public CompletableFuture<Map<String, GdbModule>> listModules(boolean refresh) {
if (refresh) {
if (listModules.isBusy()) {
manager.logInfo("Refresh requested while busy. Keeping cache.");
}
else {
manager.logInfo("Refresh requested. Forgetting module cache.");
listModules.forget();
}
}
manager.logInfo("Modules requested");
return listModules.request();
}
protected CompletableFuture<Void> doLoadSections() {
return manager.execMaintInfoSectionsAllObjects(this).thenAccept(lines -> {
protected CompletableFuture<Map<String, GdbModule>> doListModules() {
return manager.execMaintInfoSectionsAllObjects(this).thenApply(lines -> {
parseAndUpdateAllModuleSections(lines);
return unmodifiableModules;
});
}
protected GdbModuleImpl resyncCreateModule(String name) {
Msg.warn(this, "Resync: Missed loaded module/library: " + name);
//Msg.warn(this, "Resync: Missed loaded module/library: " + name);
//manager.listenersInferior.fire.libraryLoaded(this, name, Causes.UNCLAIMED);
return createModule(name);
}
@ -258,7 +269,8 @@ public class GdbInferiorImpl implements GdbInferior {
}
protected void libraryLoaded(String name) {
modules.computeIfAbsent(name, this::createModule);
manager.logInfo("Module loaded: " + name + ". Forgeting module cache.");
listModules.forget();
}
protected void libraryUnloaded(String name) {
@ -266,15 +278,11 @@ public class GdbInferiorImpl implements GdbInferior {
}
protected void resyncRetainModules(Set<String> names) {
for (Iterator<Entry<String, GdbModuleImpl>> mit = modules.entrySet().iterator(); mit
.hasNext();) {
Entry<String, GdbModuleImpl> ent = mit.next();
if (!names.contains(ent.getKey())) {
Msg.warn(this, "Resync: Missed unloaded module/library: " + ent);
/*manager.listenersInferior.fire.libraryUnloaded(this, ent.getKey(),
Causes.UNCLAIMED);*/
}
}
/**
* NOTE: We used to fire libraryUnloaded on removes detected during resync. For that, we
* used an iterator. Without a listener callback, we have simplified.
*/
modules.keySet().retainAll(names);
}
protected String nameFromLine(String line) {
@ -290,45 +298,28 @@ public class GdbInferiorImpl implements GdbInferior {
}
protected void parseAndUpdateAllModuleSections(String[] lines) {
Set<String> namesSeen = new HashSet<>();
Set<String> modNamesSeen = new HashSet<>();
Set<String> secNamesSeen = new HashSet<>();
GdbModuleImpl curModule = null;
for (String line : lines) {
String name = nameFromLine(line);
if (name != null) {
if (curModule != null) {
curModule.loadSections.provide().complete(null);
curModule.resyncRetainSections(secNamesSeen);
secNamesSeen.clear();
}
namesSeen.add(name);
modNamesSeen.add(name);
curModule = modules.computeIfAbsent(name, this::resyncCreateModule);
// NOTE: This will usurp the module's lazy loader, but we're about to
// provide it anyway
if (curModule.loadSections.isDone()) {
curModule = null;
}
continue;
}
if (curModule == null) {
continue;
else if (curModule != null) {
curModule.processSectionLine(line, secNamesSeen);
}
curModule.processSectionLine(line);
}
if (curModule != null) {
curModule.loadSections.provide().complete(null);
curModule.resyncRetainSections(secNamesSeen);
// No need to clear secNamesSeen
}
resyncRetainModules(namesSeen);
}
protected Map<String, GdbModule> parseModuleNames(String[] lines) {
Set<String> namesSeen = new HashSet<>();
for (String line : lines) {
String name = nameFromLine(line);
if (name != null) {
namesSeen.add(name);
modules.computeIfAbsent(name, this::resyncCreateModule);
}
}
resyncRetainModules(namesSeen);
return unmodifiableModules;
resyncRetainModules(modNamesSeen);
}
@Override

View file

@ -1781,6 +1781,10 @@ public class GdbManagerImpl implements GdbManager {
return runningInterpreter;
}
private boolean isProbablyValid(String out) {
return out.contains("->0x");
}
private CompletableFuture<Map.Entry<String, String[]>> nextMaintInfoSections(
GdbInferiorImpl inferior, String cmds[], List<String[]> results) {
if (results.size() == cmds.length) {
@ -1795,7 +1799,7 @@ public class GdbManagerImpl implements GdbManager {
String cmd = cmds[results.size()];
return inferior.consoleCapture(cmd, CompletesWithRunning.CANNOT).thenCompose(out -> {
String[] lines = out.split("\n");
if (lines.length >= 10) {
if (isProbablyValid(out)) {
return CompletableFuture.completedFuture(Map.entry(cmd, lines));
}
results.add(lines);
@ -1824,7 +1828,7 @@ public class GdbManagerImpl implements GdbManager {
inferior.consoleCapture(maintInfoSectionsCmd, CompletesWithRunning.CANNOT);
return futureOut.thenCompose(out -> {
String[] lines = out.split("\n");
if (lines.length >= 10) {
if (isProbablyValid(out)) {
return CompletableFuture.completedFuture(lines);
}
CompletableFuture<Entry<String, String[]>> futureBest = nextMaintInfoSections(inferior,
@ -1889,4 +1893,10 @@ public class GdbManagerImpl implements GdbManager {
}
return null;
}
public void logInfo(String string) {
if (LOG_IO) {
DBG_LOG.println("INFO: " + string);
}
}
}

View file

@ -89,7 +89,6 @@ public class GdbModuleImpl implements GdbModule {
protected final Map<String, GdbModuleSectionImpl> sections = new LinkedHashMap<>();
protected final Map<String, GdbModuleSection> unmodifiableSections =
Collections.unmodifiableMap(sections);
protected final AsyncLazyValue<Void> loadSections = new AsyncLazyValue<>(this::doLoadSections);
protected final AsyncLazyValue<Map<String, GdbMinimalSymbol>> minimalSymbols =
new AsyncLazyValue<>(this::doGetMinimalSymbols);
@ -104,36 +103,19 @@ public class GdbModuleImpl implements GdbModule {
return name;
}
protected CompletableFuture<Void> doLoadSections() {
return inferior.doLoadSections();
}
@Override
public CompletableFuture<Long> computeBase() {
return loadSections.request().thenApply(__ -> base);
}
@Override
public CompletableFuture<Long> computeMax() {
return loadSections.request().thenApply(__ -> max);
}
@Override
public Long getKnownBase() {
public Long getBase() {
return base;
}
@Override
public Long getKnownMax() {
public Long getMax() {
return max;
}
@Override
public CompletableFuture<Map<String, GdbModuleSection>> listSections() {
if (sections.isEmpty() && loadSections.isDone()) {
loadSections.forget();
}
return loadSections.request().thenApply(__ -> unmodifiableSections);
public CompletableFuture<Map<String, GdbModuleSection>> listSections(boolean refresh) {
return inferior.listModules(refresh).thenApply(__ -> unmodifiableSections);
}
@Override
@ -168,7 +150,7 @@ public class GdbModuleImpl implements GdbModule {
return minimalSymbols.request();
}
protected void processSectionLine(String line) {
protected void processSectionLine(String line, Set<String> namesSeen) {
Matcher matcher = inferior.manager.matchSectionLine(line);
if (matcher != null) {
try {
@ -177,6 +159,7 @@ public class GdbModuleImpl implements GdbModule {
long offset = Long.parseLong(matcher.group("offset"), 16);
String sectionName = matcher.group("name");
namesSeen.add(sectionName);
List<String> attrs = new ArrayList<>();
for (String a : matcher.group("attrs").split("\\s+")) {
if (a.length() != 0) {
@ -191,7 +174,6 @@ public class GdbModuleImpl implements GdbModule {
if (sections.put(sectionName,
new GdbModuleSectionImpl(sectionName, vmaStart, vmaEnd, offset,
attrs)) != null) {
Msg.warn(this, "Duplicate section name: " + line);
}
}
catch (NumberFormatException e) {
@ -199,4 +181,16 @@ public class GdbModuleImpl implements GdbModule {
}
}
}
protected void resyncRetainSections(Set<String> names) {
/**
* The need for this seems dubious. If it's the same module, why would its sections ever
* change? However, in theory, a module could be unloaded, replaced on disk, and reloaded.
* If all those happen between two queries, then yes, it'll seem as though an existing
* module's sections changed.
*
* If we ever need a callback, we'll have to use an iterator-based implementation.
*/
sections.keySet().retainAll(names);
}
}

View file

@ -111,8 +111,8 @@ public class GdbModelTargetModule extends
}
protected AddressRange doGetRange() {
Long base = module.getKnownBase();
Long max = module.getKnownMax();
Long base = module.getBase();
Long max = module.getMax();
max = max == null ? base : (Long) (max - 1); // GDB gives end+1
if (base == null) {
Address addr = impl.space.getMinAddress();

View file

@ -101,12 +101,12 @@ public class GdbModelTargetModuleContainer
@Override
protected CompletableFuture<Void> requestElements(RefreshBehavior refresh) {
// Ignore 'refresh' because inferior.getKnownModules may exclude executable
return doRefresh();
// listModules is now cached by the manager
return doRefresh(refresh.isRefresh(elements.keySet()));
}
protected CompletableFuture<Void> doRefresh() {
return inferior.listModules().thenCompose(byName -> {
protected CompletableFuture<Void> doRefresh(boolean force) {
return inferior.listModules(force).thenCompose(byName -> {
for (String modName : inferior.getKnownModules().keySet()) {
if (!byName.keySet().contains(modName)) {
impl.deleteModelObject(byName.get(modName));
@ -129,7 +129,7 @@ public class GdbModelTargetModuleContainer
}
protected CompletableFuture<?> refreshInternal() {
return doRefresh().exceptionally(ex -> {
return doRefresh(false).exceptionally(ex -> {
impl.reportError(this, "Problem refreshing inferior's modules", ex);
return null;
});

View file

@ -21,6 +21,7 @@ import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import agent.gdb.manager.GdbModuleSection;
import ghidra.async.AsyncUtils;
import ghidra.dbg.DebuggerObjectModel.RefreshBehavior;
import ghidra.dbg.agent.DefaultTargetObject;
import ghidra.dbg.target.TargetObject;
@ -62,9 +63,10 @@ public class GdbModelTargetSectionContainer
@Override
public CompletableFuture<Void> requestElements(RefreshBehavior refresh) {
// getKnownSections is not guaranteed to be populated
// listSections is cached by manager, so just use it always
return module.module.listSections().thenAccept(this::updateUsingSections);
if (!refresh.isRefresh(elements.keySet())) {
return AsyncUtils.NIL;
}
return module.module.listSections(true).thenAccept(this::updateUsingSections);
}
protected synchronized GdbModelTargetSection getTargetSection(GdbModuleSection section) {

View file

@ -167,10 +167,10 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
waitOn(startManager(mgr));
GdbInferior inferior = mgr.currentInferior();
waitOn(inferior.fileExecAndSymbols("/usr/bin/echo"));
Map<String, GdbModule> modules = waitOn(inferior.listModules());
Map<String, GdbModule> modules = waitOn(inferior.listModules(false));
GdbModule modEcho = modules.get("/usr/bin/echo");
assertNotNull(modEcho);
Map<String, GdbModuleSection> sectionsEcho = waitOn(modEcho.listSections());
Map<String, GdbModuleSection> sectionsEcho = waitOn(modEcho.listSections(false));
GdbModuleSection secEchoText = sectionsEcho.get(".text");
assertNotNull(secEchoText);
}

View file

@ -133,7 +133,9 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter
@SuppressWarnings("unused")
private final AutoService.Wiring autoServiceWiring;
@AutoOptionDefined(name = "Default Extended Step", description = "The default string for the extended step command")
@AutoOptionDefined(
name = "Default Extended Step",
description = "The default string for the extended step command")
String extendedStep = "";
@SuppressWarnings("unused")
@ -491,7 +493,7 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter
public void addTable(ObjectContainer container) {
AtomicReference<ObjectContainer> update = new AtomicReference<>();
AsyncUtils.sequence(TypeSpec.cls(ObjectContainer.class)).then(seq -> {
container.getOffspring().handle(seq::next);
container.getOffspring(RefreshBehavior.REFRESH_WHEN_ABSENT).handle(seq::next);
}, update).then(seq -> {
try {
ObjectContainer oc = update.get();
@ -1242,9 +1244,14 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter
public void performRefresh(ActionContext context) {
TargetObject current = getObjectFromContext(context);
if (current != null) {
current.resync(RefreshBehavior.REFRESH_ALWAYS, RefreshBehavior.REFRESH_ALWAYS);
refresh(current.getName());
}
else {
TargetObject modelRoot = getModel().getModelRoot();
if (modelRoot != null) {
modelRoot.resync(RefreshBehavior.REFRESH_ALWAYS, RefreshBehavior.REFRESH_ALWAYS);
}
refresh();
}
}

View file

@ -153,11 +153,11 @@ public class ObjectContainer implements Comparable<ObjectContainer> {
* p.update
*/
public CompletableFuture<ObjectContainer> getOffspring() {
public CompletableFuture<ObjectContainer> getOffspring(RefreshBehavior refresh) {
if (targetObject == null) {
return CompletableFuture.completedFuture(null);
}
return targetObject.resync(RefreshBehavior.REFRESH_ALWAYS, RefreshBehavior.REFRESH_ALWAYS).thenApplyAsync(__ -> {
return targetObject.resync(refresh, refresh).thenApplyAsync(__ -> {
rebuildContainers(targetObject.getCachedElements(), targetObject.getCachedAttributes());
propagateProvider(provider);
return this;

View file

@ -24,6 +24,7 @@ import docking.widgets.tree.*;
import generic.theme.GIcon;
import ghidra.app.plugin.core.debug.gui.objects.DebuggerObjectsProvider;
import ghidra.app.plugin.core.debug.gui.objects.ObjectContainer;
import ghidra.dbg.DebuggerObjectModel.RefreshBehavior;
import ghidra.dbg.target.*;
import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState;
import ghidra.util.Msg;
@ -77,11 +78,13 @@ public class ObjectNode extends GTreeSlowLoadingNode { //extends GTreeNode
}
@Override
public List<GTreeNode> generateChildren(TaskMonitor monitor) throws CancelledException {
public List<GTreeNode> generateChildren(TaskMonitor monitor)
throws CancelledException {
if (!container.isImmutable() || isInProgress()) {
try {
CompletableFuture<ObjectContainer> cf = container.getOffspring();
CompletableFuture<ObjectContainer> cf =
container.getOffspring(RefreshBehavior.REFRESH_WHEN_ABSENT);
if (cf != null) {
// NB: We're allowed to do this because we're guaranteed to be
// in our own thread by the GTreeSlowLoadingNode

View file

@ -89,7 +89,7 @@ public class DefaultModuleRecorder implements ManagedModuleRecorder {
return traceModule.addSection(path, section.getIndex(), traceRange);
}
catch (DuplicateNameException e) {
Msg.warn(this, path + " already recorded");
// Msg.warn(this, path + " already recorded");
return moduleManager.getLoadedSectionByPath(snap, path);
}
}
@ -123,6 +123,27 @@ public class DefaultModuleRecorder implements ManagedModuleRecorder {
}
}
public void moduleChanged(TargetModule module, AddressRange traceRng) {
long snap = recorder.getSnap();
String path = module.getJoinedPath(".");
recorder.parTx.execute("Module " + path + " range updated", () -> {
doModuleChanged(snap, path, traceRng);
}, path);
}
protected void doModuleChanged(long snap, String path, AddressRange traceRng) {
TraceModule traceModule = moduleManager.getLoadedModuleByPath(snap, path);
if (traceModule == null) {
Msg.warn(this, "changed " + path + " is not in the trace");
return;
}
/**
* Yes, this will modify the module's previous history, which technically could be
* incorrect. The occasion should be rare, and the OBTR will handle it correctly.
*/
traceModule.setRange(traceRng);
}
@Override
public void removeProcessModule(TargetModule module) {
long snap = recorder.getSnap();

View file

@ -118,6 +118,7 @@ public class TraceObjectManager {
putAttributesHandler(TargetBreakpointLocation.class,
this::attributesChangedBreakpointLocation);
putAttributesHandler(TargetMemoryRegion.class, this::attributesChangedMemoryRegion);
putAttributesHandler(TargetModule.class, this::attributesChangedModule);
putAttributesHandler(TargetRegister.class, this::attributesChangedRegister);
putAttributesHandler(TargetStackFrame.class, this::attributesChangedStackFrame);
putAttributesHandler(TargetThread.class, this::attributesChangedThread);
@ -147,11 +148,12 @@ public class TraceObjectManager {
});
}
@SuppressWarnings("unchecked")
private <U extends TargetObject> BiFunction<TargetObject, Map<String, ?>, Void> putHandler(
Class<?> key, BiConsumer<TargetObject, Map<String, ?>> handler,
Class<?> key, BiConsumer<U, Map<String, ?>> handler,
LinkedHashMap<Class<?>, BiFunction<TargetObject, Map<String, ?>, Void>> handlerMap) {
return handlerMap.put(key, (u, v) -> {
handler.accept(u, v);
handler.accept((U) u, v);
return null;
});
}
@ -172,7 +174,7 @@ public class TraceObjectManager {
}
public <U extends TargetObject> BiFunction<TargetObject, Map<String, ?>, Void> putAttributesHandler(
Class<?> key, BiConsumer<TargetObject, Map<String, ?>> handler) {
Class<U> key, BiConsumer<U, Map<String, ?>> handler) {
return putHandler(key, handler, handlerMapAttributes);
}
@ -509,6 +511,13 @@ public class TraceObjectManager {
}
}
public void attributesChangedModule(TargetModule module, Map<String, ?> added) {
if (added.containsKey(TargetModule.RANGE_ATTRIBUTE_NAME)) {
AddressRange traceRng = recorder.getMemoryMapper().targetToTrace(module.getRange());
recorder.moduleRecorder.moduleChanged(module, traceRng);
}
}
public void attributesChangedRegister(TargetObject parent, Map<String, ?> added) {
if (added.containsKey(TargetRegister.CONTAINER_ATTRIBUTE_NAME)) {
TargetRegister register = (TargetRegister) parent;

View file

@ -132,6 +132,7 @@ public class AsyncLazyValue<T> {
/**
* Check if the value has been requested, but not yet completed
*
* <p>
* This will also return true if something is providing the value out of band.
*
* @return true if {@link #request()} or {@link #provide()} has been called, but not completed

View file

@ -21,7 +21,6 @@ import java.util.concurrent.RejectedExecutionException;
import java.util.function.Predicate;
import ghidra.async.*;
import ghidra.dbg.DebuggerObjectModel.RefreshBehavior;
import ghidra.dbg.error.*;
import ghidra.dbg.target.TargetMemory;
import ghidra.dbg.target.TargetObject;
@ -65,13 +64,30 @@ import ghidra.util.Msg;
* risk deadlocking Ghidra's UI.
*/
public interface DebuggerObjectModel {
public static enum RefreshBehavior {
REFRESH_ALWAYS,
REFRESH_NEVER,
REFRESH_WHEN_ABSENT
REFRESH_ALWAYS {
@Override
public boolean isRefresh(Collection<?> col) {
return true;
}
},
REFRESH_NEVER {
@Override
public boolean isRefresh(Collection<?> col) {
return false;
}
},
REFRESH_WHEN_ABSENT {
@Override
public boolean isRefresh(Collection<?> col) {
return col.isEmpty();
}
};
public abstract boolean isRefresh(Collection<?> col);
}
public static final TypeSpec<Map<String, ? extends TargetObject>> ELEMENT_MAP_TYPE =
TypeSpec.auto();
public static final TypeSpec<Map<String, ?>> ATTRIBUTE_MAP_TYPE = TypeSpec.auto();