GP-1527: Improve automatic module mapping, including optional memorization.

This commit is contained in:
Dan 2023-01-09 10:26:20 -05:00
parent f65b3c4a05
commit 835127c928
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
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
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%">
<TBODY>
@ -124,7 +126,8 @@
<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
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>

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,
Consumer<JButton> 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) {

View file

@ -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<ActionContext> getActionContexts() {
return provider.getActionContexts();
}
@Override
public void addResolutionAction(DockingActionIf action) {
provider.addResolutionAction(action);

View file

@ -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<ActionContext> getActionContexts() {
synchronized (buffer) {
return List.copyOf(logTableModel.getMap().keySet());
}
}
protected void addResolutionAction(DockingActionIf action) {
DockingActionIf replaced =
actionsByOwnerThenName.computeIfAbsent(action.getOwner(), o -> new LinkedHashMap<>())

View file

@ -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<DomainFile> 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<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) {
trackingTrait.setSpec(spec);
}

View file

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

View file

@ -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) {

View file

@ -545,9 +545,12 @@ public class DebuggerStaticMappingServicePlugin extends Plugin
private Set<Trace> affectedTraces = new HashSet<>();
private Set<Program> 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<ModuleMapEntry> entries, TaskMonitor monitor,
boolean truncateExisting) throws CancelledException {
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
@ -979,8 +999,8 @@ public class DebuggerStaticMappingServicePlugin extends Plugin
}
@Override
public Set<DomainFile> findProbableModulePrograms(TraceModule module) {
return DebuggerStaticMappingUtils.findProbableModulePrograms(module, tool.getProject());
public DomainFile findBestModuleProgram(AddressSpace space, TraceModule module) {
return programModuleIndexer.getBestMatch(space, module, programManager.getCurrentProgram());
}
@Override

View file

@ -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<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)) {
protected static void collectLibraries(ProjectData project, DomainFile cur,
Set<DomainFile> 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<String> 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> 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
* <p>
* 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<Program> collectLibraries(Program seed, TaskMonitor monitor)
throws CancelledException {
Set<Program> result = new LinkedHashSet<>();
collectLibraries(seed.getDomainFile().getParent().getProjectData(), seed, result,
monitor);
public static Set<DomainFile> collectLibraries(Collection<DomainFile> seeds) {
Set<DomainFile> result = new LinkedHashSet<>();
for (DomainFile seed : seeds) {
collectLibraries(seed.getParent().getProjectData(), seed, result);
}
return result;
}

View file

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

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;
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<ActionContext> getActionContexts();
/**
* 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
* 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 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<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>
* 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<DomainFile> findProbableModulePrograms(TraceModule module);
DomainFile findBestModuleProgram(AddressSpace space, TraceModule module);
/**
* 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
*/
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);
}
/**