mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-05 02:39:44 +02:00
GP-1527: Improve automatic module mapping, including optional memorization.
This commit is contained in:
parent
f65b3c4a05
commit
835127c928
16 changed files with 603 additions and 142 deletions
|
@ -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 |
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<>())
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue