Merge remote-tracking branch

'origin/GP-1527_Dan_customModuleMapping--SQUASHED' (Closes #3641,
Closes #3675)
This commit is contained in:
Ryan Kurtz 2023-01-10 05:14:41 -05:00
commit f5751de8ee
16 changed files with 603 additions and 142 deletions

View file

@ -109,7 +109,9 @@
open programs for the selected modules and proposes new mappings. The user can examine and 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 tweak the proposal before confirming or canceling it. Typically, this is done automatically by
the <A href="help/topics/DebuggerBots/DebuggerBots.html#map_modules">Map Modules</A> debugger the <A href="help/topics/DebuggerBots/DebuggerBots.html#map_modules">Map Modules</A> debugger
bot.</P> 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.</P>
<TABLE width="100%"> <TABLE width="100%">
<TBODY> <TBODY>
@ -124,7 +126,8 @@
<P>This action is available from a single module's pop-up menu, when there is an open program. <P>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 It behaves like Map Modules, except that it will propose the selected module be mapped to the
current program.</P> current program. This action with the "Memorize" toggle is a good way to override or specify a
module mapping once and for all.</P>
<H3><A name="map_sections"></A>Map Sections</H3> <H3><A name="map_sections"></A>Map Sections</H3>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before After
Before After

View file

@ -52,6 +52,10 @@ public class ConsoleActionsCellRenderer extends AbstractGhidraColumnRenderer<Act
static void populateBox(JPanel box, List<JButton> buttonCache, ActionList value, static void populateBox(JPanel box, List<JButton> buttonCache, ActionList value,
Consumer<JButton> extraConfig) { Consumer<JButton> extraConfig) {
box.removeAll(); box.removeAll();
if (value == null) {
// IDK how this is happening.... An empty row or something?
return;
}
ensureCacheSize(buttonCache, value.size(), extraConfig); ensureCacheSize(buttonCache, value.size(), extraConfig);
int i = 0; int i = 0;
for (BoundAction a : value) { for (BoundAction a : value) {

View file

@ -15,6 +15,8 @@
*/ */
package ghidra.app.plugin.core.debug.gui.console; package ghidra.app.plugin.core.debug.gui.console;
import java.util.List;
import javax.swing.Icon; import javax.swing.Icon;
import org.apache.logging.log4j.Level; import org.apache.logging.log4j.Level;
@ -120,6 +122,11 @@ public class DebuggerConsolePlugin extends Plugin implements DebuggerConsoleServ
return provider.logContains(context); return provider.logContains(context);
} }
@Override
public List<ActionContext> getActionContexts() {
return provider.getActionContexts();
}
@Override @Override
public void addResolutionAction(DockingActionIf action) { public void addResolutionAction(DockingActionIf action) {
provider.addResolutionAction(action); provider.addResolutionAction(action);

View file

@ -177,7 +177,7 @@ public class DebuggerConsoleProvider extends ComponentProviderAdapter
this.message = message; this.message = message;
this.date = date; this.date = date;
this.context = context; this.context = context;
this.actions = actions; this.actions = Objects.requireNonNull(actions);
} }
public Icon getIcon() { public Icon getIcon() {
@ -456,6 +456,12 @@ public class DebuggerConsoleProvider extends ComponentProviderAdapter
} }
} }
protected List<ActionContext> getActionContexts() {
synchronized (buffer) {
return List.copyOf(logTableModel.getMap().keySet());
}
}
protected void addResolutionAction(DockingActionIf action) { protected void addResolutionAction(DockingActionIf action) {
DockingActionIf replaced = DockingActionIf replaced =
actionsByOwnerThenName.computeIfAbsent(action.getOwner(), o -> new LinkedHashMap<>()) actionsByOwnerThenName.computeIfAbsent(action.getOwner(), o -> new LinkedHashMap<>())

View file

@ -29,6 +29,7 @@ import javax.swing.event.ChangeListener;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.jdom.Element; import org.jdom.Element;
import docking.ActionContext;
import docking.WindowPosition; import docking.WindowPosition;
import docking.action.DockingAction; import docking.action.DockingAction;
import docking.action.ToggleDockingAction; import docking.action.ToggleDockingAction;
@ -60,8 +61,7 @@ import ghidra.framework.plugintool.AutoConfigState;
import ghidra.framework.plugintool.AutoService; import ghidra.framework.plugintool.AutoService;
import ghidra.framework.plugintool.annotation.AutoConfigStateField; import ghidra.framework.plugintool.annotation.AutoConfigStateField;
import ghidra.framework.plugintool.annotation.AutoServiceConsumed; import ghidra.framework.plugintool.annotation.AutoServiceConsumed;
import ghidra.program.model.address.Address; import ghidra.program.model.address.*;
import ghidra.program.model.address.AddressSetView;
import ghidra.program.model.listing.Program; import ghidra.program.model.listing.Program;
import ghidra.program.util.ProgramLocation; import ghidra.program.util.ProgramLocation;
import ghidra.program.util.ProgramSelection; import ghidra.program.util.ProgramSelection;
@ -121,13 +121,8 @@ public class DebuggerListingProvider extends CodeViewerProvider {
return; return;
} }
doMarkTrackedLocation(); 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.
*/
} }
} }
@ -845,6 +840,10 @@ public class DebuggerListingProvider extends CodeViewerProvider {
if (loc == null) { // Redundant? if (loc == null) { // Redundant?
return; return;
} }
AddressSpace space = loc.getAddress().getAddressSpace();
if (space == null) {
return; // Is this NO_ADDRESS or something?
}
if (mappingService == null) { if (mappingService == null) {
return; return;
} }
@ -873,22 +872,21 @@ public class DebuggerListingProvider extends CodeViewerProvider {
modMan.getSectionsAt(snap, address).stream().map(s -> s.getModule())) modMan.getSectionsAt(snap, address).stream().map(s -> s.getModule()))
.collect(Collectors.toSet()); .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? // TODO: What if sections are not presented?
for (TraceModule mod : modules) { for (TraceModule mod : modules) {
Set<DomainFile> matches = mappingService.findProbableModulePrograms(mod); DomainFile match = mappingService.findBestModuleProgram(space, mod);
if (matches.isEmpty()) { if (match == null) {
missing.add(mod); missing.add(mod);
} }
else { else {
toOpen.addAll(matches); toOpen.add(match);
} }
} }
if (programManager != null && !toOpen.isEmpty()) { if (programManager != null && !toOpen.isEmpty()) {
for (DomainFile df : toOpen) { for (DomainFile df : toOpen) {
// Do not presume a goTo is about to happen. There are no mappings, yet. // Do not presume a goTo is about to happen. There are no mappings, yet.
doTryOpenProgram(df, DomainFile.DEFAULT_VERSION, doTryOpenProgram(df, DomainFile.DEFAULT_VERSION, ProgramManager.OPEN_VISIBLE);
ProgramManager.OPEN_VISIBLE);
} }
} }
@ -903,12 +901,41 @@ public class DebuggerListingProvider extends CodeViewerProvider {
new DebuggerMissingModuleActionContext(mod)); new DebuggerMissingModuleActionContext(mod));
} }
/** /**
* Once the programs are opened, including those which are successfully imported, the * Once the programs are opened, including those which are successfully imported, the mapper
* section mapper should take over, eventually invoking callbacks to our mapping change * bot should take over, eventually invoking callbacks to our mapping change listener.
* listener.
*/ */
} }
protected boolean isMapped(AddressRange range) {
if (range == null) {
return false;
}
return mappingService.getStaticLocationFromDynamic(
new ProgramLocation(getProgram(), range.getMinAddress())) != null;
}
protected void cleanMissingModuleMessages(Set<Trace> 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) { public void setTrackingSpec(LocationTrackingSpec spec) {
trackingTrait.setSpec(spec); trackingTrait.setSpec(spec);
} }

View file

@ -48,7 +48,8 @@ public class DebuggerModuleMapProposalDialog
? e.getToProgram().getName() ? e.getToProgram().getName()
: e.getToProgram().getDomainFile().getName())), : e.getToProgram().getDomainFile().getName())),
STATIC_BASE("Static Base", Address.class, e -> e.getToProgram().getImageBase()), 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 String header;
private final Class<?> cls; private final Class<?> cls;

View file

@ -728,12 +728,10 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
Msg.error(this, "Import service is not present"); Msg.error(this, "Import service is not present");
} }
importModuleFromFileSystem(context.getModule()); importModuleFromFileSystem(context.getModule());
consoleService.removeFromLog(context); // TODO: Should remove when mapping is created
} }
private void activatedMapMissingModule(DebuggerMissingModuleActionContext context) { private void activatedMapMissingModule(DebuggerMissingModuleActionContext context) {
mapModuleTo(context.getModule()); mapModuleTo(context.getModule());
consoleService.removeFromLog(context); // TODO: Should remove when mapping is created
} }
private void toggledFilter(ActionContext ignored) { private void toggledFilter(ActionContext ignored) {

View file

@ -545,9 +545,12 @@ public class DebuggerStaticMappingServicePlugin extends Plugin
private Set<Trace> affectedTraces = new HashSet<>(); private Set<Trace> affectedTraces = new HashSet<>();
private Set<Program> affectedPrograms = new HashSet<>(); private Set<Program> affectedPrograms = new HashSet<>();
private final ProgramModuleIndexer programModuleIndexer;
public DebuggerStaticMappingServicePlugin(PluginTool tool) { public DebuggerStaticMappingServicePlugin(PluginTool tool) {
super(tool); super(tool);
this.autoWiring = AutoService.wireServicesProvidedAndConsumed(this); this.autoWiring = AutoService.wireServicesProvidedAndConsumed(this);
this.programModuleIndexer = new ProgramModuleIndexer(tool);
changeDebouncer.addListener(this::fireChangeListeners); changeDebouncer.addListener(this::fireChangeListeners);
tool.getProject().getProjectData().addDomainFolderChangeListener(this); tool.getProject().getProjectData().addDomainFolderChangeListener(this);
@ -783,6 +786,23 @@ public class DebuggerStaticMappingServicePlugin extends Plugin
public void addModuleMappings(Collection<ModuleMapEntry> entries, TaskMonitor monitor, public void addModuleMappings(Collection<ModuleMapEntry> entries, TaskMonitor monitor,
boolean truncateExisting) throws CancelledException { boolean truncateExisting) throws CancelledException {
addMappings(entries, monitor, truncateExisting, "Add module mappings"); addMappings(entries, monitor, truncateExisting, "Add module mappings");
Map<Program, List<ModuleMapEntry>> entriesByProgram = new HashMap<>();
for (ModuleMapEntry entry : entries) {
if (entry.isMemorize()) {
entriesByProgram.computeIfAbsent(entry.getToProgram(), p -> new ArrayList<>())
.add(entry);
}
}
for (Map.Entry<Program, List<ModuleMapEntry>> 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 @Override
@ -979,8 +999,8 @@ public class DebuggerStaticMappingServicePlugin extends Plugin
} }
@Override @Override
public Set<DomainFile> findProbableModulePrograms(TraceModule module) { public DomainFile findBestModuleProgram(AddressSpace space, TraceModule module) {
return DebuggerStaticMappingUtils.findProbableModulePrograms(module, tool.getProject()); return programModuleIndexer.getBestMatch(space, module, programManager.getCurrentProgram());
} }
@Override @Override

View file

@ -15,15 +15,13 @@
*/ */
package ghidra.app.plugin.core.debug.service.modules; package ghidra.app.plugin.core.debug.service.modules;
import java.io.IOException;
import java.net.URL; import java.net.URL;
import java.util.*; import java.util.*;
import ghidra.app.plugin.core.debug.utils.ProgramURLUtils; import ghidra.app.plugin.core.debug.utils.ProgramURLUtils;
import ghidra.app.services.MapEntry; import ghidra.app.services.MapEntry;
import ghidra.framework.data.OpenedDomainFile; import ghidra.framework.model.DomainFile;
import ghidra.framework.model.*; import ghidra.framework.model.ProjectData;
import ghidra.framework.store.FileSystem;
import ghidra.program.model.address.*; import ghidra.program.model.address.*;
import ghidra.program.model.listing.Library; import ghidra.program.model.listing.Library;
import ghidra.program.model.listing.Program; import ghidra.program.model.listing.Program;
@ -33,9 +31,6 @@ import ghidra.trace.model.*;
import ghidra.trace.model.modules.*; import ghidra.trace.model.modules.*;
import ghidra.trace.model.program.TraceProgramView; import ghidra.trace.model.program.TraceProgramView;
import ghidra.util.Msg; import ghidra.util.Msg;
import ghidra.util.exception.CancelledException;
import ghidra.util.exception.VersionException;
import ghidra.util.task.TaskMonitor;
public enum DebuggerStaticMappingUtils { public enum DebuggerStaticMappingUtils {
; ;
@ -45,121 +40,50 @@ public enum DebuggerStaticMappingUtils {
return null; return null;
} }
public static DomainFile resolve(DomainFolder folder, String path) { protected static void collectLibraries(ProjectData project, DomainFile cur,
StringBuilder fullPath = new StringBuilder(folder.getPathname()); Set<DomainFile> col) {
if (!fullPath.toString().endsWith(FileSystem.SEPARATOR)) { if (!Program.class.isAssignableFrom(cur.getDomainObjectClass()) || !col.add(cur)) {
// Only root should end with /, anyway
fullPath.append(FileSystem.SEPARATOR_CHAR);
}
fullPath.append(path);
return folder.getProjectData().getFile(fullPath.toString());
}
public static Set<DomainFile> 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<DomainFile> findProgramsByPathOrName(String modulePath,
DomainFolder folder) {
Set<DomainFile> 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<DomainFile> 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<DomainFile> 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<Program> col,
TaskMonitor monitor) throws CancelledException {
if (!col.add(cur)) {
return; return;
} }
ExternalManager externs = cur.getExternalManager(); Set<String> paths = new HashSet<>();
for (String extName : externs.getExternalLibraryNames()) { try (PeekOpenedDomainObject peek = new PeekOpenedDomainObject(cur)) {
monitor.checkCanceled(); if (!(peek.object instanceof Program program)) {
Library lib = externs.getExternalLibrary(extName); return;
String libPath = lib.getAssociatedProgramPath();
if (libPath == null) {
continue;
} }
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) { if (libFile == null) {
Msg.info(DebuggerStaticMappingUtils.class,
"Referenced external program not found: " + libPath);
continue;
}
try (OpenedDomainFile<Program> 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; 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 * <p>
* @param monitor a monitor to cancel the process * This will only descend into domain files that are already opened. This will only include
* @return the set of found programs, including the seed * results whose content type is a {@link Program}.
* @throws CancelledException if cancelled by the monitor *
* @param seeds the seeds, usually including the executable
* @return the set of found domain files, including the seeds
*/ */
public static Set<Program> collectLibraries(Program seed, TaskMonitor monitor) public static Set<DomainFile> collectLibraries(Collection<DomainFile> seeds) {
throws CancelledException { Set<DomainFile> result = new LinkedHashSet<>();
Set<Program> result = new LinkedHashSet<>(); for (DomainFile seed : seeds) {
collectLibraries(seed.getDomainFile().getParent().getProjectData(), seed, result, collectLibraries(seed.getParent().getProjectData(), seed, result);
monitor); }
return result; return result;
} }

View file

@ -84,6 +84,7 @@ public class DefaultModuleMapProposal
} }
protected AddressRange moduleRange; protected AddressRange moduleRange;
protected boolean memorize = false;
/** /**
* Construct a module map entry * Construct a module map entry
@ -146,6 +147,16 @@ public class DefaultModuleMapProposal
throw new AssertionError(e); throw new AssertionError(e);
} }
} }
@Override
public boolean isMemorize() {
return memorize;
}
@Override
public void setMemorize(boolean memorize) {
this.memorize = memorize;
}
} }
protected final TraceModule module; protected final TraceModule module;

View file

@ -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);
}
}
}

View file

@ -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<String> moduleNames) {
Options options = program.getOptions(Program.PROGRAM_INFO);
LinkedHashSet<String> distinct = moduleNames instanceof LinkedHashSet<String> yes ? yes
: new LinkedHashSet<>(moduleNames);
options.setString(MODULE_PATHS_PROPERTY, JSON.toJson(distinct));
}
public static Collection<String> getModulePaths(DomainFile df) {
return getModulePaths(df.getMetadata());
}
public static Collection<String> getModulePaths(Map<String, String> metadata) {
String json = metadata.get(MODULE_PATHS_PROPERTY);
if (json == null) {
return List.of();
}
return JSON.fromJson(json, new TypeToken<List<String>>() {}.getType());
}
public static void addModulePaths(Program program, Collection<String> moduleNames) {
LinkedHashSet<String> 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<K, V> {
public final Map<K, Set<V>> 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<V> set = map.get(key);
if (set == null) {
return;
}
set.remove(value);
if (set.isEmpty()) {
map.remove(key);
}
}
}
protected static class ModuleIndex {
final MapOfSets<String, IndexEntry> entriesByName = new MapOfSets<>();
final MapOfSets<String, IndexEntry> 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<IndexEntry> remove = entriesByFile.map.remove(fileID);
if (remove == null) {
return;
}
for (IndexEntry entry : remove) {
entriesByName.remove(entry.name, entry);
}
}
public Collection<IndexEntry> 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<Program, ModuleChangeListener> 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<String, String> 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<IndexEntry> entries, Set<DomainFile> libraries,
Map<DomainFolder, Integer> 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<IndexEntry> byIsLibrary = Comparator.comparing(e -> {
DomainFile df = projectData.getFileByID(e.dfID);
return libraries.contains(df) ? 1 : 0;
});
Comparator<IndexEntry> byNameSource = Comparator.comparing(e -> -e.source.ordinal());
Map<IndexEntry, Integer> folderScores = new HashMap<>();
Comparator<IndexEntry> 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<IndexEntry> 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<DomainFolder, Integer> folderUses = new HashMap<>();
Set<DomainFile> 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<DomainFile> 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<IndexEntry> 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;
}
}

View file

@ -15,6 +15,8 @@
*/ */
package ghidra.app.services; package ghidra.app.services;
import java.util.List;
import javax.swing.Icon; import javax.swing.Icon;
import docking.ActionContext; import docking.ActionContext;
@ -72,6 +74,13 @@ public interface DebuggerConsoleService extends DebuggerConsoleLogger {
*/ */
boolean logContains(ActionContext context); 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<ActionContext> getActionContexts();
/** /**
* Add an action which might be applied to an actionable log message * Add an action which might be applied to an actionable log message
* *

View file

@ -233,6 +233,10 @@ public interface DebuggerStaticMappingService {
* entry fails, including due to conflicts, that failure is logged but ignored, and the * entry fails, including due to conflicts, that failure is logged but ignored, and the
* remaining entries are processed. * remaining entries are processed.
* *
* <p>
* Any entries indicated for memorization will have their module paths added to the destination
* program's metadata.
*
* @param entries the entries to add * @param entries the entries to add
* @param monitor a monitor to cancel the operation * @param monitor a monitor to cancel the operation
* @param truncateExisting true to delete or truncate the lifespan of overlapping entries * @param truncateExisting true to delete or truncate the lifespan of overlapping entries
@ -410,19 +414,23 @@ public interface DebuggerStaticMappingService {
CompletableFuture<Void> changesSettled(); CompletableFuture<Void> 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
* *
* <p> * <p>
* If the trace is saved in a project, this will search that project preferring its siblings; if * The service maintains an index of likely module names to domain files in the active project.
* no sibling are probable, it will try the rest of the project. Otherwise, it will search the * This will search that index for the module's full file path. Failing that, it will search
* current project. "Probable" leaves room for implementations to use any number of heuristics * just for the module's file name. Among the programs found, it first prefers those whose
* available, e.g., name, path, type; however, they should refrain from opening or checking out * module name list (see {@link ProgramModuleIndexer#setModulePaths(Program, List)}) include the
* domain files. * 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 * @param module the trace module
* @return the, possibly empty, set of probable matches * @return the, possibly empty, set of probable matches
*/ */
Set<DomainFile> findProbableModulePrograms(TraceModule module); DomainFile findBestModuleProgram(AddressSpace space, TraceModule module);
/** /**
* Propose a module map for the given module to the given program * Propose a module map for the given module to the given program

View file

@ -51,6 +51,20 @@ public interface ModuleMapProposal extends MapProposal<TraceModule, Program, Mod
* @param program the program * @param program the program
*/ */
void setProgram(Program program); void setProgram(Program program);
/**
* Check if the user would like to memorize this mapping for future traces
*
* @return true to memorize
*/
boolean isMemorize();
/**
* Set whether this mapping should be memorized for future traces
*
* @param memorize true to memorize
*/
void setMemorize(boolean memorize);
} }
/** /**