From 835127c928f47a263fcad6e768fc61e7cdffbbbe Mon Sep 17 00:00:00 2001 From: Dan <46821332+nsadeveloper789@users.noreply.github.com> Date: Mon, 9 Jan 2023 10:26:20 -0500 Subject: [PATCH] GP-1527: Improve automatic module mapping, including optional memorization. --- .../DebuggerModulesPlugin.html | 7 +- .../DebuggerModuleMapProposalDialog.png | Bin 15193 -> 15353 bytes .../console/ConsoleActionsCellRenderer.java | 4 + .../gui/console/DebuggerConsolePlugin.java | 7 + .../gui/console/DebuggerConsoleProvider.java | 8 +- .../gui/listing/DebuggerListingProvider.java | 61 ++- .../DebuggerModuleMapProposalDialog.java | 3 +- .../gui/modules/DebuggerModulesProvider.java | 2 - .../DebuggerStaticMappingServicePlugin.java | 24 +- .../modules/DebuggerStaticMappingUtils.java | 144 ++----- .../modules/DefaultModuleMapProposal.java | 11 + .../modules/PeekOpenedDomainObject.java | 34 ++ .../service/modules/ProgramModuleIndexer.java | 395 ++++++++++++++++++ .../app/services/DebuggerConsoleService.java | 9 + .../DebuggerStaticMappingService.java | 22 +- .../app/services/ModuleMapProposal.java | 14 + 16 files changed, 603 insertions(+), 142 deletions(-) create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/PeekOpenedDomainObject.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/ProgramModuleIndexer.java diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerModulesPlugin/DebuggerModulesPlugin.html b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerModulesPlugin/DebuggerModulesPlugin.html index 3aaa071222..68ad1d0a5f 100644 --- a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerModulesPlugin/DebuggerModulesPlugin.html +++ b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerModulesPlugin/DebuggerModulesPlugin.html @@ -109,7 +109,9 @@ open programs for the selected modules and proposes new mappings. The user can examine and tweak the proposal before confirming or canceling it. Typically, this is done automatically by the Map Modules debugger - bot.

+ bot. By selecting "Memorize" and confirming the dialog, the user can cause the mapper to re-use + the memorized mapping in future sessions. The memorized module name is saved to the program + database.

@@ -124,7 +126,8 @@

This action is available from a single module's pop-up menu, when there is an open program. It behaves like Map Modules, except that it will propose the selected module be mapped to the - current program.

+ current program. This action with the "Memorize" toggle is a good way to override or specify a + module mapping once and for all.

Map Sections

diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerModulesPlugin/images/DebuggerModuleMapProposalDialog.png b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerModulesPlugin/images/DebuggerModuleMapProposalDialog.png index 66a259c15620779ce5750fe810dd831345f1de7a..b30098225d81129cfcbc537f95fa9b25662341e5 100644 GIT binary patch literal 15353 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 H#ruB%Dgp6` diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/ConsoleActionsCellRenderer.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/ConsoleActionsCellRenderer.java index dba02d6ef0..531c03eb3d 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/ConsoleActionsCellRenderer.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/ConsoleActionsCellRenderer.java @@ -52,6 +52,10 @@ public class ConsoleActionsCellRenderer extends AbstractGhidraColumnRenderer buttonCache, ActionList value, Consumer extraConfig) { box.removeAll(); + if (value == null) { + // IDK how this is happening.... An empty row or something? + return; + } ensureCacheSize(buttonCache, value.size(), extraConfig); int i = 0; for (BoundAction a : value) { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/DebuggerConsolePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/DebuggerConsolePlugin.java index eef802210f..417ab1222f 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/DebuggerConsolePlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/DebuggerConsolePlugin.java @@ -15,6 +15,8 @@ */ package ghidra.app.plugin.core.debug.gui.console; +import java.util.List; + import javax.swing.Icon; import org.apache.logging.log4j.Level; @@ -120,6 +122,11 @@ public class DebuggerConsolePlugin extends Plugin implements DebuggerConsoleServ return provider.logContains(context); } + @Override + public List getActionContexts() { + return provider.getActionContexts(); + } + @Override public void addResolutionAction(DockingActionIf action) { provider.addResolutionAction(action); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/DebuggerConsoleProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/DebuggerConsoleProvider.java index b3d68bac88..c0513000cf 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/DebuggerConsoleProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/DebuggerConsoleProvider.java @@ -177,7 +177,7 @@ public class DebuggerConsoleProvider extends ComponentProviderAdapter this.message = message; this.date = date; this.context = context; - this.actions = actions; + this.actions = Objects.requireNonNull(actions); } public Icon getIcon() { @@ -456,6 +456,12 @@ public class DebuggerConsoleProvider extends ComponentProviderAdapter } } + protected List getActionContexts() { + synchronized (buffer) { + return List.copyOf(logTableModel.getMap().keySet()); + } + } + protected void addResolutionAction(DockingActionIf action) { DockingActionIf replaced = actionsByOwnerThenName.computeIfAbsent(action.getOwner(), o -> new LinkedHashMap<>()) diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProvider.java index 0923321f72..108c385299 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProvider.java @@ -30,6 +30,7 @@ import javax.swing.event.ChangeListener; import org.apache.commons.lang3.StringUtils; import org.jdom.Element; +import docking.ActionContext; import docking.WindowPosition; import docking.action.DockingAction; import docking.action.ToggleDockingAction; @@ -63,8 +64,7 @@ import ghidra.framework.plugintool.AutoConfigState; import ghidra.framework.plugintool.AutoService; import ghidra.framework.plugintool.annotation.AutoConfigStateField; import ghidra.framework.plugintool.annotation.AutoServiceConsumed; -import ghidra.program.model.address.Address; -import ghidra.program.model.address.AddressSetView; +import ghidra.program.model.address.*; import ghidra.program.model.listing.Program; import ghidra.program.util.ProgramLocation; import ghidra.program.util.ProgramSelection; @@ -124,13 +124,8 @@ public class DebuggerListingProvider extends CodeViewerProvider { return; } doMarkTrackedLocation(); + cleanMissingModuleMessages(affectedTraces); }); - - /** - * TODO: Remove "missing" entry in modules dialog, if present? There's some nuance here, - * because the trace presenting the mapping may not be the same as the trace that missed - * the module originally. I'm tempted to just leave it and let the user remove it. - */ } } @@ -859,6 +854,10 @@ public class DebuggerListingProvider extends CodeViewerProvider { if (loc == null) { // Redundant? return; } + AddressSpace space = loc.getAddress().getAddressSpace(); + if (space == null) { + return; // Is this NO_ADDRESS or something? + } if (mappingService == null) { return; } @@ -887,22 +886,21 @@ public class DebuggerListingProvider extends CodeViewerProvider { modMan.getSectionsAt(snap, address).stream().map(s -> s.getModule())) .collect(Collectors.toSet()); - // Attempt to open probable matches. All others, attempt to import + // Attempt to open probable matches. All others, list to import // TODO: What if sections are not presented? for (TraceModule mod : modules) { - Set matches = mappingService.findProbableModulePrograms(mod); - if (matches.isEmpty()) { + DomainFile match = mappingService.findBestModuleProgram(space, mod); + if (match == null) { missing.add(mod); } else { - toOpen.addAll(matches); + toOpen.add(match); } } if (programManager != null && !toOpen.isEmpty()) { for (DomainFile df : toOpen) { // Do not presume a goTo is about to happen. There are no mappings, yet. - doTryOpenProgram(df, DomainFile.DEFAULT_VERSION, - ProgramManager.OPEN_VISIBLE); + doTryOpenProgram(df, DomainFile.DEFAULT_VERSION, ProgramManager.OPEN_VISIBLE); } } @@ -917,12 +915,41 @@ public class DebuggerListingProvider extends CodeViewerProvider { new DebuggerMissingModuleActionContext(mod)); } /** - * Once the programs are opened, including those which are successfully imported, the - * section mapper should take over, eventually invoking callbacks to our mapping change - * listener. + * Once the programs are opened, including those which are successfully imported, the mapper + * bot should take over, eventually invoking callbacks to our mapping change listener. */ } + protected boolean isMapped(AddressRange range) { + if (range == null) { + return false; + } + return mappingService.getStaticLocationFromDynamic( + new ProgramLocation(getProgram(), range.getMinAddress())) != null; + } + + protected void cleanMissingModuleMessages(Set affectedTraces) { + nextCtx: for (ActionContext ctx : consoleService.getActionContexts()) { + if (!(ctx instanceof DebuggerMissingModuleActionContext mmCtx)) { + continue; + } + TraceModule module = mmCtx.getModule(); + if (!affectedTraces.contains(module.getTrace())) { + continue; + } + if (isMapped(module.getRange())) { + consoleService.removeFromLog(mmCtx); + continue; + } + for (TraceSection section : module.getSections()) { + if (isMapped(section.getRange())) { + consoleService.removeFromLog(mmCtx); + continue nextCtx; + } + } + } + } + public void setTrackingSpec(LocationTrackingSpec spec) { trackingTrait.setSpec(spec); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModuleMapProposalDialog.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModuleMapProposalDialog.java index 64272d00a5..327a976af7 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModuleMapProposalDialog.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModuleMapProposalDialog.java @@ -48,7 +48,8 @@ public class DebuggerModuleMapProposalDialog ? e.getToProgram().getName() : e.getToProgram().getDomainFile().getName())), STATIC_BASE("Static Base", Address.class, e -> e.getToProgram().getImageBase()), - SIZE("Size", Long.class, e -> e.getModuleRange().getLength()); + SIZE("Size", Long.class, e -> e.getModuleRange().getLength()), + MEMORIZE("Memorize", Boolean.class, ModuleMapEntry::isMemorize, ModuleMapEntry::setMemorize); private final String header; private final Class cls; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProvider.java index da82383952..b5ce14bab5 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProvider.java @@ -728,12 +728,10 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter { Msg.error(this, "Import service is not present"); } importModuleFromFileSystem(context.getModule()); - consoleService.removeFromLog(context); // TODO: Should remove when mapping is created } private void activatedMapMissingModule(DebuggerMissingModuleActionContext context) { mapModuleTo(context.getModule()); - consoleService.removeFromLog(context); // TODO: Should remove when mapping is created } private void toggledFilter(ActionContext ignored) { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingServicePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingServicePlugin.java index 0e7a1296fd..63871de72d 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingServicePlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingServicePlugin.java @@ -545,9 +545,12 @@ public class DebuggerStaticMappingServicePlugin extends Plugin private Set affectedTraces = new HashSet<>(); private Set affectedPrograms = new HashSet<>(); + private final ProgramModuleIndexer programModuleIndexer; + public DebuggerStaticMappingServicePlugin(PluginTool tool) { super(tool); this.autoWiring = AutoService.wireServicesProvidedAndConsumed(this); + this.programModuleIndexer = new ProgramModuleIndexer(tool); changeDebouncer.addListener(this::fireChangeListeners); tool.getProject().getProjectData().addDomainFolderChangeListener(this); @@ -783,6 +786,23 @@ public class DebuggerStaticMappingServicePlugin extends Plugin public void addModuleMappings(Collection entries, TaskMonitor monitor, boolean truncateExisting) throws CancelledException { addMappings(entries, monitor, truncateExisting, "Add module mappings"); + + Map> entriesByProgram = new HashMap<>(); + for (ModuleMapEntry entry : entries) { + if (entry.isMemorize()) { + entriesByProgram.computeIfAbsent(entry.getToProgram(), p -> new ArrayList<>()) + .add(entry); + } + } + for (Map.Entry> ent : entriesByProgram.entrySet()) { + try (UndoableTransaction tid = + UndoableTransaction.start(ent.getKey(), "Memorize module mapping")) { + for (ModuleMapEntry entry : ent.getValue()) { + ProgramModuleIndexer.addModulePaths(entry.getToProgram(), + List.of(entry.getModule().getName())); + } + } + } } @Override @@ -979,8 +999,8 @@ public class DebuggerStaticMappingServicePlugin extends Plugin } @Override - public Set findProbableModulePrograms(TraceModule module) { - return DebuggerStaticMappingUtils.findProbableModulePrograms(module, tool.getProject()); + public DomainFile findBestModuleProgram(AddressSpace space, TraceModule module) { + return programModuleIndexer.getBestMatch(space, module, programManager.getCurrentProgram()); } @Override diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingUtils.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingUtils.java index 80dfe8b104..a42b48fade 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingUtils.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingUtils.java @@ -15,15 +15,13 @@ */ package ghidra.app.plugin.core.debug.service.modules; -import java.io.IOException; import java.net.URL; import java.util.*; import ghidra.app.plugin.core.debug.utils.ProgramURLUtils; import ghidra.app.services.MapEntry; -import ghidra.framework.data.OpenedDomainFile; -import ghidra.framework.model.*; -import ghidra.framework.store.FileSystem; +import ghidra.framework.model.DomainFile; +import ghidra.framework.model.ProjectData; import ghidra.program.model.address.*; import ghidra.program.model.listing.Library; import ghidra.program.model.listing.Program; @@ -33,9 +31,6 @@ import ghidra.trace.model.*; import ghidra.trace.model.modules.*; import ghidra.trace.model.program.TraceProgramView; import ghidra.util.Msg; -import ghidra.util.exception.CancelledException; -import ghidra.util.exception.VersionException; -import ghidra.util.task.TaskMonitor; public enum DebuggerStaticMappingUtils { ; @@ -45,121 +40,50 @@ public enum DebuggerStaticMappingUtils { return null; } - public static DomainFile resolve(DomainFolder folder, String path) { - StringBuilder fullPath = new StringBuilder(folder.getPathname()); - if (!fullPath.toString().endsWith(FileSystem.SEPARATOR)) { - // Only root should end with /, anyway - fullPath.append(FileSystem.SEPARATOR_CHAR); - } - fullPath.append(path); - return folder.getProjectData().getFile(fullPath.toString()); - } - - public static Set findPrograms(String modulePath, DomainFolder folder) { - // TODO: If not found, consider filenames with space + extra info - while (folder != null) { - DomainFile found = resolve(folder, modulePath); - if (found != null) { - return Set.of(found); - } - folder = folder.getParent(); - } - return Set.of(); - } - - public static Set findProgramsByPathOrName(String modulePath, - DomainFolder folder) { - Set found = findPrograms(modulePath, folder); - if (!found.isEmpty()) { - return found; - } - int idx = modulePath.lastIndexOf(FileSystem.SEPARATOR); - if (idx == -1) { - return Set.of(); - } - found = findPrograms(modulePath.substring(idx + 1), folder); - if (!found.isEmpty()) { - return found; - } - return Set.of(); - } - - public static Set findProgramsByPathOrName(String modulePath, Project project) { - return findProgramsByPathOrName(modulePath, project.getProjectData().getRootFolder()); - } - - protected static String normalizePath(String path) { - path = path.replace('\\', FileSystem.SEPARATOR_CHAR); - while (path.startsWith(FileSystem.SEPARATOR)) { - path = path.substring(1); - } - return path; - } - - public static Set findProbableModulePrograms(TraceModule module, Project project) { - // TODO: Consider folders containing existing mapping destinations - DomainFile df = module.getTrace().getDomainFile(); - String modulePath = normalizePath(module.getName()); - if (df == null) { - return findProgramsByPathOrName(modulePath, project); - } - DomainFolder parent = df.getParent(); - if (parent == null) { - return findProgramsByPathOrName(modulePath, project); - } - return findProgramsByPathOrName(modulePath, parent); - } - - protected static void collectLibraries(ProjectData project, Program cur, Set col, - TaskMonitor monitor) throws CancelledException { - if (!col.add(cur)) { + protected static void collectLibraries(ProjectData project, DomainFile cur, + Set col) { + if (!Program.class.isAssignableFrom(cur.getDomainObjectClass()) || !col.add(cur)) { return; } - ExternalManager externs = cur.getExternalManager(); - for (String extName : externs.getExternalLibraryNames()) { - monitor.checkCanceled(); - Library lib = externs.getExternalLibrary(extName); - String libPath = lib.getAssociatedProgramPath(); - if (libPath == null) { - continue; + Set paths = new HashSet<>(); + try (PeekOpenedDomainObject peek = new PeekOpenedDomainObject(cur)) { + if (!(peek.object instanceof Program program)) { + return; } - DomainFile libFile = project.getFile(libPath); + ExternalManager externalManager = program.getExternalManager(); + for (String libraryName : externalManager.getExternalLibraryNames()) { + Library library = externalManager.getExternalLibrary(libraryName); + String path = library.getAssociatedProgramPath(); + if (path != null) { + paths.add(path); + } + } + } + + for (String libraryPath : paths) { + DomainFile libFile = project.getFile(libraryPath); if (libFile == null) { - Msg.info(DebuggerStaticMappingUtils.class, - "Referenced external program not found: " + libPath); - continue; - } - try (OpenedDomainFile 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