program =
- OpenedDomainFile.open(Program.class, libFile, monitor)) {
- collectLibraries(project, program.content, col, monitor);
- }
- catch (ClassCastException e) {
- Msg.info(DebuggerStaticMappingUtils.class,
- "Referenced external program is not a program: " + libPath + " is " +
- libFile.getDomainObjectClass());
- continue;
- }
- catch (VersionException | CancelledException | IOException e) {
- Msg.info(DebuggerStaticMappingUtils.class,
- "Referenced external program could not be opened: " + e);
continue;
}
+ collectLibraries(project, libFile, col);
}
}
/**
- * Recursively collect external programs, i.e., libraries, starting at the given seed
+ * Recursively collect external programs, i.e., libraries, starting at the given seeds
*
- * @param seed the seed, usually the executable
- * @param monitor a monitor to cancel the process
- * @return the set of found programs, including the seed
- * @throws CancelledException if cancelled by the monitor
+ *
+ * This will only descend into domain files that are already opened. This will only include
+ * results whose content type is a {@link Program}.
+ *
+ * @param seeds the seeds, usually including the executable
+ * @return the set of found domain files, including the seeds
*/
- public static Set collectLibraries(Program seed, TaskMonitor monitor)
- throws CancelledException {
- Set result = new LinkedHashSet<>();
- collectLibraries(seed.getDomainFile().getParent().getProjectData(), seed, result,
- monitor);
+ public static Set collectLibraries(Collection seeds) {
+ Set result = new LinkedHashSet<>();
+ for (DomainFile seed : seeds) {
+ collectLibraries(seed.getParent().getProjectData(), seed, result);
+ }
return result;
}
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DefaultModuleMapProposal.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DefaultModuleMapProposal.java
index 5c21fa79ee..3382dbbb6a 100644
--- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DefaultModuleMapProposal.java
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DefaultModuleMapProposal.java
@@ -84,6 +84,7 @@ public class DefaultModuleMapProposal
}
protected AddressRange moduleRange;
+ protected boolean memorize = false;
/**
* Construct a module map entry
@@ -146,6 +147,16 @@ public class DefaultModuleMapProposal
throw new AssertionError(e);
}
}
+
+ @Override
+ public boolean isMemorize() {
+ return memorize;
+ }
+
+ @Override
+ public void setMemorize(boolean memorize) {
+ this.memorize = memorize;
+ }
}
protected final TraceModule module;
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/PeekOpenedDomainObject.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/PeekOpenedDomainObject.java
new file mode 100644
index 0000000000..0e7ce10561
--- /dev/null
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/PeekOpenedDomainObject.java
@@ -0,0 +1,34 @@
+/* ###
+ * 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.app.plugin.core.debug.service.modules;
+
+import ghidra.framework.model.DomainFile;
+import ghidra.framework.model.DomainObject;
+
+public class PeekOpenedDomainObject implements AutoCloseable {
+ public final DomainObject object;
+
+ public PeekOpenedDomainObject(DomainFile df) {
+ this.object = df.getOpenedDomainObject(this);
+ }
+
+ @Override
+ public void close() {
+ if (object != null) {
+ object.release(this);
+ }
+ }
+}
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/ProgramModuleIndexer.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/ProgramModuleIndexer.java
new file mode 100644
index 0000000000..15f59ee71a
--- /dev/null
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/ProgramModuleIndexer.java
@@ -0,0 +1,395 @@
+/* ###
+ * 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.app.plugin.core.debug.service.modules;
+
+import java.io.File;
+import java.util.*;
+import java.util.stream.Collectors;
+
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+
+import ghidra.app.plugin.core.debug.utils.DomainFolderChangeAdapter;
+import ghidra.app.plugin.core.debug.utils.ProgramURLUtils;
+import ghidra.framework.model.*;
+import ghidra.framework.options.Options;
+import ghidra.framework.plugintool.PluginTool;
+import ghidra.program.model.address.AddressRangeImpl;
+import ghidra.program.model.address.AddressSpace;
+import ghidra.program.model.listing.Program;
+import ghidra.trace.model.modules.TraceModule;
+
+// TODO: Consider making this a front-end plugin?
+public class ProgramModuleIndexer implements DomainFolderChangeAdapter {
+ public static final String MODULE_PATHS_PROPERTY = "Module Paths";
+ private static final Gson JSON = new Gson();
+
+ public static void setModulePaths(Program program, Collection moduleNames) {
+ Options options = program.getOptions(Program.PROGRAM_INFO);
+ LinkedHashSet distinct = moduleNames instanceof LinkedHashSet yes ? yes
+ : new LinkedHashSet<>(moduleNames);
+ options.setString(MODULE_PATHS_PROPERTY, JSON.toJson(distinct));
+ }
+
+ public static Collection getModulePaths(DomainFile df) {
+ return getModulePaths(df.getMetadata());
+ }
+
+ public static Collection getModulePaths(Map metadata) {
+ String json = metadata.get(MODULE_PATHS_PROPERTY);
+ if (json == null) {
+ return List.of();
+ }
+ return JSON.fromJson(json, new TypeToken>() {}.getType());
+ }
+
+ public static void addModulePaths(Program program, Collection moduleNames) {
+ LinkedHashSet union = new LinkedHashSet<>(getModulePaths(program.getMetadata()));
+ union.addAll(moduleNames);
+ setModulePaths(program, union);
+ }
+
+ protected enum NameSource {
+ MODULE_PATH,
+ MODULE_NAME,
+ PROGRAM_EXECUTABLE_PATH,
+ PROGRAM_EXECUTABLE_NAME,
+ PROGRAM_NAME,
+ DOMAIN_FILE_NAME,
+ }
+
+ // TODO: Note language and prefer those from the same processor?
+ // Will get difficult with new OBTR, since I'd need a platform
+ // There's also the WoW64 issue....
+ protected record IndexEntry(String name, String dfID, NameSource source) {
+ }
+
+ protected class ModuleChangeListener
+ implements DomainObjectListener, DomainObjectClosedListener {
+ private final Program program;
+
+ public ModuleChangeListener(Program program) {
+ this.program = program;
+ program.addListener(this);
+ program.addCloseListener(this);
+ return;
+ }
+
+ protected void dispose() {
+ program.removeListener(this);
+ program.removeCloseListener(this);
+ }
+
+ @Override
+ public void domainObjectClosed() {
+ dispose();
+ }
+
+ @Override
+ public void domainObjectChanged(DomainObjectChangedEvent ev) {
+ if (disposed) {
+ return;
+ }
+ if (ev.containsEvent(DomainObject.DO_OBJECT_RESTORED)) {
+ refreshIndex(program.getDomainFile(), program);
+ return;
+ }
+ if (ev.containsEvent(DomainObject.DO_PROPERTY_CHANGED)) {
+ for (DomainObjectChangeRecord rec : ev) {
+ if (rec.getEventType() == DomainObject.DO_PROPERTY_CHANGED) {
+ // OldValue is actually the property name :/
+ // See DomainObjectAdapter#propertyChanged
+ String propertyName = (String) rec.getOldValue();
+ if ((Program.PROGRAM_INFO + "." + MODULE_PATHS_PROPERTY)
+ .equals(propertyName)) {
+ refreshIndex(program.getDomainFile(), program);
+ return;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ protected static class MapOfSets {
+ public final Map> map = new HashMap<>();
+
+ public void put(K key, V value) {
+ map.computeIfAbsent(key, k -> new HashSet<>()).add(value);
+ }
+
+ public void remove(K key, V value) {
+ Set set = map.get(key);
+ if (set == null) {
+ return;
+ }
+ set.remove(value);
+ if (set.isEmpty()) {
+ map.remove(key);
+ }
+ }
+ }
+
+ protected static class ModuleIndex {
+ final MapOfSets entriesByName = new MapOfSets<>();
+ final MapOfSets entriesByFile = new MapOfSets<>();
+
+ void addEntry(String name, String dfID, NameSource source) {
+ IndexEntry entry = new IndexEntry(name, dfID, source);
+ entriesByName.put(name, entry);
+ entriesByFile.put(dfID, entry);
+ }
+
+ void removeEntry(IndexEntry entry) {
+ entriesByName.remove(entry.name, entry);
+ entriesByFile.remove(entry.dfID, entry);
+ }
+
+ void removeFile(String fileID) {
+ Set remove = entriesByFile.map.remove(fileID);
+ if (remove == null) {
+ return;
+ }
+ for (IndexEntry entry : remove) {
+ entriesByName.remove(entry.name, entry);
+ }
+ }
+
+ public Collection getByName(String name) {
+ return entriesByName.map.getOrDefault(name, Set.of());
+ }
+ }
+
+ private final Project project;
+ private final ProjectData projectData;
+ private volatile boolean disposed;
+
+ private final Map openedForUpdate = new HashMap<>();
+ private final ModuleIndex index = new ModuleIndex();
+
+ public ProgramModuleIndexer(PluginTool tool) {
+ this.project = tool.getProject();
+ this.projectData = tool.getProject().getProjectData();
+ this.projectData.addDomainFolderChangeListener(this);
+
+ indexFolder(projectData.getRootFolder());
+ }
+
+ void dispose() {
+ disposed = true;
+ projectData.removeDomainFolderChangeListener(this);
+ }
+
+ protected void indexFolder(DomainFolder folder) {
+ for (DomainFile file : folder.getFiles()) {
+ addToIndex(file);
+ }
+ for (DomainFolder sub : folder.getFolders()) {
+ indexFolder(sub);
+ }
+ }
+
+ protected void addToIndex(DomainFile file, Program program) {
+ if (disposed) {
+ return;
+ }
+ addToIndex(file, program.getMetadata());
+ }
+
+ protected void addToIndex(DomainFile file) {
+ if (disposed) {
+ return;
+ }
+ if (!Program.class.isAssignableFrom(file.getDomainObjectClass())) {
+ return;
+ }
+ addToIndex(file, file.getMetadata());
+ }
+
+ protected void addToIndex(DomainFile file, Map metadata) {
+ String dfID = file.getFileID();
+
+ String dfName = file.getName().toLowerCase();
+ String progName = metadata.get("Program Name");
+ if (progName != null) {
+ progName = progName.toLowerCase();
+ }
+ String exePath = metadata.get("Executable Location");
+ if (exePath != null) {
+ exePath = exePath.toLowerCase();
+ }
+ String exeName = exePath == null ? null : new File(exePath).getName();
+
+ for (String modPath : getModulePaths(metadata)) {
+ String modName = new File(modPath).getName();
+ if (!modPath.equals(modName)) {
+ index.addEntry(modPath, dfID, NameSource.MODULE_PATH);
+ }
+ index.addEntry(modName, dfID, NameSource.MODULE_NAME);
+ }
+
+ index.addEntry(dfName, dfID, NameSource.DOMAIN_FILE_NAME);
+ if (progName != null) {
+ index.addEntry(progName, dfID, NameSource.DOMAIN_FILE_NAME);
+ }
+ if (exeName != null) {
+ if (!exePath.equals(exeName)) {
+ index.addEntry(exePath, dfID, NameSource.PROGRAM_EXECUTABLE_PATH);
+ }
+ index.addEntry(exeName, dfID, NameSource.PROGRAM_EXECUTABLE_NAME);
+ }
+ }
+
+ protected void removeFromIndex(String fileID) {
+ index.removeFile(fileID);
+ }
+
+ protected void refreshIndex(DomainFile file) {
+ removeFromIndex(file.getFileID());
+ addToIndex(file);
+ }
+
+ protected void refreshIndex(DomainFile file, Program program) {
+ removeFromIndex(file.getFileID());
+ addToIndex(file, program);
+ }
+
+ @Override
+ public void domainFileAdded(DomainFile file) {
+ addToIndex(file);
+ }
+
+ @Override
+ public void domainFileRemoved(DomainFolder parent, String name, String fileID) {
+ removeFromIndex(fileID);
+ }
+
+ @Override
+ public void domainFileRenamed(DomainFile file, String oldName) {
+ refreshIndex(file);
+ }
+
+ @Override
+ public void domainFileMoved(DomainFile file, DomainFolder oldParent, String oldName) {
+ refreshIndex(file);
+ }
+
+ @Override
+ public void domainFileObjectReplaced(DomainFile file, DomainObject oldObject) {
+ refreshIndex(file);
+ }
+
+ @Override
+ public void domainFileObjectOpenedForUpdate(DomainFile file, DomainObject object) {
+ if (disposed) {
+ return;
+ }
+ if (object instanceof Program program) {
+ synchronized (openedForUpdate) {
+ openedForUpdate.computeIfAbsent(program, ModuleChangeListener::new);
+ }
+ }
+ }
+
+ @Override
+ public void domainFileObjectClosed(DomainFile file, DomainObject object) {
+ if (disposed) {
+ return;
+ }
+ synchronized (openedForUpdate) {
+ ModuleChangeListener listener = openedForUpdate.remove(object);
+ if (listener != null) {
+ listener.dispose();
+ }
+ }
+ }
+
+ private DomainFile selectBest(List entries, Set libraries,
+ Map folderUses, Program currentProgram) {
+ if (currentProgram != null) {
+ DomainFile currentFile = currentProgram.getDomainFile();
+ if (currentFile != null) {
+ String currentID = currentFile.getFileID();
+ for (IndexEntry entry : entries) {
+ if (entry.dfID.equals(currentID)) {
+ return currentFile;
+ }
+ }
+ }
+ }
+ Comparator byIsLibrary = Comparator.comparing(e -> {
+ DomainFile df = projectData.getFileByID(e.dfID);
+ return libraries.contains(df) ? 1 : 0;
+ });
+ Comparator byNameSource = Comparator.comparing(e -> -e.source.ordinal());
+ Map folderScores = new HashMap<>();
+ Comparator byFolderUses = Comparator.comparing(e -> {
+ return folderScores.computeIfAbsent(e, k -> {
+ DomainFile df = projectData.getFileByID(k.dfID);
+ int score = 0;
+ for (DomainFolder folder = df.getParent(); folder != null; folder =
+ folder.getParent()) {
+ score += folderUses.getOrDefault(folder, 0);
+ }
+ return score;
+ });
+ });
+ /**
+ * It's not clear if being a library of an already-mapped program should override a
+ * user-provided module name.... That said, unless there are already bogus mappings in the
+ * trace, or bogus external libraries in a mapped program, scoring libraries before module
+ * names should not cause problems.
+ */
+ Comparator comparator = byIsLibrary
+ .thenComparing(byNameSource)
+ .thenComparing(byFolderUses);
+ return projectData.getFileByID(entries.stream().max(comparator).get().dfID);
+ }
+
+ public DomainFile getBestMatch(AddressSpace space, TraceModule module, Program currentProgram) {
+ Map folderUses = new HashMap<>();
+ Set alreadyMapped = module.getTrace()
+ .getStaticMappingManager()
+ .findAllOverlapping(
+ new AddressRangeImpl(space.getMinAddress(), space.getMaxAddress()),
+ module.getLifespan())
+ .stream()
+ .map(m -> ProgramURLUtils.getFileForHackedUpGhidraURL(project,
+ m.getStaticProgramURL()))
+ .collect(Collectors.toSet());
+ Set libraries = DebuggerStaticMappingUtils.collectLibraries(alreadyMapped);
+ alreadyMapped.stream()
+ .map(df -> df.getParent())
+ .filter(folder -> folder.getProjectData() == projectData)
+ .forEach(folder -> {
+ for (; folder != null; folder = folder.getParent()) {
+ folderUses.compute(folder, (f, c) -> c == null ? 1 : (c + 1));
+ }
+ });
+
+ String modulePathName = module.getName().toLowerCase();
+ List entries = new ArrayList<>(index.getByName(modulePathName));
+ if (!entries.isEmpty()) {
+ return selectBest(entries, libraries, folderUses, currentProgram);
+ }
+ String moduleFileName = new File(modulePathName).getName();
+ entries.addAll(index.getByName(moduleFileName));
+ if (!entries.isEmpty()) {
+ return selectBest(entries, libraries, folderUses, currentProgram);
+ }
+ return null;
+ }
+}
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerConsoleService.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerConsoleService.java
index beb7713279..5eac50922f 100644
--- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerConsoleService.java
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerConsoleService.java
@@ -15,6 +15,8 @@
*/
package ghidra.app.services;
+import java.util.List;
+
import javax.swing.Icon;
import docking.ActionContext;
@@ -72,6 +74,13 @@ public interface DebuggerConsoleService extends DebuggerConsoleLogger {
*/
boolean logContains(ActionContext context);
+ /**
+ * Get the action context for all actionable messages
+ *
+ * @return a copy of the collection of contexts, in no particular order
+ */
+ List getActionContexts();
+
/**
* Add an action which might be applied to an actionable log message
*
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerStaticMappingService.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerStaticMappingService.java
index f4709836fe..f1ba38d153 100644
--- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerStaticMappingService.java
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerStaticMappingService.java
@@ -233,6 +233,10 @@ public interface DebuggerStaticMappingService {
* entry fails, including due to conflicts, that failure is logged but ignored, and the
* remaining entries are processed.
*
+ *
+ * Any entries indicated for memorization will have their module paths added to the destination
+ * program's metadata.
+ *
* @param entries the entries to add
* @param monitor a monitor to cancel the operation
* @param truncateExisting true to delete or truncate the lifespan of overlapping entries
@@ -410,19 +414,23 @@ public interface DebuggerStaticMappingService {
CompletableFuture changesSettled();
/**
- * Collect likely matches for destination programs for the given trace module
+ * Find the best match among programs in the project for the given trace module
*
*
- * If the trace is saved in a project, this will search that project preferring its siblings; if
- * no sibling are probable, it will try the rest of the project. Otherwise, it will search the
- * current project. "Probable" leaves room for implementations to use any number of heuristics
- * available, e.g., name, path, type; however, they should refrain from opening or checking out
- * domain files.
+ * The service maintains an index of likely module names to domain files in the active project.
+ * This will search that index for the module's full file path. Failing that, it will search
+ * just for the module's file name. Among the programs found, it first prefers those whose
+ * module name list (see {@link ProgramModuleIndexer#setModulePaths(Program, List)}) include the
+ * sought module. Then, it prefers those whose executable path (see
+ * {@link Program#setExecutablePath(String)}) matches the sought module. Finally, it prefers
+ * matches on the program name and the domain file name. Ties in name matching are broken by
+ * looking for domain files in the same folders as those programs already mapped into the trace
+ * in the given address space.
*
* @param module the trace module
* @return the, possibly empty, set of probable matches
*/
- Set findProbableModulePrograms(TraceModule module);
+ DomainFile findBestModuleProgram(AddressSpace space, TraceModule module);
/**
* Propose a module map for the given module to the given program
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/ModuleMapProposal.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/ModuleMapProposal.java
index 263368960e..7090f11b8c 100644
--- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/ModuleMapProposal.java
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/ModuleMapProposal.java
@@ -51,6 +51,20 @@ public interface ModuleMapProposal extends MapProposal