diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetMemoryRegion.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetMemoryRegion.java index 843b0869e3..ae56376ed5 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetMemoryRegion.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetMemoryRegion.java @@ -52,9 +52,9 @@ public class GdbModelTargetMemoryRegion protected static String computeDisplay(GdbMemoryMapping mapping) { // NOTE: This deviates from GDB's table display, as it'd be confusing in isolation if (mapping.getObjfile() == null || mapping.getObjfile().length() == 0) { - return String.format("?? [0x%x-0x%x]", mapping.getStart(), mapping.getEnd()); + return String.format("?? (0x%x-0x%x)", mapping.getStart(), mapping.getEnd()); } - return String.format("%s [0x%x-0x%x] (0x%x)", mapping.getObjfile(), mapping.getStart(), + return String.format("%s (0x%x-0x%x,0x%x)", mapping.getObjfile(), mapping.getStart(), mapping.getEnd(), mapping.getOffset()); } diff --git a/Ghidra/Debug/Debugger/certification.manifest b/Ghidra/Debug/Debugger/certification.manifest index 2b87be0f13..197b22ac1d 100644 --- a/Ghidra/Debug/Debugger/certification.manifest +++ b/Ghidra/Debug/Debugger/certification.manifest @@ -95,6 +95,7 @@ src/main/help/help/topics/DebuggerObjectsPlugin/images/stop.png||GHIDRA||||END| src/main/help/help/topics/DebuggerPcodeStepperPlugin/DebuggerPcodeStepperPlugin.html||GHIDRA||||END| src/main/help/help/topics/DebuggerPcodeStepperPlugin/images/DebuggerPcodeStepperPlugin.png||GHIDRA||||END| src/main/help/help/topics/DebuggerRegionsPlugin/DebuggerRegionsPlugin.html||GHIDRA||||END| +src/main/help/help/topics/DebuggerRegionsPlugin/images/DebuggerRegionMapProposalDialog.png||GHIDRA||||END| src/main/help/help/topics/DebuggerRegionsPlugin/images/DebuggerRegionsPlugin.png||GHIDRA||||END| src/main/help/help/topics/DebuggerRegistersPlugin/DebuggerRegistersPlugin.html||GHIDRA||||END| src/main/help/help/topics/DebuggerRegistersPlugin/images/DebuggerAvailableRegistersDialog.png||GHIDRA||||END| diff --git a/Ghidra/Debug/Debugger/ghidra_scripts/AddMapping.java b/Ghidra/Debug/Debugger/ghidra_scripts/AddMapping.java index ce98c0ee31..1831dcd77c 100644 --- a/Ghidra/Debug/Debugger/ghidra_scripts/AddMapping.java +++ b/Ghidra/Debug/Debugger/ghidra_scripts/AddMapping.java @@ -22,7 +22,6 @@ import ghidra.program.model.address.AddressSpace; import ghidra.program.util.ProgramLocation; import ghidra.trace.model.DefaultTraceLocation; import ghidra.trace.model.Trace; -import ghidra.util.database.UndoableTransaction; public class AddMapping extends GhidraScript { @Override @@ -35,13 +34,10 @@ public class AddMapping extends GhidraScript { AddressSpace dynRam = currentTrace.getBaseAddressFactory().getDefaultAddressSpace(); AddressSpace statRam = currentProgram.getAddressFactory().getDefaultAddressSpace(); - try (UndoableTransaction tid = - UndoableTransaction.start(currentTrace, "Add Mapping", true)) { - mappings.addMapping( - new DefaultTraceLocation(currentTrace, null, Range.atLeast(0L), - dynRam.getAddress(0x00400000)), - new ProgramLocation(currentProgram, statRam.getAddress(0x00400000)), - 0x10000, false); - } + mappings.addMapping( + new DefaultTraceLocation(currentTrace, null, Range.atLeast(0L), + dynRam.getAddress(0x00400000)), + new ProgramLocation(currentProgram, statRam.getAddress(0x00400000)), + 0x10000, false); } } diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerBots/DebuggerBots.html b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerBots/DebuggerBots.html index c7e4d3751b..c4ef1ef1be 100644 --- a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerBots/DebuggerBots.html +++ b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerBots/DebuggerBots.html @@ -59,5 +59,15 @@ performed manually using the Map Sections action in the Modules and Sections window.

+ +

Map Regions

+ +

This bot automatically maps trace regions to memory blocks of programs opened in the same + tool. Its operation is analogous to that of the Map Modules Bot, except that it creates the + mapped ranges by region. It is not commonly used, as it's less efficient than the Map Modules + Bot, but it is required whenever a target fails to present modules. This action can be + performed manually using the Map Regions + action in the Regions window.

diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerRegionsPlugin/DebuggerRegionsPlugin.html b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerRegionsPlugin/DebuggerRegionsPlugin.html index 189531e017..cb98d7ce1f 100644 --- a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerRegionsPlugin/DebuggerRegionsPlugin.html +++ b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerRegionsPlugin/DebuggerRegionsPlugin.html @@ -55,6 +55,41 @@

Other than modifications enabled by the table, the Regions window provides the following actions:

+

Map Regions

+ +

This action is analogous to the Map Modules and Map Sections actions from the Modules window. It searches + the tool's open programs for blocks matching the selected regions and proposes new mappings. + Users who prefer this should also consider using the Map Regions debugger bot. For the + best result, the selection regions should comprise a complete module. In particular, it should + include the region containing the module's image base, as the offset from this base is used in + scoring the best-matched blocks. Additionally, the region names must include the module's file + name, otherwise the matcher has no means to identify a corresponding program.

+ + + + + + + +
+ +

Map Regions to Current Program

+ +

This action is available from the pop-up menu, when there is a selection of regions and + there is an open program. It behaves like Map Regions, except that it will attempt to map the + selected regions to blocks in the current program only. This is useful if the regions are not + named according to the module filename. The selected regions should still comprise a complete + module for best results.

+ +

Map Region to Current Block

+ +

This action is available from a single region's pop-up menu, when there is an open program. + It behaves like Map Regions, except that it will propose the selected region be mapped to the + block containing the cursor in the static listing.

+

Select Addresses

This action is available when at least one region is selected. It selects all addresses in diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerRegionsPlugin/images/DebuggerRegionMapProposalDialog.png b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerRegionsPlugin/images/DebuggerRegionMapProposalDialog.png new file mode 100644 index 0000000000..286db9888b Binary files /dev/null and b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerRegionsPlugin/images/DebuggerRegionMapProposalDialog.png differ diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerRegionsPlugin/images/DebuggerRegionsPlugin.png b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerRegionsPlugin/images/DebuggerRegionsPlugin.png index d6340e8253..b9b4c637f0 100644 Binary files a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerRegionsPlugin/images/DebuggerRegionsPlugin.png and b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerRegionsPlugin/images/DebuggerRegionsPlugin.png differ diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/AbstractDebuggerMapProposalDialog.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/AbstractDebuggerMapProposalDialog.java similarity index 92% rename from Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/AbstractDebuggerMapProposalDialog.java rename to Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/AbstractDebuggerMapProposalDialog.java index 4433284935..81203e50d4 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/AbstractDebuggerMapProposalDialog.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/AbstractDebuggerMapProposalDialog.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package ghidra.app.plugin.core.debug.gui.modules; +package ghidra.app.plugin.core.debug.gui; import java.awt.BorderLayout; import java.util.Collection; @@ -59,6 +59,16 @@ public abstract class AbstractDebuggerMapProposalDialog extends DialogCompone createActions(); } + /* testing */ + public GTable getTable() { + return table; + } + + /* testing */ + public EnumeratedColumnTableModel getTableModel() { + return tableModel; + } + protected void removeEntry(R entry) { tableModel.delete(entry); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerBlockChooserDialog.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerBlockChooserDialog.java similarity index 82% rename from Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerBlockChooserDialog.java rename to Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerBlockChooserDialog.java index 1c6d32f957..3f6b8b5e3b 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerBlockChooserDialog.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerBlockChooserDialog.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package ghidra.app.plugin.core.debug.gui.modules; +package ghidra.app.plugin.core.debug.gui; import java.awt.BorderLayout; import java.util.*; @@ -34,16 +34,17 @@ import ghidra.program.model.address.Address; import ghidra.program.model.listing.Program; import ghidra.program.model.mem.MemoryBlock; import ghidra.program.util.ProgramLocation; +import ghidra.trace.model.memory.TraceMemoryRegion; import ghidra.trace.model.modules.TraceSection; import ghidra.util.table.GhidraTableFilterPanel; public class DebuggerBlockChooserDialog extends DialogComponentProvider { - static class MemoryBlockRow { + public static class MemoryBlockRow { private final Program program; private final MemoryBlock block; private double score; - public MemoryBlockRow(Program program, MemoryBlock block) { + protected MemoryBlockRow(Program program, MemoryBlock block) { this.program = program; this.block = block; } @@ -87,6 +88,13 @@ public class DebuggerBlockChooserDialog extends DialogComponentProvider { return score = service.proposeSectionMap(section, program, block).computeScore(); } + public double score(TraceMemoryRegion region, DebuggerStaticMappingService service) { + if (region == null) { + return score = 0; + } + return score = service.proposeRegionMap(region, program, block).computeScore(); + } + public ProgramLocation getProgramLocation() { return new ProgramLocation(program, block.getStart()); } @@ -143,11 +151,21 @@ public class DebuggerBlockChooserDialog extends DialogComponentProvider { private Entry chosen; - protected DebuggerBlockChooserDialog() { + public DebuggerBlockChooserDialog() { super("Memory Blocks", true, true, true, false); populateComponents(); } + /* testing */ + public EnumeratedColumnTableModel getTableModel() { + return tableModel; + } + + /* testing */ + public GhidraTableFilterPanel getTableFilterPanel() { + return filterPanel; + } + protected void populateComponents() { JPanel panel = new JPanel(new BorderLayout()); @@ -183,16 +201,28 @@ public class DebuggerBlockChooserDialog extends DialogComponentProvider { public Map.Entry chooseBlock(PluginTool tool, TraceSection section, Collection programs) { + DebuggerStaticMappingService service = tool.getService(DebuggerStaticMappingService.class); + return chooseBlock(tool, programs, rec -> rec.score(section, service)); + } + + public Map.Entry chooseBlock(PluginTool tool, TraceMemoryRegion region, + Collection programs) { + DebuggerStaticMappingService service = tool.getService(DebuggerStaticMappingService.class); + return chooseBlock(tool, programs, rec -> rec.score(region, service)); + } + + protected Map.Entry chooseBlock(PluginTool tool, + Collection programs, Function scorer) { setBlocksFromPrograms(programs); - computeScores(section, tool.getService(DebuggerStaticMappingService.class)); + computeScores(scorer); selectHighestScoringBlock(); tool.showDialog(this); return getChosen(); } - protected void computeScores(TraceSection section, DebuggerStaticMappingService service) { + protected void computeScores(Function scorer) { for (MemoryBlockRow rec : tableModel.getModelData()) { - rec.score(section, service); + scorer.apply(rec); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java index c4a7d7898f..ee76a1751e 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java @@ -149,6 +149,7 @@ public interface DebuggerResources { ImageIcon ICON_MAP_IDENTICALLY = ResourceManager.loadImage("images/doubleArrow.png"); ImageIcon ICON_MAP_MODULES = ResourceManager.loadImage("images/modules.png"); ImageIcon ICON_MAP_SECTIONS = ICON_MAP_MODULES; // TODO + ImageIcon ICON_MAP_REGIONS = ICON_MAP_MODULES; // TODO ImageIcon ICON_BLOCK = ICON_MAP_SECTIONS; // TODO // TODO: Draw an icon ImageIcon ICON_SELECT_ADDRESSES = ResourceManager.loadImage("images/NextSelectionBlock16.gif"); @@ -1344,7 +1345,7 @@ public interface DebuggerResources { interface MapSectionToAction { String NAME_PREFIX = "Map Section to "; - String DESCRIPTION = "Map the selected module to the current program"; + String DESCRIPTION = "Map the selected section to the current program"; Icon ICON = ICON_MAP_SECTIONS; // TODO: Probably no icon String GROUP = GROUP_MAPPING; String HELP_ANCHOR = "map_section_to"; @@ -1374,6 +1375,57 @@ public interface DebuggerResources { } } + interface MapRegionsAction { + String NAME = "Map Regions"; + String DESCRIPTION = "Map selected regions to program memory blocks"; + Icon ICON = ICON_MAP_REGIONS; // TODO: Probably no icon + String GROUP = GROUP_MAPPING; + String HELP_ANCHOR = "map_regions"; + + static ActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new ActionBuilder(NAME, ownerName).description(DESCRIPTION) + //.toolBarIcon(ICON) + //.toolBarGroup(GROUP) + //.popupMenuIcon(ICON) + .popupMenuPath(NAME) + .popupMenuGroup(GROUP) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); + } + } + + interface MapRegionToAction { + String NAME_PREFIX = "Map Region to "; + String DESCRIPTION = "Map the selected region to the current program"; + Icon ICON = ICON_MAP_SECTIONS; // TODO: Probably no icon + String GROUP = GROUP_MAPPING; + String HELP_ANCHOR = "map_region_to"; + + static ActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new ActionBuilder(NAME_PREFIX, ownerName).description(DESCRIPTION) + .popupMenuPath(NAME_PREFIX + "...") + .popupMenuGroup(GROUP) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); + } + } + + interface MapRegionsToAction { + String NAME_PREFIX = "Map Regions to "; + String DESCRIPTION = "Map the selected (module) regions to the current program"; + Icon ICON = ICON_MAP_SECTIONS; + String GROUP = GROUP_MAPPING; + String HELP_ANCHOR = "map_regions_to"; + + static ActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new ActionBuilder(NAME_PREFIX, ownerName).description(DESCRIPTION) + .popupMenuPath(NAME_PREFIX + "...") + .popupMenuGroup(GROUP) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); + } + } + /*interface SelectAddressesAction { // TODO: Finish this conversion String NAME = "Select Addresses"; Icon ICON = ICON_SELECT_ADDRESSES; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerRegionMapProposalDialog.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerRegionMapProposalDialog.java new file mode 100644 index 0000000000..7f6f3891fb --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerRegionMapProposalDialog.java @@ -0,0 +1,164 @@ +/* ### + * 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.gui.memory; + +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Function; + +import javax.swing.table.TableColumn; +import javax.swing.table.TableColumnModel; + +import docking.widgets.table.*; +import docking.widgets.table.DefaultEnumeratedColumnTableModel.EnumeratedTableColumn; +import ghidra.app.plugin.core.debug.gui.AbstractDebuggerMapProposalDialog; +import ghidra.app.plugin.core.debug.gui.DebuggerResources; +import ghidra.app.plugin.core.debug.gui.DebuggerResources.MapRegionsAction; +import ghidra.app.services.RegionMapProposal.RegionMapEntry; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Program; +import ghidra.program.model.mem.MemoryBlock; +import ghidra.util.Swing; + +public class DebuggerRegionMapProposalDialog + extends AbstractDebuggerMapProposalDialog { + + static final int BUTTON_SIZE = 32; + + protected enum RegionMapTableColumns + implements EnumeratedTableColumn { + REMOVE("Remove", String.class, e -> "Remove Proposed Entry", (e, v) -> nop()), + REGION_NAME("Region", String.class, e -> e.getRegion().getName()), + DYNAMIC_BASE("Dynamic Base", Address.class, e -> e.getRegion().getMinAddress()), + CHOOSE("Choose", String.class, e -> "Choose Block", (e, s) -> nop()), + PROGRAM_NAME("Program", String.class, e -> e.getToProgram().getName()), + BLOCK_NAME("Block", String.class, e -> e.getBlock().getName()), + STATIC_BASE("Static Base", Address.class, e -> e.getBlock().getStart()), + SIZE("Size", Long.class, e -> e.getMappingLength()); + + private final String header; + private final Class cls; + private final Function getter; + private final BiConsumer setter; + + private static void nop() { + } + + @SuppressWarnings("unchecked") + RegionMapTableColumns(String header, Class cls, Function getter, + BiConsumer setter) { + this.header = header; + this.cls = cls; + this.getter = getter; + this.setter = (BiConsumer) setter; + } + + RegionMapTableColumns(String header, Class cls, + Function getter) { + this(header, cls, getter, null); + } + + @Override + public String getHeader() { + return header; + } + + @Override + public Class getValueClass() { + return cls; + } + + @Override + public Object getValueOf(RegionMapEntry row) { + return getter.apply(row); + } + + @Override + public boolean isEditable(RegionMapEntry row) { + return setter != null; + } + + @Override + public void setValueOf(RegionMapEntry row, Object value) { + setter.accept(row, value); + } + } + + protected static class RegionMapPropsalTableModel extends + DefaultEnumeratedColumnTableModel { + + public RegionMapPropsalTableModel() { + super("Region Map", RegionMapTableColumns.class); + } + + @Override + public List defaultSortOrder() { + return List.of(RegionMapTableColumns.REGION_NAME); + } + } + + private final DebuggerRegionsProvider provider; + + public DebuggerRegionMapProposalDialog(DebuggerRegionsProvider provider) { + super(MapRegionsAction.NAME); + this.provider = provider; + } + + @Override + protected RegionMapPropsalTableModel createTableModel() { + return new RegionMapPropsalTableModel(); + } + + @Override + protected void populateComponents() { + super.populateComponents(); + setPreferredSize(600, 300); + + TableColumnModel columnModel = table.getColumnModel(); + + TableColumn removeCol = columnModel.getColumn(RegionMapTableColumns.REMOVE.ordinal()); + CellEditorUtils.installButton(table, filterPanel, removeCol, + DebuggerResources.ICON_DELETE, BUTTON_SIZE, this::removeEntry); + + TableColumn dynBaseCol = + columnModel.getColumn(RegionMapTableColumns.DYNAMIC_BASE.ordinal()); + dynBaseCol.setCellRenderer(CustomToStringCellRenderer.MONO_OBJECT); + + TableColumn chooseCol = columnModel.getColumn(RegionMapTableColumns.CHOOSE.ordinal()); + CellEditorUtils.installButton(table, filterPanel, chooseCol, DebuggerResources.ICON_PROGRAM, + BUTTON_SIZE, this::chooseAndSetBlock); + + TableColumn stBaseCol = columnModel.getColumn(RegionMapTableColumns.STATIC_BASE.ordinal()); + stBaseCol.setCellRenderer(CustomToStringCellRenderer.MONO_OBJECT); + + TableColumn sizeCol = columnModel.getColumn(RegionMapTableColumns.SIZE.ordinal()); + sizeCol.setCellRenderer(CustomToStringCellRenderer.MONO_ULONG_HEX); + } + + private void chooseAndSetBlock(RegionMapEntry entry) { + Map.Entry choice = + provider.askBlock(entry.getRegion(), entry.getToProgram(), entry.getBlock()); + if (choice == null) { + return; + } + + Swing.runIfSwingOrRunLater(() -> { + entry.setBlock(choice.getKey(), choice.getValue()); + tableModel.notifyUpdated(entry); + }); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerRegionsPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerRegionsPlugin.java index c5ae4d2e93..18efe8d07b 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerRegionsPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerRegionsPlugin.java @@ -15,32 +15,33 @@ */ package ghidra.app.plugin.core.debug.gui.memory; +import ghidra.app.events.*; import ghidra.app.plugin.PluginCategoryNames; import ghidra.app.plugin.core.debug.AbstractDebuggerPlugin; import ghidra.app.plugin.core.debug.DebuggerPluginPackage; import ghidra.app.plugin.core.debug.event.TraceActivatedPluginEvent; -import ghidra.app.plugin.core.debug.event.TraceClosedPluginEvent; import ghidra.app.services.*; import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.util.PluginStatus; -@PluginInfo( // - shortDescription = "Debugger regions manager", // - description = "GUI to manage memory regions", // - category = PluginCategoryNames.DEBUGGER, // - packageName = DebuggerPluginPackage.NAME, // - status = PluginStatus.RELEASED, // - eventsConsumed = { - TraceActivatedPluginEvent.class, // - TraceClosedPluginEvent.class, // - }, // - servicesRequired = { // - DebuggerModelService.class, // - DebuggerStaticMappingService.class, // - DebuggerTraceManagerService.class, // - ProgramManager.class, // - } // -) +@PluginInfo( + shortDescription = "Debugger regions manager", + description = "GUI to manage memory regions", + category = PluginCategoryNames.DEBUGGER, + packageName = DebuggerPluginPackage.NAME, + status = PluginStatus.RELEASED, + eventsConsumed = { + ProgramActivatedPluginEvent.class, + ProgramLocationPluginEvent.class, + ProgramClosedPluginEvent.class, + TraceActivatedPluginEvent.class, + }, + servicesRequired = { + DebuggerModelService.class, + DebuggerStaticMappingService.class, + DebuggerTraceManagerService.class, + ProgramManager.class, + }) public class DebuggerRegionsPlugin extends AbstractDebuggerPlugin { protected DebuggerRegionsProvider provider; @@ -63,7 +64,19 @@ public class DebuggerRegionsPlugin extends AbstractDebuggerPlugin { @Override public void processEvent(PluginEvent event) { super.processEvent(event); - if (event instanceof TraceActivatedPluginEvent) { + if (event instanceof ProgramActivatedPluginEvent) { + ProgramActivatedPluginEvent ev = (ProgramActivatedPluginEvent) event; + provider.setProgram(ev.getActiveProgram()); + } + else if (event instanceof ProgramLocationPluginEvent) { + ProgramLocationPluginEvent ev = (ProgramLocationPluginEvent) event; + provider.setLocation(ev.getLocation()); + } + else if (event instanceof ProgramClosedPluginEvent) { + ProgramClosedPluginEvent ev = (ProgramClosedPluginEvent) event; + provider.programClosed(ev.getProgram()); + } + else if (event instanceof TraceActivatedPluginEvent) { TraceActivatedPluginEvent ev = (TraceActivatedPluginEvent) event; provider.setTrace(ev.getActiveCoordinates().getTrace()); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerRegionsProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerRegionsProvider.java index b8df73ce10..ffbb098f5e 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerRegionsProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerRegionsProvider.java @@ -18,6 +18,7 @@ package ghidra.app.plugin.core.debug.gui.memory; import java.awt.BorderLayout; import java.awt.event.*; import java.util.*; +import java.util.Map.Entry; import java.util.function.BiConsumer; import java.util.function.Function; import java.util.stream.Collectors; @@ -34,17 +35,21 @@ import docking.action.*; import docking.widgets.table.CustomToStringCellRenderer; import docking.widgets.table.DefaultEnumeratedColumnTableModel.EnumeratedTableColumn; import ghidra.app.plugin.core.debug.DebuggerPluginPackage; +import ghidra.app.plugin.core.debug.gui.DebuggerBlockChooserDialog; import ghidra.app.plugin.core.debug.gui.DebuggerResources; -import ghidra.app.plugin.core.debug.gui.DebuggerResources.AbstractSelectAddressesAction; -import ghidra.app.plugin.core.debug.gui.DebuggerResources.SelectRowsAction; +import ghidra.app.plugin.core.debug.gui.DebuggerResources.*; +import ghidra.app.plugin.core.debug.gui.modules.DebuggerModulesProvider; +import ghidra.app.plugin.core.debug.service.modules.MapRegionsBackgroundCommand; import ghidra.app.plugin.core.debug.utils.DebouncedRowWrappedEnumeratedColumnTableModel; -import ghidra.app.services.DebuggerListingService; -import ghidra.app.services.DebuggerTraceManagerService; +import ghidra.app.services.*; +import ghidra.app.services.RegionMapProposal.RegionMapEntry; import ghidra.framework.model.DomainObject; import ghidra.framework.plugintool.AutoService; import ghidra.framework.plugintool.ComponentProviderAdapter; import ghidra.framework.plugintool.annotation.AutoServiceConsumed; import ghidra.program.model.address.*; +import ghidra.program.model.listing.Program; +import ghidra.program.model.mem.MemoryBlock; import ghidra.program.util.ProgramLocation; import ghidra.program.util.ProgramSelection; import ghidra.trace.model.Trace; @@ -52,6 +57,7 @@ import ghidra.trace.model.Trace.TraceMemoryRegionChangeType; import ghidra.trace.model.TraceDomainObjectListener; import ghidra.trace.model.memory.TraceMemoryManager; import ghidra.trace.model.memory.TraceMemoryRegion; +import ghidra.util.Msg; import ghidra.util.database.ObjectKey; import ghidra.util.table.GhidraTable; import ghidra.util.table.GhidraTableFilterPanel; @@ -213,9 +219,13 @@ public class DebuggerRegionsProvider extends ComponentProviderAdapter { private final DebuggerRegionsPlugin plugin; @AutoServiceConsumed - private DebuggerListingService listingService; + private DebuggerStaticMappingService staticMappingService; @AutoServiceConsumed private DebuggerTraceManagerService traceManager; + @AutoServiceConsumed + private DebuggerListingService listingService; + @AutoServiceConsumed + ProgramManager programManager; @SuppressWarnings("unused") private final AutoService.Wiring autoServiceWiring; @@ -229,7 +239,17 @@ public class DebuggerRegionsProvider extends ComponentProviderAdapter { private final JPanel mainPanel = new JPanel(new BorderLayout()); + // TODO: Lazy construction of these dialogs? + private final DebuggerBlockChooserDialog blockChooserDialog; + private final DebuggerRegionMapProposalDialog regionProposalDialog; + private DebuggerRegionActionContext myActionContext; + private Program currentProgram; + private ProgramLocation currentLocation; + + DockingAction actionMapRegions; + DockingAction actionMapRegionTo; + DockingAction actionMapRegionsTo; SelectAddressesAction actionSelectAddresses; DockingAction actionSelectRows; @@ -247,6 +267,9 @@ public class DebuggerRegionsProvider extends ComponentProviderAdapter { this.autoServiceWiring = AutoService.wireServicesConsumed(plugin, this); + blockChooserDialog = new DebuggerBlockChooserDialog(); + regionProposalDialog = new DebuggerRegionMapProposalDialog(this); + setDefaultWindowPosition(WindowPosition.BOTTOM); setVisible(true); createActions(); @@ -335,12 +358,119 @@ public class DebuggerRegionsProvider extends ComponentProviderAdapter { } protected void createActions() { + actionMapRegions = MapRegionsAction.builder(plugin) + .withContext(DebuggerRegionActionContext.class) + .enabledWhen(this::isContextNonEmpty) + .popupWhen(this::isContextNonEmpty) + .onAction(this::activatedMapRegions) + .buildAndInstallLocal(this); + actionMapRegionTo = MapRegionToAction.builder(plugin) + .withContext(DebuggerRegionActionContext.class) + .enabledWhen(ctx -> currentProgram != null && ctx.getSelectedRegions().size() == 1) + .popupWhen(ctx -> currentProgram != null && ctx.getSelectedRegions().size() == 1) + .onAction(this::activatedMapRegionTo) + .buildAndInstallLocal(this); + actionMapRegionsTo = MapRegionsToAction.builder(plugin) + .withContext(DebuggerRegionActionContext.class) + .enabledWhen(ctx -> currentProgram != null && isContextNonEmpty(ctx)) + .popupWhen(ctx -> currentProgram != null && isContextNonEmpty(ctx)) + .onAction(this::activatedMapRegionsTo) + .buildAndInstallLocal(this); actionSelectAddresses = new SelectAddressesAction(); actionSelectRows = SelectRowsAction.builder(plugin) .description("Select regions by trace selection") .enabledWhen(ctx -> currentTrace != null) .onAction(this::activatedSelectCurrent) .buildAndInstallLocal(this); + + contextChanged(); + } + + private boolean isContextNonEmpty(DebuggerRegionActionContext ctx) { + return !ctx.getSelectedRegions().isEmpty(); + } + + private static Set getSelectedRegions(DebuggerRegionActionContext ctx) { + if (ctx == null) { + return null; + } + return ctx.getSelectedRegions() + .stream() + .map(r -> r.getRegion()) + .collect(Collectors.toSet()); + } + + private void activatedMapRegions(DebuggerRegionActionContext ignored) { + mapRegions(getSelectedRegions(myActionContext)); + } + + private void activatedMapRegionsTo(DebuggerRegionActionContext ignored) { + Set sel = getSelectedRegions(myActionContext); + if (sel == null || sel.isEmpty()) { + return; + } + mapRegionsTo(sel); + } + + private void activatedMapRegionTo(DebuggerRegionActionContext ignored) { + Set sel = getSelectedRegions(myActionContext); + if (sel == null || sel.size() != 1) { + return; + } + mapRegionTo(sel.iterator().next()); + } + + protected void promptRegionProposal(Collection proposal) { + if (proposal.isEmpty()) { + Msg.showInfo(this, getComponent(), "Map Regions", + "Could not formulate a propsal for any selection region." + + " You may need to import and/or open the destination images first."); + return; + } + Collection adjusted = + regionProposalDialog.adjustCollection(getTool(), proposal); + if (adjusted == null || staticMappingService == null) { + return; + } + tool.executeBackgroundCommand( + new MapRegionsBackgroundCommand(staticMappingService, adjusted), currentTrace); + } + + protected void mapRegions(Set regions) { + if (staticMappingService == null) { + return; + } + Map map = staticMappingService.proposeRegionMaps(regions, + List.of(programManager.getAllOpenPrograms())); + Collection proposal = MapProposal.flatten(map.values()); + promptRegionProposal(proposal); + } + + protected void mapRegionsTo(Set regions) { + if (staticMappingService == null) { + return; + } + Program program = currentProgram; + if (program == null) { + return; + } + RegionMapProposal map = staticMappingService.proposeRegionMap(regions, program); + Collection proposal = map.computeMap().values(); + promptRegionProposal(proposal); + } + + protected void mapRegionTo(TraceMemoryRegion region) { + if (staticMappingService == null) { + return; + } + ProgramLocation location = currentLocation; + MemoryBlock block = computeBlock(location); + if (block == null) { + return; + } + RegionMapProposal map = + staticMappingService.proposeRegionMap(region, location.getProgram(), block); + promptRegionProposal(map.computeMap().values()); } private void activatedSelectCurrent(ActionContext ignored) { @@ -385,6 +515,34 @@ public class DebuggerRegionsProvider extends ComponentProviderAdapter { return mainPanel; } + public void setProgram(Program program) { + currentProgram = program; + String name = (program == null ? "..." : program.getName()); + actionMapRegionTo.getPopupMenuData().setMenuItemName(MapRegionToAction.NAME_PREFIX + name); + actionMapRegionsTo.getPopupMenuData() + .setMenuItemName(MapRegionsToAction.NAME_PREFIX + name); + } + + public static MemoryBlock computeBlock(ProgramLocation location) { + return DebuggerModulesProvider.computeBlock(location); + } + + public static String computeBlockName(ProgramLocation location) { + return DebuggerModulesProvider.computeBlockName(location); + } + + public void setLocation(ProgramLocation location) { + currentLocation = location; + String name = MapRegionToAction.NAME_PREFIX + computeBlockName(location); + actionMapRegionTo.getPopupMenuData().setMenuItemName(name); + } + + public void programClosed(Program program) { + if (currentProgram == program) { + currentProgram = null; + } + } + public void setTrace(Trace trace) { if (currentTrace == trace) { return; @@ -409,4 +567,14 @@ public class DebuggerRegionsProvider extends ComponentProviderAdapter { } currentTrace.addListener(regionsListener); } + + public Entry askBlock(TraceMemoryRegion region, Program program, + MemoryBlock block) { + if (programManager == null) { + Msg.warn(this, "No program manager!"); + return null; + } + return blockChooserDialog.chooseBlock(getTool(), region, + List.of(programManager.getAllOpenPrograms())); + } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerAddMappingDialog.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerAddMappingDialog.java index 54adfe4b36..912f5c158a 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerAddMappingDialog.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerAddMappingDialog.java @@ -35,7 +35,6 @@ import ghidra.program.util.ProgramLocation; import ghidra.trace.model.*; import ghidra.trace.model.modules.TraceConflictedMappingException; import ghidra.util.MathUtilities; -import ghidra.util.database.UndoableTransaction; import ghidra.util.layout.PairLayout; public class DebuggerAddMappingDialog extends DialogComponentProvider { @@ -296,10 +295,8 @@ public class DebuggerAddMappingDialog extends DialogComponentProvider { ProgramLocation to = new ProgramLocation(program, fieldProgRange.getRange().getMinAddress()); - try (UndoableTransaction tid = - UndoableTransaction.start(trace, "Add Static Mapping", false)) { + try { mappingService.addMapping(from, to, getLength(), true); - tid.commit(); } catch (TraceConflictedMappingException e) { throw new AssertionError(e); // I said truncateExisting diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModuleMapProposalDialog.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModuleMapProposalDialog.java index 5a59e66f05..fe31078150 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModuleMapProposalDialog.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModuleMapProposalDialog.java @@ -24,9 +24,10 @@ import javax.swing.table.TableColumnModel; import docking.widgets.table.*; import docking.widgets.table.DefaultEnumeratedColumnTableModel.EnumeratedTableColumn; +import ghidra.app.plugin.core.debug.gui.AbstractDebuggerMapProposalDialog; import ghidra.app.plugin.core.debug.gui.DebuggerResources; import ghidra.app.plugin.core.debug.gui.DebuggerResources.MapModulesAction; -import ghidra.app.services.DebuggerStaticMappingService.ModuleMapEntry; +import ghidra.app.services.ModuleMapProposal.ModuleMapEntry; import ghidra.framework.model.DomainFile; import ghidra.program.model.address.Address; import ghidra.program.model.listing.Program; @@ -43,8 +44,8 @@ public class DebuggerModuleMapProposalDialog MODULE_NAME("Module", String.class, e -> e.getModule().getName()), DYNAMIC_BASE("Dynamic Base", Address.class, e -> e.getModule().getBase()), CHOOSE("Choose", String.class, e -> "Choose Program", (e, v) -> nop()), - PROGRAM_NAME("Program", String.class, e -> e.getProgram().getName()), - STATIC_BASE("Static Base", Address.class, e -> e.getProgram().getImageBase()), + PROGRAM_NAME("Program", String.class, e -> e.getToProgram().getName()), + STATIC_BASE("Static Base", Address.class, e -> e.getToProgram().getImageBase()), SIZE("Size", Long.class, e -> e.getModuleRange().getLength()); private final String header; @@ -146,7 +147,7 @@ public class DebuggerModuleMapProposalDialog } private void chooseAndSetProgram(ModuleMapEntry entry) { - DomainFile file = provider.askProgram(entry.getProgram()); + DomainFile file = provider.askProgram(entry.getToProgram()); if (file == null) { return; } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesPlugin.java index 4bd84e7aea..76a75d1300 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesPlugin.java @@ -24,25 +24,24 @@ import ghidra.app.services.*; import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.util.PluginStatus; -@PluginInfo( // - shortDescription = "Debugger module and section manager", // - description = "GUI to manage modules and sections", // - category = PluginCategoryNames.DEBUGGER, // - packageName = DebuggerPluginPackage.NAME, // - status = PluginStatus.RELEASED, // +@PluginInfo( + shortDescription = "Debugger module and section manager", + description = "GUI to manage modules and sections", + category = PluginCategoryNames.DEBUGGER, + packageName = DebuggerPluginPackage.NAME, + status = PluginStatus.RELEASED, eventsConsumed = { - ProgramActivatedPluginEvent.class, // - ProgramLocationPluginEvent.class, // - ProgramClosedPluginEvent.class, // - TraceActivatedPluginEvent.class, // - }, // - servicesRequired = { // - DebuggerModelService.class, // - DebuggerStaticMappingService.class, // - DebuggerTraceManagerService.class, // - ProgramManager.class, // - } // -) + ProgramActivatedPluginEvent.class, + ProgramLocationPluginEvent.class, + ProgramClosedPluginEvent.class, + TraceActivatedPluginEvent.class, + }, + servicesRequired = { + DebuggerModelService.class, + DebuggerStaticMappingService.class, + DebuggerTraceManagerService.class, + ProgramManager.class, + }) public class DebuggerModulesPlugin extends AbstractDebuggerPlugin { protected DebuggerModulesProvider provider; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProvider.java index 1fe24fe85f..fc6bcea6f4 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProvider.java @@ -38,6 +38,7 @@ import docking.widgets.table.CustomToStringCellRenderer; import docking.widgets.table.DefaultEnumeratedColumnTableModel.EnumeratedTableColumn; import docking.widgets.table.TableFilter; import ghidra.app.plugin.core.debug.DebuggerPluginPackage; +import ghidra.app.plugin.core.debug.gui.DebuggerBlockChooserDialog; import ghidra.app.plugin.core.debug.gui.DebuggerResources; import ghidra.app.plugin.core.debug.gui.DebuggerResources.*; import ghidra.app.plugin.core.debug.service.modules.MapModulesBackgroundCommand; @@ -45,7 +46,8 @@ import ghidra.app.plugin.core.debug.service.modules.MapSectionsBackgroundCommand import ghidra.app.plugin.core.debug.utils.BackgroundUtils; import ghidra.app.plugin.core.debug.utils.DebouncedRowWrappedEnumeratedColumnTableModel; import ghidra.app.services.*; -import ghidra.app.services.DebuggerStaticMappingService.*; +import ghidra.app.services.ModuleMapProposal.ModuleMapEntry; +import ghidra.app.services.SectionMapProposal.SectionMapEntry; import ghidra.async.AsyncUtils; import ghidra.async.TypeSpec; import ghidra.framework.main.AppInfo; @@ -634,7 +636,9 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter { } private void loadModules() { + moduleTable.getSelectionModel().clearSelection(); moduleTableModel.clear(); + sectionTable.getSelectionModel().clearSelection(); sectionTableModel.clear(); if (currentTrace == null) { @@ -758,8 +762,8 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter { .onAction(this::activatedMapModules) .buildAndInstallLocal(this); actionMapModuleTo = MapModuleToAction.builder(plugin) - .enabledWhen(ctx -> currentProgram != null) .withContext(DebuggerModuleActionContext.class) + .enabledWhen(ctx -> currentProgram != null && ctx.getSelectedModules().size() == 1) .popupWhen(ctx -> currentProgram != null && ctx.getSelectedModules().size() == 1) .onAction(this::activatedMapModuleTo) .buildAndInstallLocal(this); @@ -769,13 +773,13 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter { .onAction(this::activatedMapSections) .buildAndInstallLocal(this); actionMapSectionTo = MapSectionToAction.builder(plugin) - .enabledWhen(ctx -> currentProgram != null) .withContext(DebuggerSectionActionContext.class) + .enabledWhen(ctx -> currentProgram != null && ctx.getSelectedSections().size() == 1) .popupWhen(ctx -> currentProgram != null && ctx.getSelectedSections().size() == 1) .onAction(this::activatedMapSectionTo) .buildAndInstallLocal(this); actionMapSectionsTo = MapSectionsToAction.builder(plugin) - .enabledWhen(ctx -> currentProgram != null) + .enabledWhen(ctx -> currentProgram != null && isContextSectionsOfOneModule(ctx)) .popupWhen(ctx -> currentProgram != null && isContextSectionsOfOneModule(ctx)) .onAction(this::activatedMapSectionsTo) .buildAndInstallLocal(this); @@ -1006,7 +1010,7 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter { } Map map = staticMappingService.proposeModuleMaps(modules, List.of(programManager.getAllOpenPrograms())); - Collection proposal = ModuleMapProposal.flatten(map.values()); + Collection proposal = MapProposal.flatten(map.values()); promptModuleProposal(proposal); } @@ -1045,9 +1049,9 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter { } Set modules = sections.stream().map(TraceSection::getModule).collect(Collectors.toSet()); - Map map = staticMappingService.proposeSectionMaps(modules, + Map map = staticMappingService.proposeSectionMaps(modules, List.of(programManager.getAllOpenPrograms())); - Collection proposal = SectionMapProposal.flatten(map.values()); + Collection proposal = MapProposal.flatten(map.values()); Collection filtered = proposal.stream() .filter(e -> sections.contains(e.getSection())) .collect(Collectors.toSet()); @@ -1085,7 +1089,9 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter { if (block == null) { return; } - promptSectionProposal(List.of(new SectionMapEntry(section, location.getProgram(), block))); + SectionMapProposal map = + staticMappingService.proposeSectionMap(section, location.getProgram(), block); + promptSectionProposal(map.computeMap().values()); } protected Set collectBlocksInOpenPrograms() { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerSectionMapProposalDialog.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerSectionMapProposalDialog.java index 717823c458..b01faaeb40 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerSectionMapProposalDialog.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerSectionMapProposalDialog.java @@ -25,9 +25,10 @@ import javax.swing.table.TableColumnModel; import docking.widgets.table.*; import docking.widgets.table.DefaultEnumeratedColumnTableModel.EnumeratedTableColumn; +import ghidra.app.plugin.core.debug.gui.AbstractDebuggerMapProposalDialog; import ghidra.app.plugin.core.debug.gui.DebuggerResources; import ghidra.app.plugin.core.debug.gui.DebuggerResources.MapSectionsAction; -import ghidra.app.services.DebuggerStaticMappingService.SectionMapEntry; +import ghidra.app.services.SectionMapProposal.SectionMapEntry; import ghidra.program.model.address.Address; import ghidra.program.model.listing.Program; import ghidra.program.model.mem.MemoryBlock; @@ -45,10 +46,10 @@ public class DebuggerSectionMapProposalDialog SECTION_NAME("Section", String.class, e -> e.getSection().getName()), DYNAMIC_BASE("Dynamic Base", Address.class, e -> e.getSection().getStart()), CHOOSE("Choose", String.class, e -> "Choose Block", (e, s) -> nop()), - PROGRAM_NAME("Program", String.class, e -> e.getProgram().getName()), + PROGRAM_NAME("Program", String.class, e -> e.getToProgram().getName()), BLOCK_NAME("Block", String.class, e -> e.getBlock().getName()), STATIC_BASE("Static Base", Address.class, e -> e.getBlock().getStart()), - SIZE("Size", Long.class, e -> e.getLength()); + SIZE("Size", Long.class, e -> e.getMappingLength()); private final String header; private final Class cls; @@ -151,7 +152,7 @@ public class DebuggerSectionMapProposalDialog private void chooseAndSetBlock(SectionMapEntry entry) { Map.Entry choice = - provider.askBlock(entry.getSection(), entry.getProgram(), entry.getBlock()); + provider.askBlock(entry.getSection(), entry.getToProgram(), entry.getBlock()); if (choice == null) { return; } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/AbstractMapEntry.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/AbstractMapEntry.java new file mode 100644 index 0000000000..3765a1284d --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/AbstractMapEntry.java @@ -0,0 +1,99 @@ +/* ### + * 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.util.Objects; + +import ghidra.app.services.MapEntry; +import ghidra.program.model.listing.Program; +import ghidra.program.util.ProgramLocation; +import ghidra.trace.model.*; + +public abstract class AbstractMapEntry implements MapEntry { + protected final Trace fromTrace; + protected final T fromObject; + protected Program toProgram; + protected P toObject; + + public AbstractMapEntry(Trace fromTrace, T fromObject, Program toProgram, P toObject) { + this.fromTrace = fromTrace; + this.fromObject = fromObject; + this.toProgram = toProgram; + this.toObject = toObject; + } + + @Override + public boolean equals(Object obj) { + // TODO: I guess comparing only the "from" object is sufficient.... + if (!(obj instanceof AbstractMapEntry)) { + return false; + } + AbstractMapEntry that = (AbstractMapEntry) obj; + return this.fromObject == that.fromObject; + } + + @Override + public int hashCode() { + return Objects.hash(fromObject); + } + + @Override + public Trace getFromTrace() { + return fromTrace; + } + + @Override + public T getFromObject() { + return fromObject; + } + + @Override + public TraceLocation getFromTraceLocation() { + return new DefaultTraceLocation(fromTrace, null, getFromLifespan(), + getFromRange().getMinAddress()); + } + + protected void setToObject(Program toProgram, P toObject) { + this.toProgram = toProgram; + this.toObject = toObject; + } + + @Override + public Program getToProgram() { + return toProgram; + } + + @Override + public P getToObject() { + return toObject; + } + + @Override + public ProgramLocation getToProgramLocation() { + return new ProgramLocation(toProgram, getToRange().getMinAddress()); + } + + /** + * {@inheritDoc} + * + * @implNote Ideally the "from" and "to" objects have exactly the same length. If they don't, + * take the minimum. + */ + @Override + public long getMappingLength() { + return Math.min(getFromRange().getLength(), getToRange().getLength()); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/AbstractMapProposal.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/AbstractMapProposal.java new file mode 100644 index 0000000000..1ed834ea38 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/AbstractMapProposal.java @@ -0,0 +1,145 @@ +/* ### + * 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.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +import ghidra.app.services.MapEntry; +import ghidra.app.services.MapProposal; +import ghidra.program.model.address.AddressRange; +import ghidra.program.model.listing.Program; +import ghidra.trace.model.Trace; + +public abstract class AbstractMapProposal> + implements MapProposal { + + protected abstract static class Matcher { + protected final T fromObject; + protected final P toObject; + protected final AddressRange fromRange; + protected final AddressRange toRange; + protected final double score; + + protected Matcher(T fromObject, P toObject) { + this.fromObject = fromObject; + this.toObject = toObject; + this.fromRange = fromObject == null ? null : getFromRange(); + this.toRange = toObject == null ? null : getToRange(); + this.score = fromObject == null || toObject == null ? 0 : computeScore(); + } + + protected abstract AddressRange getFromRange(); + + protected abstract AddressRange getToRange(); + + protected double computeScore() { + return computeKeyMatchScore() + computeLengthScore(); + } + + protected int computeKeyMatchScore() { + return 3; + } + + protected long shiftRight1RoundUp(long val) { + if ((val & 1) == 1) { + return (val >>> 1) + 1; + } + return val >>> 1; + } + + protected double computeLengthScore() { + long fLen = fromRange.getLength(); + long tLen = toRange.getLength(); + for (int bitsmatched = 64; bitsmatched > 0; bitsmatched--) { + if ((fLen == tLen)) { + return bitsmatched / 6.4d; + } + fLen = shiftRight1RoundUp(fLen); + tLen = shiftRight1RoundUp(tLen); + } + return 0; + } + } + + protected static abstract class MatcherMap> { + protected Map> fromsByJoin = new LinkedHashMap<>(); + protected Map map = new LinkedHashMap<>(); + + protected abstract M newMatcher(T fromObject, P toObject); + + protected abstract K getFromJoinKey(T fromObject); + + protected abstract K getToJoinKey(P toObject); + + protected void processFromObject(T fromObject) { + fromsByJoin.computeIfAbsent(getFromJoinKey(fromObject), k -> new LinkedHashSet<>()) + .add(fromObject); + } + + protected void processToObject(P toObject) { + Set froms = fromsByJoin.get(getToJoinKey(toObject)); + if (froms == null) { + return; + } + for (T f : froms) { + M bestM = map.get(f); + M candM = newMatcher(f, toObject); + if (bestM == null || candM.score > bestM.score) { + map.put(f, candM); + } + } + } + + protected double averageScore() { + return map.values() + .stream() + .reduce(0d, (s, m) -> s + m.score, Double::sum) / + map.size(); + } + + protected Map computeMap(Function newEntry) { + return map.values() + .stream() + .filter(m -> m.fromObject != null && m.toObject != null) + .collect(Collectors.toMap(m -> m.fromObject, newEntry)); + } + + protected P getToObject(T fromObject) { + M m = map.get(fromObject); + return m == null ? null : m.toObject; + } + } + + protected final Trace trace; + protected final Program program; + + public AbstractMapProposal(Trace trace, Program program) { + this.trace = trace; + this.program = program; + } + + @Override + public Trace getTrace() { + return trace; + } + + @Override + public Program getProgram() { + return program; + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingProposals.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingProposals.java new file mode 100644 index 0000000000..32d69c345a --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingProposals.java @@ -0,0 +1,287 @@ +/* ### + * 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.function.BiPredicate; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import ghidra.app.services.*; +import ghidra.dbg.util.PathUtils; +import ghidra.framework.model.DomainFile; +import ghidra.graph.*; +import ghidra.graph.jung.JungDirectedGraph; +import ghidra.program.model.listing.Program; +import ghidra.program.model.mem.MemoryBlock; +import ghidra.trace.model.memory.TraceMemoryRegion; +import ghidra.trace.model.modules.TraceModule; +import ghidra.trace.model.modules.TraceSection; +import ghidra.util.Msg; + +public enum DebuggerStaticMappingProposals { + ; + + protected static String getLastLower(String path) { + return new File(path).getName().toLowerCase(); + } + + /** + * Check if either the program's name, its executable path, or its domain file name contains the + * given module name + * + * @param program the program whose names to check + * @param moduleLowerName the module name to check for in lower case + * @return true if matched, false if not + */ + protected static boolean namesContain(Program program, String moduleLowerName) { + DomainFile df = program.getDomainFile(); + if (df == null || df.getProjectLocator() == null) { + return false; + } + String programName = getLastLower(program.getName()); + if (programName.contains(moduleLowerName)) { + return true; + } + String exePath = program.getExecutablePath(); + if (exePath != null) { + String execName = getLastLower(exePath); + if (execName.contains(moduleLowerName)) { + return true; + } + } + String fileName = df.getName().toLowerCase(); + if (fileName.contains(moduleLowerName)) { + return true; + } + return false; + } + + protected abstract static class ProposalGenerator> { + protected abstract MP proposeMap(F from, T to); + + protected abstract J computeFromJoinKey(F from); + + protected abstract boolean isJoined(J key, T to); + + protected MP proposeBestMap(F from, Collection tos) { + double bestScore = -1; + MP bestMap = null; + for (T t : tos) { + MP map = proposeMap(from, t); + double score = map.computeScore(); + // NOTE: Ties prefer first in candidate collection + if (score > bestScore) { + bestScore = score; + bestMap = map; + } + } + return bestMap; + } + + protected Map proposeBestMaps(Collection froms, + Collection tos) { + Map result = new LinkedHashMap<>(); + for (F f : froms) { + J joinKey = computeFromJoinKey(f); + Set joined = tos.stream() + .filter(t -> isJoined(joinKey, t)) + // Need to preserve order here + .collect(Collectors.toCollection(LinkedHashSet::new)); + MP map = proposeBestMap(f, joined); + if (map != null) { + result.put(f, map); + } + } + return result; + } + } + + protected static class ModuleMapProposalGenerator + extends ProposalGenerator { + @Override + protected ModuleMapProposal proposeMap(TraceModule from, Program to) { + return new DefaultModuleMapProposal(from, to); + } + + @Override + protected String computeFromJoinKey(TraceModule from) { + return getLastLower(from.getName()); + } + + @Override + protected boolean isJoined(String key, Program to) { + return namesContain(to, key); + } + } + + protected static final ModuleMapProposalGenerator MODULES = new ModuleMapProposalGenerator(); + + public static ModuleMapProposal proposeModuleMap(TraceModule module, Program program) { + return MODULES.proposeMap(module, program); + } + + public static ModuleMapProposal proposeModuleMap(TraceModule module, + Collection programs) { + return MODULES.proposeBestMap(module, programs); + } + + public static Map proposeModuleMaps( + Collection modules, Collection programs) { + return MODULES.proposeBestMaps(modules, programs); + } + + protected static class SectionMapProposalGenerator + extends ProposalGenerator { + @Override + protected SectionMapProposal proposeMap(TraceModule from, Program to) { + return new DefaultSectionMapProposal(from, to); + } + + @Override + protected String computeFromJoinKey(TraceModule from) { + return getLastLower(from.getName()); + } + + @Override + protected boolean isJoined(String key, Program to) { + return namesContain(to, key); + } + } + + protected static final SectionMapProposalGenerator SECTIONS = new SectionMapProposalGenerator(); + + public static SectionMapProposal proposeSectionMap(TraceSection section, Program program, + MemoryBlock block) { + return new DefaultSectionMapProposal(section, program, block); + } + + public static SectionMapProposal proposeSectionMap(TraceModule module, Program program) { + return SECTIONS.proposeMap(module, program); + } + + public static SectionMapProposal proposeSectionMap(TraceModule module, + Collection programs) { + return SECTIONS.proposeBestMap(module, programs); + } + + public static Map proposeSectionMaps( + Collection modules, Collection programs) { + return SECTIONS.proposeBestMaps(modules, programs); + } + + protected static class RegionMapProposalGenerator extends + ProposalGenerator, Program, Set, // + RegionMapProposal> { + + @Override + protected RegionMapProposal proposeMap(Collection from, + Program to) { + return new DefaultRegionMapProposal(from, to); + } + + @Override + protected Set computeFromJoinKey(Collection from) { + return from.stream() + .flatMap(r -> getLikelyModulesFromName(r).stream()) + .map(n -> getLastLower(n)) + .collect(Collectors.toSet()); + } + + @Override + protected boolean isJoined(Set key, Program to) { + return key.stream().anyMatch(n -> namesContain(to, n)); + } + } + + protected static final RegionMapProposalGenerator REGIONS = new RegionMapProposalGenerator(); + + public static RegionMapProposal proposeRegionMap(TraceMemoryRegion region, Program program, + MemoryBlock block) { + return new DefaultRegionMapProposal(region, program, block); + } + + public static RegionMapProposal proposeRegionMap( + Collection regions, + Program program) { + return REGIONS.proposeMap(Collections.unmodifiableCollection(regions), program); + } + + public static RegionMapProposal proposeRegionMap( + Collection regions, + Collection programs) { + return REGIONS.proposeBestMap(Collections.unmodifiableCollection(regions), programs); + } + + public static Set> groupByComponents(Collection vertices, + Function precompute, BiPredicate areConnected) { + List vs = List.copyOf(vertices); + List pres = vs.stream().map(precompute).collect(Collectors.toList()); + GDirectedGraph> graph = new JungDirectedGraph<>(); + for (V v : vs) { + graph.addVertex(v); // Lone regions should still be considered + } + int size = vs.size(); + for (int i = 0; i < size; i++) { + V v1 = vs.get(i); + J j1 = pres.get(i); + for (int j = i + 1; j < size; j++) { + V v2 = vs.get(j); + J j2 = pres.get(j); + if (areConnected.test(j1, j2)) { + graph.addEdge(new DefaultGEdge<>(v1, v2)); + graph.addEdge(new DefaultGEdge<>(v2, v1)); + } + } + } + return GraphAlgorithms.getStronglyConnectedComponents(graph); + } + + protected static Set getLikelyModulesFromName(TraceMemoryRegion region) { + String key; + try { + List path = PathUtils.parse(region.getName()); + key = PathUtils.getKey(path); + if (PathUtils.isIndex(key)) { + key = PathUtils.parseIndex(key); + } + } + catch (IllegalArgumentException e) { // Parse error + Msg.error(DebuggerStaticMappingProposals.class, + "Encountered unparsable path: " + region.getName()); + key = region.getName(); // Not a great fallback, but it'll have to do + } + return Stream.of(key.split("\\s+")) + .filter(n -> n.replaceAll("[0-9A-Fa-f]+", "").length() >= 5) + .collect(Collectors.toSet()); + } + + public static Set> groupRegionsByLikelyModule( + Collection regions) { + return groupByComponents(regions, r -> getLikelyModulesFromName(r), (m1, m2) -> { + return m1.stream().anyMatch(m2::contains); + }); + } + + public static Map, RegionMapProposal> proposeRegionMaps( + Collection regions, + Collection programs) { + Set> groups = groupRegionsByLikelyModule(regions); + return REGIONS.proposeBestMaps(groups, programs); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingServicePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingServicePlugin.java index 9348c19b52..cac79b061b 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingServicePlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingServicePlugin.java @@ -15,8 +15,6 @@ */ package ghidra.app.plugin.core.debug.service.modules; -import java.io.File; -import java.io.IOException; import java.net.URL; import java.util.*; import java.util.Map.Entry; @@ -34,23 +32,21 @@ import ghidra.app.plugin.core.debug.event.TraceClosedPluginEvent; import ghidra.app.plugin.core.debug.event.TraceOpenedPluginEvent; import ghidra.app.plugin.core.debug.utils.*; import ghidra.app.services.*; +import ghidra.app.services.ModuleMapProposal.ModuleMapEntry; +import ghidra.app.services.RegionMapProposal.RegionMapEntry; +import ghidra.app.services.SectionMapProposal.SectionMapEntry; import ghidra.async.AsyncDebouncer; import ghidra.async.AsyncTimer; -import ghidra.framework.data.OpenedDomainFile; import ghidra.framework.model.*; import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.annotation.AutoServiceConsumed; import ghidra.framework.plugintool.util.PluginStatus; -import ghidra.framework.store.FileSystem; import ghidra.generic.util.datastruct.TreeValueSortedMap; import ghidra.generic.util.datastruct.ValueSortedMap; import ghidra.program.model.address.*; -import ghidra.program.model.listing.Library; import ghidra.program.model.listing.Program; import ghidra.program.model.mem.MemoryBlock; -import ghidra.program.model.symbol.ExternalManager; import ghidra.program.util.ProgramLocation; -import ghidra.trace.database.DBTraceUtils; import ghidra.trace.model.*; import ghidra.trace.model.Trace.TraceStaticMappingChangeType; import ghidra.trace.model.memory.TraceMemoryRegion; @@ -60,7 +56,6 @@ import ghidra.util.Msg; import ghidra.util.database.UndoableTransaction; import ghidra.util.datastruct.ListenerSet; import ghidra.util.exception.CancelledException; -import ghidra.util.exception.VersionException; import ghidra.util.task.TaskMonitor; @PluginInfo( @@ -85,199 +80,6 @@ import ghidra.util.task.TaskMonitor; public class DebuggerStaticMappingServicePlugin extends Plugin implements DebuggerStaticMappingService, DomainFolderChangeAdapter { - protected static class PluginModuleMapProposal implements ModuleMapProposal { - private final TraceModule module; - private final Program program; - - private final NavigableMap matchers = new TreeMap<>(); - private Address imageBase; - private Address moduleBase; - private long imageSize; - private AddressRange moduleRange; // TODO: This is now in the trace schema. Use it. - - public PluginModuleMapProposal(TraceModule module, Program program) { - this.module = module; - this.program = program; - processProgram(); - processModule(); - } - - @Override - public TraceModule getModule() { - return module; - } - - @Override - public Program getProgram() { - return program; - } - - private RegionMatcher getMatcher(long baseOffset) { - return matchers.computeIfAbsent(baseOffset, RegionMatcher::new); - } - - private void processProgram() { - imageBase = program.getImageBase(); - imageSize = ModuleMapEntry.computeImageSize(program); - // TODO: How to handle Harvard architectures? - for (MemoryBlock block : program.getMemory().getBlocks()) { - if (!ModuleMapEntry.includeBlock(program, block)) { - continue; - } - getMatcher(block.getStart().subtract(imageBase)).block = block; - } - } - - /** - * Must be called after processProgram, so that image size is known - */ - private void processModule() { - moduleBase = module.getBase(); - try { - moduleRange = new AddressRangeImpl(moduleBase, imageSize); - } - catch (AddressOverflowException e) { - return; // Just score it as having no matches? - } - for (TraceMemoryRegion region : module.getTrace() - .getMemoryManager() - .getRegionsIntersecting(module.getLifespan(), moduleRange)) { - getMatcher(region.getMinAddress().subtract(moduleBase)).region = region; - } - } - - @Override - public double computeScore() { - return ((double) matchers.values() - .stream() - .reduce(0, (s, m) -> s + m.score(), Integer::sum)) / - matchers.size(); - } - - @Override - public Map computeMap() { - return Map.of(module, new ModuleMapEntry(module, program, moduleRange)); - } - } - - protected static class RegionMatcher { - private MemoryBlock block; - private TraceMemoryRegion region; - - public RegionMatcher(long baseOffset) { - } - - private int score() { - if (block == null || region == null) { - return 0; // Unmatched - } - int score = 3; // For the matching offset - if (block.getSize() == region.getLength()) { - score += 10; - } - return score; - } - } - - protected static class PluginSectionMapProposal implements SectionMapProposal { - private final TraceModule module; - private final Program program; - private final Map matchers = new LinkedHashMap<>(); - - public PluginSectionMapProposal(TraceModule module, Program program) { - this.module = module; - this.program = program; - processModule(); - processProgram(); - } - - public PluginSectionMapProposal(TraceSection section, Program program, MemoryBlock block) { - this.module = section.getModule(); - this.program = program; - processSection(section); - processBlock(block); - } - - @Override - public TraceModule getModule() { - return module; - } - - @Override - public Program getProgram() { - return program; - } - - private void processSection(TraceSection section) { - matchers.put(section.getName(), new SectionMatcher(section)); - } - - private void processBlock(MemoryBlock block) { - SectionMatcher m = - matchers.computeIfAbsent(block.getName(), n -> new SectionMatcher(null)); - m.block = block; - } - - private void processModule() { - for (TraceSection section : module.getSections()) { - processSection(section); - } - } - - private void processProgram() { - for (MemoryBlock block : program.getMemory().getBlocks()) { - processBlock(block); - } - } - - @Override - public double computeScore() { - return ((double) matchers.values() - .stream() - .reduce(0, (s, m) -> s + m.score(), Integer::sum)) / - matchers.size(); - } - - @Override - public Map computeMap() { - return matchers.values() - .stream() - .filter(m -> m.section != null && m.block != null) - .collect(Collectors.toMap(m -> m.section, - m -> new SectionMapEntry(m.section, program, m.block))); - } - - @Override - public MemoryBlock getDestination(TraceSection section) { - SectionMatcher m = matchers.get(section.getName()); - return m == null ? null : m.block; - } - } - - protected static class SectionMatcher { - private final TraceSection section; - private MemoryBlock block; - - public SectionMatcher(TraceSection section) { - this.section = section; - } - - public int score() { - if (section == null || block == null) { - return 0; // Unmatched - } - int score = 3; // For the matching name - if (section.getRange().getLength() == block.getSize()) { - score += 10; - } - if ((section.getStart().getOffset() & 0xfff) == (block.getStart().getOffset() & - 0xfff)) { - score += 20; - } - return score; - } - } - protected class MappingEntry { private final TraceStaticMapping mapping; @@ -937,184 +739,75 @@ public class DebuggerStaticMappingServicePlugin extends Plugin @Override public void addMapping(TraceLocation from, ProgramLocation to, long length, boolean truncateExisting) throws TraceConflictedMappingException { - Program tp = to.getProgram(); - if (tp instanceof TraceProgramView) { - throw new IllegalArgumentException( - "Mapping destination cannot be a " + TraceProgramView.class.getSimpleName()); + try (UndoableTransaction tid = + UndoableTransaction.start(from.getTrace(), "Add mapping", true)) { + DebuggerStaticMappingUtils.addMapping(from, to, length, truncateExisting); } - TraceStaticMappingManager manager = from.getTrace().getStaticMappingManager(); - URL toURL = ProgramURLUtils.getUrlFromProgram(tp); - if (toURL == null) { - noProject(); - } - Address fromAddress = from.getAddress(); - Address toAddress = to.getByteAddress(); - long maxFromLengthMinus1 = - fromAddress.getAddressSpace().getMaxAddress().subtract(fromAddress); - long maxToLengthMinus1 = - toAddress.getAddressSpace().getMaxAddress().subtract(toAddress); - if (Long.compareUnsigned(length - 1, maxFromLengthMinus1) > 0) { - throw new IllegalArgumentException("Length would cause address overflow in trace"); - } - if (Long.compareUnsigned(length - 1, maxToLengthMinus1) > 0) { - throw new IllegalArgumentException("Length would cause address overflow in program"); - } - Address end = fromAddress.addWrap(length - 1); - // Also check end in the destination - AddressRangeImpl range = new AddressRangeImpl(fromAddress, end); - Range fromLifespan = from.getLifespan(); - if (truncateExisting) { - long truncEnd = DBTraceUtils.lowerEndpoint(fromLifespan) - 1; - for (TraceStaticMapping existing : List - .copyOf(manager.findAllOverlapping(range, fromLifespan))) { - existing.delete(); - if (fromLifespan.hasLowerBound() && - Long.compare(existing.getStartSnap(), truncEnd) <= 0) { - manager.add(existing.getTraceAddressRange(), - Range.closed(existing.getStartSnap(), truncEnd), - existing.getStaticProgramURL(), existing.getStaticAddress()); - } - } - } - manager.add(range, fromLifespan, toURL, toAddress.toString(true)); } - static protected AddressRange clippedRange(Trace trace, String spaceName, long min, - long max) { - AddressSpace space = trace.getBaseAddressFactory().getAddressSpace(spaceName); - if (space == null) { - return null; + @Override + public void addMapping(MapEntry entry, boolean truncateExisting) + throws TraceConflictedMappingException { + try (UndoableTransaction tid = + UndoableTransaction.start(entry.getFromTrace(), "Add mapping", true)) { + DebuggerStaticMappingUtils.addMapping(entry, truncateExisting); } - Address spaceMax = space.getMaxAddress(); - if (Long.compareUnsigned(min, spaceMax.getOffset()) > 0) { - return null; + } + + @Override + public void addMappings(Collection> entries, TaskMonitor monitor, + boolean truncateExisting, String description) throws CancelledException { + Map>> byTrace = + entries.stream().collect(Collectors.groupingBy(ent -> ent.getFromTrace())); + for (Map.Entry>> ent : byTrace.entrySet()) { + Trace trace = ent.getKey(); + try (UndoableTransaction tid = + UndoableTransaction.start(trace, description, true)) { + doAddMappings(trace, ent.getValue(), monitor, truncateExisting); + } } - if (Long.compareUnsigned(max, spaceMax.getOffset()) > 0) { - return new AddressRangeImpl(space.getAddress(min), spaceMax); + } + + protected static void doAddMappings(Trace trace, Collection> entries, + TaskMonitor monitor, boolean truncateExisting) throws CancelledException { + for (MapEntry ent : entries) { + monitor.checkCanceled(); + try { + DebuggerStaticMappingUtils.addMapping(ent, truncateExisting); + } + catch (Exception e) { + Msg.error(DebuggerStaticMappingService.class, + "Could not add mapping " + ent + ": " + e.getMessage()); + } } - return new AddressRangeImpl(space.getAddress(min), space.getAddress(max)); } @Override public void addIdentityMapping(Trace from, Program toProgram, Range lifespan, boolean truncateExisting) { try (UndoableTransaction tid = - UndoableTransaction.start(from, "Add identity mappings", false)) { - doAddIdentityMapping(from, toProgram, lifespan, truncateExisting); - tid.commit(); + UndoableTransaction.start(from, "Add identity mappings", true)) { + DebuggerStaticMappingUtils.addIdentityMapping(from, toProgram, lifespan, + truncateExisting); } } - protected void doAddIdentityMapping(Trace from, Program toProgram, Range lifespan, - boolean truncateExisting) { - Map mins = new HashMap<>(); - Map maxs = new HashMap<>(); - for (AddressRange range : toProgram.getMemory().getAddressRanges()) { - mins.compute(range.getAddressSpace().getName(), (n, min) -> { - Address can = range.getMinAddress(); - if (min == null || can.compareTo(min) < 0) { - return can; - } - return min; - }); - maxs.compute(range.getAddressSpace().getName(), (n, max) -> { - Address can = range.getMaxAddress(); - if (max == null || can.compareTo(max) > 0) { - return can; - } - return max; - }); - } - for (String name : mins.keySet()) { - AddressRange range = clippedRange(from, name, mins.get(name).getOffset(), - maxs.get(name).getOffset()); - if (range == null) { - continue; - } - try { - addMapping(new DefaultTraceLocation(from, null, lifespan, range.getMinAddress()), - new ProgramLocation(toProgram, mins.get(name)), range.getLength(), - truncateExisting); - } - catch (TraceConflictedMappingException e) { - Msg.error(this, "Could not add identity mapping " + range + ": " + e.getMessage()); - } - } - } - - @Override - public void addModuleMapping(TraceModule from, long length, Program toProgram, - boolean truncateExisting) throws TraceConflictedMappingException { - TraceLocation fromLoc = - new DefaultTraceLocation(from.getTrace(), null, from.getLifespan(), from.getBase()); - ProgramLocation toLoc = new ProgramLocation(toProgram, toProgram.getImageBase()); - addMapping(fromLoc, toLoc, length, truncateExisting); - } - @Override public void addModuleMappings(Collection entries, TaskMonitor monitor, boolean truncateExisting) throws CancelledException { - Map> byTrace = new LinkedHashMap<>(); - for (ModuleMapEntry ent : entries) { - Set subCol = - byTrace.computeIfAbsent(ent.getModule().getTrace(), t -> new LinkedHashSet<>()); - subCol.add(ent); - } - for (Map.Entry> ent : byTrace.entrySet()) { - Trace trace = ent.getKey(); - try (UndoableTransaction tid = - UndoableTransaction.start(trace, "Add module mappings", false)) { - doAddModuleMappings(trace, ent.getValue(), monitor, truncateExisting); - tid.commit(); - } - } - } - - protected void doAddModuleMappings(Trace trace, Collection entries, - TaskMonitor monitor, boolean truncateExisting) throws CancelledException { - for (ModuleMapEntry ent : entries) { - monitor.checkCanceled(); - try { - DebuggerStaticMappingUtils.addModuleMapping(ent.getModule(), - ent.getModuleRange().getLength(), ent.getProgram(), truncateExisting); - } - catch (Exception e) { - Msg.error(this, "Could not add mapping " + ent + ": " + e.getMessage()); - } - } + addMappings(entries, monitor, truncateExisting, "Add module mappings"); } @Override - public void addSectionMappings(Collection entries, - TaskMonitor monitor, boolean truncateExisting) throws CancelledException { - Map> byTrace = new LinkedHashMap<>(); - for (SectionMapEntry ent : entries) { - Set subCol = - byTrace.computeIfAbsent(ent.getSection().getTrace(), t -> new LinkedHashSet<>()); - subCol.add(ent); - } - for (Map.Entry> ent : byTrace.entrySet()) { - Trace trace = ent.getKey(); - try (UndoableTransaction tid = - UndoableTransaction.start(trace, "Add section mappings", false)) { - doAddSectionMappings(trace, ent.getValue(), monitor, truncateExisting); - tid.commit(); - } - } + public void addSectionMappings(Collection entries, TaskMonitor monitor, + boolean truncateExisting) throws CancelledException { + addMappings(entries, monitor, truncateExisting, "Add sections mappings"); } - protected void doAddSectionMappings(Trace trace, Collection entries, - TaskMonitor monitor, boolean truncateExisting) throws CancelledException { - for (SectionMapEntry ent : entries) { - monitor.checkCanceled(); - try { - DebuggerStaticMappingUtils.addSectionMapping(ent.getSection(), ent.getProgram(), - ent.getBlock(), truncateExisting); - } - catch (Exception e) { - Msg.error(this, "Could not add mapping " + ent + ": " + e.getMessage()); - } - } + @Override + public void addRegionMappings(Collection entries, TaskMonitor monitor, + boolean truncateExisting) throws CancelledException { + addMappings(entries, monitor, truncateExisting, "Add regions mappings"); } protected T noTraceInfo() { @@ -1249,237 +942,85 @@ public class DebuggerStaticMappingServicePlugin extends Plugin return info.openMappedProgramsInView(set, Range.singleton(snap), failures); } - protected String normalizePath(String path) { - path = path.replace('\\', FileSystem.SEPARATOR_CHAR); - while (path.startsWith(FileSystem.SEPARATOR)) { - path = path.substring(1); + protected Collection orderCurrentFirst( + Collection programs) { + if (programManager == null) { + return programs; } - return path; - } - - protected 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); + Program currentProgram = programManager.getCurrentProgram(); + if (!programs.contains(currentProgram)) { + return programs; } - fullPath.append(path); - return folder.getProjectData().getFile(fullPath.toString()); - } - - public Set doFindPrograms(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 Set doFindProgramsByPathOrName(String modulePath, DomainFolder folder) { - Set found = doFindPrograms(modulePath, folder); - if (!found.isEmpty()) { - return found; - } - int idx = modulePath.lastIndexOf(FileSystem.SEPARATOR); - if (idx == -1) { - return Set.of(); - } - found = doFindPrograms(modulePath.substring(idx + 1), folder); - if (!found.isEmpty()) { - return found; - } - return Set.of(); - } - - public Set doFindProgramsByPathOrName(String modulePath, Project project) { - return doFindProgramsByPathOrName(modulePath, project.getProjectData().getRootFolder()); + Set reordered = new LinkedHashSet<>(programs.size()); + reordered.add(currentProgram); + reordered.addAll(programs); + return reordered; } @Override public Set findProbableModulePrograms(TraceModule module) { - // TODO: Consider folders containing existing mapping destinations - DomainFile df = module.getTrace().getDomainFile(); - String modulePath = normalizePath(module.getName()); - if (df == null) { - return doFindProgramsByPathOrName(modulePath, tool.getProject()); - } - DomainFolder parent = df.getParent(); - if (parent == null) { - return doFindProgramsByPathOrName(modulePath, tool.getProject()); - } - return doFindProgramsByPathOrName(modulePath, parent); - } - - protected void doCollectLibraries(ProjectData project, Program cur, Set col, - TaskMonitor monitor) throws CancelledException { - if (!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; - } - DomainFile libFile = project.getFile(libPath); - if (libFile == null) { - Msg.info(this, "Referenced external program not found: " + libPath); - continue; - } - try (OpenedDomainFile program = - OpenedDomainFile.open(Program.class, libFile, monitor)) { - doCollectLibraries(project, program.content, col, monitor); - } - catch (ClassCastException e) { - Msg.info(this, - "Referenced external program is not a program: " + libPath + " is " + - libFile.getDomainObjectClass()); - continue; - } - catch (VersionException | CancelledException | IOException e) { - Msg.info(this, "Referenced external program could not be opened: " + e); - continue; - } - } + return DebuggerStaticMappingUtils.findProbableModulePrograms(module, tool.getProject()); } @Override - public Set collectLibraries(Program seed, TaskMonitor monitor) - throws CancelledException { - Set result = new LinkedHashSet<>(); - doCollectLibraries(seed.getDomainFile().getParent().getProjectData(), seed, result, - monitor); - return result; + public ModuleMapProposal proposeModuleMap(TraceModule module, Program program) { + return DebuggerStaticMappingProposals.proposeModuleMap(module, program); } @Override - public PluginModuleMapProposal proposeModuleMap(TraceModule module, Program program) { - return new PluginModuleMapProposal(module, program); - } - - @Override - public PluginModuleMapProposal proposeModuleMap(TraceModule module, + public ModuleMapProposal proposeModuleMap(TraceModule module, Collection programs) { - double bestScore = -1; - PluginModuleMapProposal bestMap = null; - for (Program program : programs) { - PluginModuleMapProposal map = proposeModuleMap(module, program); - double score = map.computeScore(); - if (score == bestScore && programManager != null) { - // Prefer the current program in ties - if (programManager.getCurrentProgram() == program) { - bestMap = map; - } - } - if (score > bestScore) { - bestScore = score; - bestMap = map; - } - } - return bestMap; + return DebuggerStaticMappingProposals.proposeModuleMap(module, orderCurrentFirst(programs)); } @Override public Map proposeModuleMaps( Collection modules, Collection programs) { - Map result = new LinkedHashMap<>(); - for (TraceModule module : modules) { - String moduleName = getLastLower(module.getName()); - Set probable = programs.stream() - .filter(p -> namesContain(p, moduleName)) - .collect(Collectors.toSet()); - PluginModuleMapProposal map = proposeModuleMap(module, probable); - if (map == null) { - continue; - } - result.put(module, map); - } - return result; + return DebuggerStaticMappingProposals.proposeModuleMaps(modules, + orderCurrentFirst(programs)); } @Override - public PluginSectionMapProposal proposeSectionMap(TraceSection section, Program program, + public SectionMapProposal proposeSectionMap(TraceSection section, Program program, MemoryBlock block) { - return new PluginSectionMapProposal(section, program, block); + return DebuggerStaticMappingProposals.proposeSectionMap(section, program, block); } @Override - public PluginSectionMapProposal proposeSectionMap(TraceModule module, Program program) { - return new PluginSectionMapProposal(module, program); + public SectionMapProposal proposeSectionMap(TraceModule module, Program program) { + return DebuggerStaticMappingProposals.proposeSectionMap(module, program); } @Override - public PluginSectionMapProposal proposeSectionMap(TraceModule module, + public SectionMapProposal proposeSectionMap(TraceModule module, Collection programs) { - double bestScore = -1; - PluginSectionMapProposal bestMap = null; - for (Program program : programs) { - PluginSectionMapProposal map = proposeSectionMap(module, program); - double score = map.computeScore(); - if (score > bestScore) { - bestScore = score; - bestMap = map; - } - } - return bestMap; - } - - protected static String getLastLower(String path) { - return new File(path).getName().toLowerCase(); - } - - /** - * Check if either the program's name, its executable path, or its domain file name contains the - * given module name - * - * @param program the program whose names to check - * @param moduleLowerName the module name to check for in lower case - * @return true if matched, false if not - */ - protected boolean namesContain(Program program, String moduleLowerName) { - DomainFile df = program.getDomainFile(); - if (df == null || df.getProjectLocator() == null) { - return false; - } - String programName = getLastLower(program.getName()); - if (programName.contains(moduleLowerName)) { - return true; - } - String exePath = program.getExecutablePath(); - if (exePath != null) { - String execName = getLastLower(exePath); - if (execName.contains(moduleLowerName)) { - return true; - } - } - String fileName = df.getName().toLowerCase(); - if (fileName.contains(moduleLowerName)) { - return true; - } - return false; + return DebuggerStaticMappingProposals.proposeSectionMap(module, + orderCurrentFirst(programs)); } @Override public Map proposeSectionMaps( Collection modules, Collection programs) { - Map result = new LinkedHashMap<>(); - for (TraceModule module : modules) { - String moduleName = getLastLower(module.getName()); - Set probable = programs.stream() - .filter(p -> namesContain(p, moduleName)) - .collect(Collectors.toSet()); - PluginSectionMapProposal map = proposeSectionMap(module, probable); - if (map == null) { - continue; - } - result.put(module, map); - } - return result; + return DebuggerStaticMappingProposals.proposeSectionMaps(modules, + orderCurrentFirst(programs)); + } + + @Override + public RegionMapProposal proposeRegionMap(TraceMemoryRegion region, Program program, + MemoryBlock block) { + return DebuggerStaticMappingProposals.proposeRegionMap(region, program, block); + } + + @Override + public RegionMapProposal proposeRegionMap(Collection regions, + Program program) { + return DebuggerStaticMappingProposals.proposeRegionMap(regions, program); + } + + @Override + public Map, RegionMapProposal> proposeRegionMaps( + Collection regions, + Collection programs) { + return DebuggerStaticMappingProposals.proposeRegionMaps(regions, programs); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingUtils.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingUtils.java index 255ecbda02..7d4323f9dc 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingUtils.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingUtils.java @@ -15,23 +15,30 @@ */ package ghidra.app.plugin.core.debug.service.modules; +import java.io.IOException; import java.net.URL; -import java.util.List; +import java.util.*; import com.google.common.collect.Range; import ghidra.app.plugin.core.debug.utils.ProgramURLUtils; -import ghidra.app.services.DebuggerStaticMappingService; +import ghidra.app.services.MapEntry; +import ghidra.framework.data.OpenedDomainFile; +import ghidra.framework.model.*; +import ghidra.framework.store.FileSystem; import ghidra.program.model.address.*; +import ghidra.program.model.listing.Library; import ghidra.program.model.listing.Program; -import ghidra.program.model.mem.MemoryBlock; +import ghidra.program.model.symbol.ExternalManager; import ghidra.program.util.ProgramLocation; import ghidra.trace.database.DBTraceUtils; -import ghidra.trace.model.DefaultTraceLocation; -import ghidra.trace.model.TraceLocation; +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 { ; @@ -41,6 +48,124 @@ public enum DebuggerStaticMappingUtils { return null; } + public static DomainFile resolve(DomainFolder folder, String path) { + StringBuilder fullPath = new StringBuilder(folder.getPathname()); + if (!fullPath.toString().endsWith(FileSystem.SEPARATOR)) { + // Only root should end with /, anyway + fullPath.append(FileSystem.SEPARATOR_CHAR); + } + fullPath.append(path); + return folder.getProjectData().getFile(fullPath.toString()); + } + + public static Set findPrograms(String modulePath, DomainFolder folder) { + // TODO: If not found, consider filenames with space + extra info + while (folder != null) { + DomainFile found = resolve(folder, modulePath); + if (found != null) { + return Set.of(found); + } + folder = folder.getParent(); + } + return Set.of(); + } + + public static Set findProgramsByPathOrName(String modulePath, + DomainFolder folder) { + Set found = findPrograms(modulePath, folder); + if (!found.isEmpty()) { + return found; + } + int idx = modulePath.lastIndexOf(FileSystem.SEPARATOR); + if (idx == -1) { + return Set.of(); + } + found = findPrograms(modulePath.substring(idx + 1), folder); + if (!found.isEmpty()) { + return found; + } + return Set.of(); + } + + public static Set findProgramsByPathOrName(String modulePath, Project project) { + return findProgramsByPathOrName(modulePath, project.getProjectData().getRootFolder()); + } + + protected static String normalizePath(String path) { + path = path.replace('\\', FileSystem.SEPARATOR_CHAR); + while (path.startsWith(FileSystem.SEPARATOR)) { + path = path.substring(1); + } + return path; + } + + public static Set findProbableModulePrograms(TraceModule module, Project project) { + // TODO: Consider folders containing existing mapping destinations + DomainFile df = module.getTrace().getDomainFile(); + String modulePath = normalizePath(module.getName()); + if (df == null) { + return findProgramsByPathOrName(modulePath, project); + } + DomainFolder parent = df.getParent(); + if (parent == null) { + return findProgramsByPathOrName(modulePath, project); + } + return findProgramsByPathOrName(modulePath, parent); + } + + protected static void collectLibraries(ProjectData project, Program cur, Set col, + TaskMonitor monitor) throws CancelledException { + if (!col.add(cur)) { + 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; + } + DomainFile libFile = project.getFile(libPath); + if (libFile == null) { + Msg.info(DebuggerStaticMappingUtils.class, + "Referenced external program not found: " + libPath); + continue; + } + try (OpenedDomainFile program = + OpenedDomainFile.open(Program.class, libFile, monitor)) { + collectLibraries(project, program.content, col, monitor); + } + catch (ClassCastException e) { + Msg.info(DebuggerStaticMappingUtils.class, + "Referenced external program is not a program: " + libPath + " is " + + libFile.getDomainObjectClass()); + continue; + } + catch (VersionException | CancelledException | IOException e) { + Msg.info(DebuggerStaticMappingUtils.class, + "Referenced external program could not be opened: " + e); + continue; + } + } + } + + /** + * Recursively collect external programs, i.e., libraries, starting at the given seed + * + * @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 + */ + public static Set collectLibraries(Program seed, TaskMonitor monitor) + throws CancelledException { + Set result = new LinkedHashSet<>(); + collectLibraries(seed.getDomainFile().getParent().getProjectData(), seed, result, + monitor); + return result; + } + /** * Add a static mapping (relocation) from the given trace to the given program * @@ -48,7 +173,6 @@ public enum DebuggerStaticMappingUtils { * Note if the trace is backed by a Ghidra database, the caller must already have started a * transaction on the relevant domain object. * - * * @param from the source trace location, including lifespan * @param to the destination program location * @param length the length of the mapped region @@ -57,8 +181,7 @@ public enum DebuggerStaticMappingUtils { * {@code truncateExisting} is false. */ public static void addMapping(TraceLocation from, ProgramLocation to, long length, - boolean truncateExisting) - throws TraceConflictedMappingException { + boolean truncateExisting) throws TraceConflictedMappingException { Program tp = to.getProgram(); if (tp instanceof TraceProgramView) { throw new IllegalArgumentException( @@ -67,75 +190,99 @@ public enum DebuggerStaticMappingUtils { TraceStaticMappingManager manager = from.getTrace().getStaticMappingManager(); URL toURL = ProgramURLUtils.getUrlFromProgram(tp); if (toURL == null) { - noProject(DebuggerStaticMappingService.class); + noProject(DebuggerStaticMappingUtils.class); } - try { - Address start = from.getAddress(); - Address end = start.addNoWrap(length - 1); - // Also check end in the destination - Address toAddress = to.getAddress(); - toAddress.addNoWrap(length - 1); // Anticipate possible AddressOverflow - AddressRangeImpl range = new AddressRangeImpl(start, end); - if (truncateExisting) { - long truncEnd = DBTraceUtils.lowerEndpoint(from.getLifespan()) - 1; - for (TraceStaticMapping existing : List - .copyOf(manager.findAllOverlapping(range, from.getLifespan()))) { - existing.delete(); - if (Long.compareUnsigned(existing.getStartSnap(), truncEnd) < 0) { - manager.add(existing.getTraceAddressRange(), - Range.closed(existing.getStartSnap(), truncEnd), - existing.getStaticProgramURL(), existing.getStaticAddress()); - } + Address fromAddress = from.getAddress(); + Address toAddress = to.getByteAddress(); + long maxFromLengthMinus1 = + fromAddress.getAddressSpace().getMaxAddress().subtract(fromAddress); + long maxToLengthMinus1 = + toAddress.getAddressSpace().getMaxAddress().subtract(toAddress); + if (Long.compareUnsigned(length - 1, maxFromLengthMinus1) > 0) { + throw new IllegalArgumentException("Length would cause address overflow in trace"); + } + if (Long.compareUnsigned(length - 1, maxToLengthMinus1) > 0) { + throw new IllegalArgumentException("Length would cause address overflow in program"); + } + Address end = fromAddress.addWrap(length - 1); + // Also check end in the destination + AddressRangeImpl range = new AddressRangeImpl(fromAddress, end); + Range fromLifespan = from.getLifespan(); + if (truncateExisting) { + long truncEnd = DBTraceUtils.lowerEndpoint(fromLifespan) - 1; + for (TraceStaticMapping existing : List + .copyOf(manager.findAllOverlapping(range, fromLifespan))) { + existing.delete(); + if (fromLifespan.hasLowerBound() && + Long.compare(existing.getStartSnap(), truncEnd) <= 0) { + manager.add(existing.getTraceAddressRange(), + Range.closed(existing.getStartSnap(), truncEnd), + existing.getStaticProgramURL(), existing.getStaticAddress()); } } - manager.add(range, from.getLifespan(), toURL, - toAddress.toString(true)); - } - catch (AddressOverflowException e) { - throw new IllegalArgumentException("Length would cause address overflow", e); } + manager.add(range, fromLifespan, toURL, toAddress.toString(true)); } - /** - * Add a static mapping (relocation) from the given module to the given program - * - *

- * This is simply a shortcut and does not mean to imply that all mappings must represent module - * relocations. The lifespan is that of the module's. - * - * @param from the source module - * @param length the "size" of the module -- {@code max-min+1} as loaded/mapped in memory - * @param toProgram the destination program - * @see #addMapping(TraceLocation, ProgramLocation, long, boolean) - */ - public static void addModuleMapping(TraceModule from, long length, Program toProgram, - boolean truncateExisting) throws TraceConflictedMappingException { - TraceLocation fromLoc = - new DefaultTraceLocation(from.getTrace(), null, from.getLifespan(), from.getBase()); - ProgramLocation toLoc = new ProgramLocation(toProgram, toProgram.getImageBase()); + public static void addMapping(MapEntry entry, boolean truncateExisting) + throws TraceConflictedMappingException { + TraceLocation fromLoc = entry.getFromTraceLocation(); + ProgramLocation toLoc = entry.getToProgramLocation(); + long length = entry.getMappingLength(); addMapping(fromLoc, toLoc, length, truncateExisting); } - /** - * Add a static mapping (relocation) from the given section to the given program memory block - * - *

- * This is simply a shortcut and does not mean to imply that all mappings must represent section - * relocations. In most cases the lengths of the from and to objects match exactly, but this may - * not be the case. Whatever the case, the minimum length is computed, and the start addresses - * are used as the location. The lifespan is that of the section's containing module. - * - * @param from the source section - * @param toProgram the destination program - * @param to the destination memory block - * @see #addMapping(TraceLocation, ProgramLocation, long, boolean) - */ - public static void addSectionMapping(TraceSection from, Program toProgram, MemoryBlock to, - boolean truncateExisting) throws TraceConflictedMappingException { - TraceLocation fromLoc = new DefaultTraceLocation(from.getTrace(), null, - from.getModule().getLifespan(), from.getStart()); - ProgramLocation toLoc = new ProgramLocation(toProgram, to.getStart()); - long length = Math.min(from.getRange().getLength(), to.getSize()); - addMapping(fromLoc, toLoc, length, truncateExisting); + public static void addIdentityMapping(Trace from, Program toProgram, Range lifespan, + boolean truncateExisting) { + Map mins = new HashMap<>(); + Map maxs = new HashMap<>(); + for (AddressRange range : toProgram.getMemory().getAddressRanges()) { + mins.compute(range.getAddressSpace().getName(), (n, min) -> { + Address can = range.getMinAddress(); + if (min == null || can.compareTo(min) < 0) { + return can; + } + return min; + }); + maxs.compute(range.getAddressSpace().getName(), (n, max) -> { + Address can = range.getMaxAddress(); + if (max == null || can.compareTo(max) > 0) { + return can; + } + return max; + }); + } + for (String name : mins.keySet()) { + AddressRange range = clippedRange(from, name, mins.get(name).getOffset(), + maxs.get(name).getOffset()); + if (range == null) { + continue; + } + try { + addMapping(new DefaultTraceLocation(from, null, lifespan, range.getMinAddress()), + new ProgramLocation(toProgram, mins.get(name)), range.getLength(), + truncateExisting); + } + catch (TraceConflictedMappingException e) { + Msg.error(DebuggerStaticMappingUtils.class, + "Could not add identity mapping " + range + ": " + e.getMessage()); + } + } + } + + protected static AddressRange clippedRange(Trace trace, String spaceName, long min, + long max) { + AddressSpace space = trace.getBaseAddressFactory().getAddressSpace(spaceName); + if (space == null) { + return null; + } + Address spaceMax = space.getMaxAddress(); + if (Long.compareUnsigned(min, spaceMax.getOffset()) > 0) { + return null; + } + if (Long.compareUnsigned(max, spaceMax.getOffset()) > 0) { + return new AddressRangeImpl(space.getAddress(min), spaceMax); + } + return new AddressRangeImpl(space.getAddress(min), space.getAddress(max)); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DefaultModuleMapProposal.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DefaultModuleMapProposal.java new file mode 100644 index 0000000000..b814a19995 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DefaultModuleMapProposal.java @@ -0,0 +1,233 @@ +/* ### + * 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.util.*; + +import com.google.common.collect.Range; + +import ghidra.app.services.DebuggerStaticMappingService; +import ghidra.app.services.ModuleMapProposal; +import ghidra.app.services.ModuleMapProposal.ModuleMapEntry; +import ghidra.program.model.address.*; +import ghidra.program.model.listing.Program; +import ghidra.program.model.mem.MemoryBlock; +import ghidra.trace.model.memory.TraceMemoryRegion; +import ghidra.trace.model.modules.TraceModule; + +public class DefaultModuleMapProposal + extends AbstractMapProposal + implements ModuleMapProposal { + + /** + * A module-program entry in a proposed module map + */ + public static class DefaultModuleMapEntry extends AbstractMapEntry + implements ModuleMapEntry { + + /** + * Check if a block should be included in size computations or analyzed for proposals + * + * @param program the program containing the block + * @param block the block + * @return true if included, false otherwise + */ + public static boolean includeBlock(Program program, MemoryBlock block) { + if (program.getImageBase().getAddressSpace() != block.getStart().getAddressSpace()) { + return false; + } + if (!block.isLoaded()) { + return false; + } + if (block.isMapped()) { + // TODO: Determine how to handle these. + return false; + } + if (MemoryBlock.EXTERNAL_BLOCK_NAME.equals(block.getName())) { + return false; + } + return true; + } + + /** + * Compute the "size" of an image + * + *

+ * This is considered the maximum loaded address as mapped in memory, minus the image base. + * + * @param program the program image whose size to compute + * @return the size + */ + public static long computeImageSize(Program program) { + Address imageBase = program.getImageBase(); + long imageSize = 0; + // TODO: How to handle Harvard architectures? + for (MemoryBlock block : program.getMemory().getBlocks()) { + if (!includeBlock(program, block)) { + continue; + } + imageSize = Math.max(imageSize, block.getEnd().subtract(imageBase) + 1); + } + return imageSize; + } + + protected AddressRange moduleRange; + + /** + * Construct a module map entry + * + *

+ * Generally, only the service implementation should construct an entry. See + * {@link DebuggerStaticMappingService#proposeModuleMap(TraceModule, Program)} and related + * to obtain these. + * + * @param module the module + * @param program the matched program + * @param moduleRange a range from the module base the size of the program's image + */ + protected DefaultModuleMapEntry(TraceModule module, Program program, + AddressRange moduleRange) { + super(module.getTrace(), module, program, program); + this.moduleRange = moduleRange; + } + + @Override + public TraceModule getModule() { + return getFromObject(); + } + + @Override + public Range getFromLifespan() { + return getModule().getLifespan(); + } + + @Override + public AddressRange getFromRange() { + return moduleRange; + } + + @Override + public AddressRange getModuleRange() { + return moduleRange; + } + + @Override + public void setProgram(Program program) { + setToObject(program, program); + try { + this.moduleRange = + new AddressRangeImpl(getModule().getBase(), computeImageSize(program)); + } + catch (AddressOverflowException e) { + // This is terribly unlikely + throw new IllegalArgumentException( + "Specified program is too large for module's memory space"); + } + } + + @Override + public AddressRange getToRange() { + try { + return new AddressRangeImpl(getToProgram().getImageBase(), moduleRange.getLength()); + } + catch (AddressOverflowException e) { + throw new AssertionError(e); + } + } + } + + protected final TraceModule module; + + // indexed by region's offset from module base + protected final NavigableMap matchers = new TreeMap<>(); + protected Address imageBase; + protected Address moduleBase; + protected long imageSize; + protected AddressRange moduleRange; // TODO: This is now in the trace schema. Use it. + + protected DefaultModuleMapProposal(TraceModule module, Program program) { + super(module.getTrace(), program); + this.module = module; + processProgram(); + processModule(); + } + + @Override + public TraceModule getModule() { + return module; + } + + private ModuleRegionMatcher getMatcher(long baseOffset) { + return matchers.computeIfAbsent(baseOffset, ModuleRegionMatcher::new); + } + + private void processProgram() { + imageBase = program.getImageBase(); + imageSize = DefaultModuleMapEntry.computeImageSize(program); + // TODO: How to handle Harvard architectures? + for (MemoryBlock block : program.getMemory().getBlocks()) { + if (!DefaultModuleMapEntry.includeBlock(program, block)) { + continue; + } + getMatcher(block.getStart().subtract(imageBase)).block = block; + } + } + + /** + * Must be called after processProgram, so that image size is known + */ + private void processModule() { + moduleBase = module.getBase(); + try { + moduleRange = new AddressRangeImpl(moduleBase, imageSize); + } + catch (AddressOverflowException e) { + return; // Just score it as having no matches? + } + for (TraceMemoryRegion region : module.getTrace() + .getMemoryManager() + .getRegionsIntersecting(module.getLifespan(), moduleRange)) { + getMatcher(region.getMinAddress().subtract(moduleBase)).region = region; + } + } + + /** + * {@inheritDoc} + * + * @implNote some information to consider: length and case of matched image and module names, + * alignment of program memory blocks to trace memory regions, etc. + */ + @Override + public double computeScore() { + return ((double) matchers.values() + .stream() + .reduce(0, (s, m) -> s + m.score(), Integer::sum)) / + matchers.size(); + } + + @Override + public Map computeMap() { + return Map.of(module, new DefaultModuleMapEntry(module, program, moduleRange)); + } + + @Override + public Program getToObject(TraceModule from) { + if (from != module) { + return null; + } + return program; + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DefaultRegionMapProposal.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DefaultRegionMapProposal.java new file mode 100644 index 0000000000..da7404714e --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DefaultRegionMapProposal.java @@ -0,0 +1,192 @@ +/* ### + * 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.util.*; +import java.util.stream.Collectors; + +import com.google.common.collect.Range; + +import ghidra.app.services.RegionMapProposal; +import ghidra.app.services.RegionMapProposal.RegionMapEntry; +import ghidra.program.model.address.*; +import ghidra.program.model.listing.Program; +import ghidra.program.model.mem.MemoryBlock; +import ghidra.trace.model.Trace; +import ghidra.trace.model.memory.TraceMemoryRegion; + +public class DefaultRegionMapProposal + extends AbstractMapProposal + implements RegionMapProposal { + + public static class DefaultRegionMapEntry + extends AbstractMapEntry + implements RegionMapEntry { + + public DefaultRegionMapEntry(TraceMemoryRegion region, + Program program, MemoryBlock block) { + super(region.getTrace(), region, program, block); + } + + @Override + public TraceMemoryRegion getRegion() { + return getFromObject(); + } + + @Override + public AddressRange getFromRange() { + return getRegion().getRange(); + } + + @Override + public Range getFromLifespan() { + return getRegion().getLifespan(); + } + + @Override + public MemoryBlock getBlock() { + return getToObject(); + } + + @Override + public AddressRange getToRange() { + return new AddressRangeImpl(getBlock().getStart(), getBlock().getEnd()); + } + + @Override + public void setBlock(Program program, MemoryBlock block) { + setToObject(program, block); + } + } + + protected class RegionMatcher extends Matcher { + public RegionMatcher(TraceMemoryRegion region, MemoryBlock block) { + super(region, block); + } + + @Override + protected AddressRange getFromRange() { + return fromObject == null ? null : fromObject.getRange(); + } + + @Override + protected AddressRange getToRange() { + return toObject == null ? null + : new AddressRangeImpl(toObject.getStart(), toObject.getEnd()); + } + + @Override + protected double computeScore() { + return computeLengthScore() + computeOffsetScore(); + } + + protected int computeOffsetScore() { + long fOff = fromRange.getMinAddress().subtract(fromBase); + long tOff = toRange.getMinAddress().subtract(toBase); + if (fOff == tOff) { + return 10; + } + return 0; + } + } + + protected class RegionMatcherMap + extends MatcherMap { + @Override + protected RegionMatcher newMatcher(TraceMemoryRegion region, MemoryBlock block) { + return new RegionMatcher(region, block); + } + + @Override + protected Void getFromJoinKey(TraceMemoryRegion region) { + return null; + } + + @Override + protected Void getToJoinKey(MemoryBlock block) { + return null; + } + } + + protected static Trace getTrace(Collection regions) { + if (regions == null || regions.isEmpty()) { + return null; + } + return regions.iterator().next().getTrace(); + } + + protected final List regions; + protected final Address fromBase; + protected final Address toBase; + protected final RegionMatcherMap matchers = new RegionMatcherMap(); + + protected DefaultRegionMapProposal(Collection regions, + Program program) { + super(getTrace(regions), program); + this.regions = Collections.unmodifiableList(regions.stream() + .sorted(Comparator.comparing(r -> r.getMinAddress())) + .collect(Collectors.toList())); + this.fromBase = computeFromBase(); + this.toBase = program.getImageBase(); + processRegions(); + processProgram(); + } + + protected DefaultRegionMapProposal(TraceMemoryRegion region, Program program, + MemoryBlock block) { + super(region.getTrace(), program); + this.regions = List.of(region); + this.fromBase = region.getMinAddress(); + this.toBase = program.getImageBase(); + processRegions(); + matchers.processToObject(block); + } + + protected Address computeFromBase() { + if (regions.isEmpty()) { + return null; + } + return regions.get(0).getMinAddress(); + } + + private void processRegions() { + for (TraceMemoryRegion region : regions) { + matchers.processFromObject(region); + } + } + + private void processProgram() { + for (MemoryBlock block : program.getMemory().getBlocks()) { + matchers.processToObject(block); + } + } + + @Override + public double computeScore() { + return matchers.averageScore(); + } + + @Override + public Map computeMap() { + return matchers + .computeMap(m -> new DefaultRegionMapEntry(m.fromObject, program, m.toObject)); + } + + @Override + public MemoryBlock getToObject(TraceMemoryRegion from) { + return matchers.getToObject(from); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DefaultSectionMapProposal.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DefaultSectionMapProposal.java new file mode 100644 index 0000000000..1c14f70454 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DefaultSectionMapProposal.java @@ -0,0 +1,178 @@ +/* ### + * 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.util.Map; + +import com.google.common.collect.Range; + +import ghidra.app.services.DebuggerStaticMappingService; +import ghidra.app.services.SectionMapProposal; +import ghidra.app.services.SectionMapProposal.SectionMapEntry; +import ghidra.program.model.address.AddressRange; +import ghidra.program.model.address.AddressRangeImpl; +import ghidra.program.model.listing.Program; +import ghidra.program.model.mem.MemoryBlock; +import ghidra.trace.model.modules.TraceModule; +import ghidra.trace.model.modules.TraceSection; + +public class DefaultSectionMapProposal + extends AbstractMapProposal + implements SectionMapProposal { + + /** + * A section-block entry in a proposed section map + */ + public static class DefaultSectionMapEntry extends AbstractMapEntry + implements SectionMapEntry { + + /** + * Construct a section map entry + * + *

+ * Generally, only the service implementation should construct an entry. See + * {@link DebuggerStaticMappingService#proposeSectionMap(TraceSection, Program, MemoryBlock)} + * and related to obtain these. + * + * @param section the section + * @param program the program containing the matched block + * @param block the matched memory block + */ + protected DefaultSectionMapEntry(TraceSection section, Program program, MemoryBlock block) { + super(section.getTrace(), section, program, block); + } + + @Override + public TraceModule getModule() { + return getFromObject().getModule(); + } + + @Override + public TraceSection getSection() { + return getFromObject(); + } + + @Override + public Range getFromLifespan() { + return getModule().getLifespan(); + } + + @Override + public AddressRange getFromRange() { + return getSection().getRange(); + } + + @Override + public MemoryBlock getBlock() { + return getToObject(); + } + + @Override + public AddressRange getToRange() { + return new AddressRangeImpl(getBlock().getStart(), getBlock().getEnd()); + } + + @Override + public void setBlock(Program program, MemoryBlock block) { + setToObject(program, block); + } + } + + protected static class SectionMatcher extends Matcher { + public SectionMatcher(TraceSection section, MemoryBlock block) { + super(section, block); + } + + @Override + protected AddressRange getFromRange() { + return fromObject == null ? null : fromObject.getRange(); + } + + @Override + protected AddressRange getToRange() { + return toObject == null ? null + : new AddressRangeImpl(toObject.getStart(), toObject.getEnd()); + } + } + + protected static class SectionMatcherMap + extends MatcherMap { + @Override + protected SectionMatcher newMatcher(TraceSection section, MemoryBlock block) { + return new SectionMatcher(section, block); + } + + @Override + protected String getFromJoinKey(TraceSection section) { + return section.getName(); + } + + @Override + protected String getToJoinKey(MemoryBlock block) { + return block.getName(); + } + } + + protected final TraceModule module; + protected final SectionMatcherMap matchers = new SectionMatcherMap(); + + protected DefaultSectionMapProposal(TraceModule module, Program program) { + super(module.getTrace(), program); + this.module = module; + processModule(); + processProgram(); + } + + protected DefaultSectionMapProposal(TraceSection section, Program program, MemoryBlock block) { + super(section.getTrace(), program); + this.module = section.getModule(); + matchers.processFromObject(section); + matchers.processToObject(block); + } + + @Override + public TraceModule getModule() { + return module; + } + + private void processModule() { + for (TraceSection section : module.getSections()) { + matchers.processFromObject(section); + } + } + + private void processProgram() { + for (MemoryBlock block : program.getMemory().getBlocks()) { + matchers.processToObject(block); + } + } + + @Override + public double computeScore() { + return matchers.averageScore(); + } + + @Override + public Map computeMap() { + return matchers + .computeMap(m -> new DefaultSectionMapEntry(m.fromObject, program, m.toObject)); + } + + @Override + public MemoryBlock getToObject(TraceSection from) { + return matchers.getToObject(from); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/MapModulesBackgroundCommand.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/MapModulesBackgroundCommand.java index 1797b08324..24100cad36 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/MapModulesBackgroundCommand.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/MapModulesBackgroundCommand.java @@ -18,7 +18,7 @@ package ghidra.app.plugin.core.debug.service.modules; import java.util.Collection; import ghidra.app.services.DebuggerStaticMappingService; -import ghidra.app.services.DebuggerStaticMappingService.ModuleMapEntry; +import ghidra.app.services.ModuleMapProposal.ModuleMapEntry; import ghidra.framework.cmd.BackgroundCommand; import ghidra.framework.model.DomainObject; import ghidra.util.exception.CancelledException; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/MapRegionsBackgroundCommand.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/MapRegionsBackgroundCommand.java new file mode 100644 index 0000000000..c7a32177dd --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/MapRegionsBackgroundCommand.java @@ -0,0 +1,48 @@ +/* ### + * 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.util.Collection; + +import ghidra.app.services.DebuggerStaticMappingService; +import ghidra.app.services.RegionMapProposal.RegionMapEntry; +import ghidra.framework.cmd.BackgroundCommand; +import ghidra.framework.model.DomainObject; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +public class MapRegionsBackgroundCommand extends BackgroundCommand { + private final DebuggerStaticMappingService service; + private final Collection entries; + + public MapRegionsBackgroundCommand(DebuggerStaticMappingService service, + Collection entries) { + super("Map regions", true, true, true); + this.service = service; + this.entries = entries; + } + + @Override + public boolean applyTo(DomainObject obj, TaskMonitor monitor) { + try { + service.addRegionMappings(entries, monitor, true); + } + catch (CancelledException e) { + return false; + } + return true; + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/MapSectionsBackgroundCommand.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/MapSectionsBackgroundCommand.java index 90eb6ec3f0..b502b9007a 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/MapSectionsBackgroundCommand.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/MapSectionsBackgroundCommand.java @@ -18,7 +18,7 @@ package ghidra.app.plugin.core.debug.service.modules; import java.util.Collection; import ghidra.app.services.DebuggerStaticMappingService; -import ghidra.app.services.DebuggerStaticMappingService.SectionMapEntry; +import ghidra.app.services.SectionMapProposal.SectionMapEntry; import ghidra.framework.cmd.BackgroundCommand; import ghidra.framework.model.DomainObject; import ghidra.util.exception.CancelledException; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/ModuleRegionMatcher.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/ModuleRegionMatcher.java new file mode 100644 index 0000000000..61161d2ee8 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/ModuleRegionMatcher.java @@ -0,0 +1,38 @@ +/* ### + * 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.program.model.mem.MemoryBlock; +import ghidra.trace.model.memory.TraceMemoryRegion; + +class ModuleRegionMatcher { + MemoryBlock block; + TraceMemoryRegion region; + + public ModuleRegionMatcher(long baseOffset) { + } + + int score() { + if (block == null || region == null) { + return 0; // Unmatched + } + int score = 3; // For the matching offset + if (block.getSize() == region.getLength()) { + score += 10; + } + return score; + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/workflow/AbstractMapDebuggerBot.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/workflow/AbstractMapDebuggerBot.java new file mode 100644 index 0000000000..dcaf594d96 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/workflow/AbstractMapDebuggerBot.java @@ -0,0 +1,180 @@ +/* ### + * 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.workflow; + +import java.util.*; + +import org.apache.commons.lang3.tuple.Pair; + +import ghidra.app.plugin.core.debug.service.workflow.*; +import ghidra.app.services.*; +import ghidra.async.AsyncDebouncer; +import ghidra.async.AsyncTimer; +import ghidra.framework.cmd.BackgroundCommand; +import ghidra.framework.model.DomainObject; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.listing.Program; +import ghidra.trace.model.Trace; +import ghidra.trace.util.TraceChangeType; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +public abstract class AbstractMapDebuggerBot implements DebuggerBot { + protected class ForChangesTraceListener extends AbstractMultiToolTraceListener { + public ForChangesTraceListener(Trace trace) { + super(trace); + + for (TraceChangeType type : getChangeTypes()) { + listenFor(type, this::changed); + } + } + + private void changed() { + queueTrace(trace); + } + } + + protected abstract Collection> getChangeTypes(); + + private DebuggerWorkflowServicePlugin plugin; + + private final MultiToolTraceListenerManager listeners = + new MultiToolTraceListenerManager<>(ForChangesTraceListener::new); + + private final Set traceQueue = new HashSet<>(); + // Debounce to ensure we don't get too eager if manager is still opening stuff + private final AsyncDebouncer debouncer = + new AsyncDebouncer<>(AsyncTimer.DEFAULT_TIMER, 500); + { + debouncer.addListener(this::queueSettled); + } + + @Override + public void enable(DebuggerWorkflowServicePlugin wp) { + this.plugin = wp; + + listeners.enable(wp); + for (PluginTool t : plugin.getProxyingPluginTools()) { + DebuggerTraceManagerService traceManager = + t.getService(DebuggerTraceManagerService.class); + if (traceManager == null) { + continue; + } + queueTraces(traceManager.getOpenTraces()); + } + } + + @Override + public void disable() { + plugin = null; + + listeners.disable(); + } + + @Override + public boolean isEnabled() { + return plugin != null; + } + + @Override + public void traceOpened(PluginTool tool, Trace trace) { + listeners.traceOpened(tool, trace); + queueTrace(trace); + } + + @Override + public void traceClosed(PluginTool tool, Trace trace) { + listeners.traceClosed(tool, trace); + } + + @Override + public void programOpened(PluginTool t, Program program) { + DebuggerTraceManagerService traceManager = t.getService(DebuggerTraceManagerService.class); + if (traceManager == null) { + return; + } + queueTraces(traceManager.getOpenTraces()); + } + + private void queueTrace(Trace trace) { + synchronized (traceQueue) { + traceQueue.add(trace); + } + debouncer.contact(null); + } + + private void queueTraces(Collection traces) { + synchronized (traceQueue) { + traceQueue.addAll(traces); + } + debouncer.contact(null); + } + + private void queueSettled(Void __) { + Set traces; + synchronized (traceQueue) { + traces = Set.copyOf(traceQueue); + traceQueue.clear(); + } + + Map>> toAnalyze = new HashMap<>(); + for (Trace trace : traces) { + for (PluginTool tool : plugin.getProxyingPluginTools()) { + DebuggerTraceManagerService traceManager = + tool.getService(DebuggerTraceManagerService.class); + if (traceManager == null) { + continue; + } + ProgramManager programManager = tool.getService(ProgramManager.class); + if (programManager == null) { + continue; + } + if (!traceManager.getOpenTraces().contains(trace)) { + continue; + } + Pair> programs = + toAnalyze.computeIfAbsent(trace, t -> Pair.of(tool, new HashSet<>())); + programs.getRight().addAll(List.of(programManager.getAllOpenPrograms())); + } + } + + for (Map.Entry>> ent : toAnalyze.entrySet()) { + PluginTool tool = ent.getValue().getLeft(); + Trace trace = ent.getKey(); + Set programs = ent.getValue().getRight(); + analyzeTrace(tool, trace, programs); + } + } + + private void analyzeTrace(PluginTool tool, Trace trace, Set programs) { + BackgroundCommand cmd = new BackgroundCommand(getDescription(), true, true, false) { + @Override + public boolean applyTo(DomainObject obj, TaskMonitor monitor) { + try { + doAnalysis(tool, trace, programs, monitor); + return true; + } + catch (CancelledException e) { + return false; + } + } + }; + tool.executeBackgroundCommand(cmd, trace); + } + + protected abstract void doAnalysis(PluginTool tool, Trace trace, Set programs, + TaskMonitor monitor) throws CancelledException; +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/workflow/MapModulesDebuggerBot.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/workflow/MapModulesDebuggerBot.java index 1b8e13f0af..324d75a18a 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/workflow/MapModulesDebuggerBot.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/workflow/MapModulesDebuggerBot.java @@ -17,201 +17,43 @@ package ghidra.app.plugin.core.debug.workflow; import java.util.*; -import org.apache.commons.lang3.tuple.Pair; - -import ghidra.app.plugin.core.debug.service.workflow.*; import ghidra.app.services.*; -import ghidra.app.services.DebuggerStaticMappingService.ModuleMapEntry; -import ghidra.app.services.DebuggerStaticMappingService.ModuleMapProposal; -import ghidra.async.AsyncDebouncer; -import ghidra.async.AsyncTimer; -import ghidra.framework.cmd.BackgroundCommand; -import ghidra.framework.model.DomainObject; +import ghidra.app.services.ModuleMapProposal.ModuleMapEntry; import ghidra.framework.options.annotation.HelpInfo; import ghidra.framework.plugintool.PluginTool; import ghidra.program.model.listing.Program; import ghidra.trace.model.Trace; import ghidra.trace.model.Trace.TraceMemoryRegionChangeType; import ghidra.trace.model.Trace.TraceModuleChangeType; -import ghidra.trace.model.memory.TraceMemoryRegion; -import ghidra.trace.model.modules.TraceModule; +import ghidra.trace.util.TraceChangeType; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; @DebuggerBotInfo( // - description = "Map modules to open programs", // - details = "Monitors open traces and programs, attempting to map modules by \"best\" match.", // - help = @HelpInfo(anchor = "map_modules"), // - enabledByDefault = true // + description = "Map modules to open programs", // + details = "Monitors open traces and programs, attempting to map modules by \"best\" match.", // + help = @HelpInfo(anchor = "map_modules"), // + enabledByDefault = true // ) -public class MapModulesDebuggerBot implements DebuggerBot { - protected class ForMapNewModulesTraceListener extends AbstractMultiToolTraceListener { +public class MapModulesDebuggerBot extends AbstractMapDebuggerBot { - public ForMapNewModulesTraceListener(Trace trace) { - super(trace); - - /** - * NB. Not reacting to LIFESPAN_CHANGED. Once something else is added or changed, we can - * knock collisions out of the way. - */ - listenFor(TraceModuleChangeType.ADDED, this::moduleAdded); - listenFor(TraceModuleChangeType.CHANGED, this::moduleChanged); - - listenFor(TraceMemoryRegionChangeType.ADDED, this::regionAdded); - listenFor(TraceMemoryRegionChangeType.CHANGED, this::regionChanged); - } - - private void moduleAdded(TraceModule module) { - queueTrace(trace); - } - - private void moduleChanged(TraceModule module) { - queueTrace(trace); - } - - private void regionAdded(TraceMemoryRegion region) { - queueTrace(trace); - } - - private void regionChanged(TraceMemoryRegion region) { - queueTrace(trace); - } - } - - private DebuggerWorkflowServicePlugin plugin; - - private final MultiToolTraceListenerManager listeners = - new MultiToolTraceListenerManager<>(ForMapNewModulesTraceListener::new); - - private final Set traceQueue = new HashSet<>(); - // Debounce to ensure we don't get too eager if manager is still opening stuff - private final AsyncDebouncer debouncer = - new AsyncDebouncer<>(AsyncTimer.DEFAULT_TIMER, 500); - { - debouncer.addListener(this::queueSettled); + @Override + protected Collection> getChangeTypes() { + return List.of(TraceModuleChangeType.ADDED, TraceModuleChangeType.CHANGED, + TraceMemoryRegionChangeType.ADDED, TraceMemoryRegionChangeType.CHANGED); } @Override - public void enable(DebuggerWorkflowServicePlugin wp) { - this.plugin = wp; - - listeners.enable(wp); - for (PluginTool t : plugin.getProxyingPluginTools()) { - DebuggerTraceManagerService traceManager = - t.getService(DebuggerTraceManagerService.class); - if (traceManager == null) { - continue; - } - queueTraces(traceManager.getOpenTraces()); + protected void doAnalysis(PluginTool tool, Trace trace, Set programs, + TaskMonitor monitor) throws CancelledException { + DebuggerStaticMappingService mappingService = + tool.getService(DebuggerStaticMappingService.class); + if (mappingService != null) { + Map maps = mappingService + .proposeModuleMaps(trace.getModuleManager().getAllModules(), programs); + Collection entries = MapProposal.flatten(maps.values()); + entries = MapProposal.removeOverlapping(entries); + mappingService.addModuleMappings(entries, monitor, false); } } - - @Override - public void disable() { - plugin = null; - - listeners.disable(); - } - - @Override - public boolean isEnabled() { - return plugin != null; - } - - @Override - public void traceOpened(PluginTool tool, Trace trace) { - listeners.traceOpened(tool, trace); - queueTrace(trace); - } - - @Override - public void traceClosed(PluginTool tool, Trace trace) { - listeners.traceClosed(tool, trace); - } - - @Override - public void programOpened(PluginTool t, Program program) { - DebuggerTraceManagerService traceManager = t.getService(DebuggerTraceManagerService.class); - if (traceManager == null) { - return; - } - queueTraces(traceManager.getOpenTraces()); - } - - private void queueTrace(Trace trace) { - synchronized (traceQueue) { - traceQueue.add(trace); - } - debouncer.contact(null); - } - - private void queueTraces(Collection traces) { - synchronized (traceQueue) { - traceQueue.addAll(traces); - } - debouncer.contact(null); - } - - private void queueSettled(Void __) { - Set traces; - synchronized (traceQueue) { - traces = Set.copyOf(traceQueue); - traceQueue.clear(); - } - - Map>> toAnalyze = new HashMap<>(); - for (Trace trace : traces) { - for (PluginTool tool : plugin.getProxyingPluginTools()) { - DebuggerTraceManagerService traceManager = - tool.getService(DebuggerTraceManagerService.class); - if (traceManager == null) { - continue; - } - ProgramManager programManager = tool.getService(ProgramManager.class); - if (programManager == null) { - continue; - } - if (!traceManager.getOpenTraces().contains(trace)) { - continue; - } - Pair> programs = - toAnalyze.computeIfAbsent(trace, t -> Pair.of(tool, new HashSet<>())); - programs.getRight().addAll(List.of(programManager.getAllOpenPrograms())); - } - } - - for (Map.Entry>> ent : toAnalyze.entrySet()) { - PluginTool tool = ent.getValue().getLeft(); - Trace trace = ent.getKey(); - Set programs = ent.getValue().getRight(); - analyzeTrace(tool, trace, programs); - } - } - - private void analyzeTrace(PluginTool t, Trace trace, Set programs) { - BackgroundCommand cmd = new BackgroundCommand("Auto-map modules", true, true, false) { - @Override - public boolean applyTo(DomainObject obj, TaskMonitor monitor) { - try { - DebuggerStaticMappingService mappingService = - t.getService(DebuggerStaticMappingService.class); - if (mappingService != null) { - Map maps = - mappingService.proposeModuleMaps( - trace.getModuleManager().getAllModules(), - programs); - Collection entries = - ModuleMapProposal.flatten(maps.values()); - entries = ModuleMapProposal.removeOverlapping(entries); - mappingService.addModuleMappings(entries, monitor, false); - } - return true; - } - catch (CancelledException e) { - return false; - } - } - }; - t.executeBackgroundCommand(cmd, trace); - } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/workflow/MapRegionsDebuggerBot.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/workflow/MapRegionsDebuggerBot.java new file mode 100644 index 0000000000..14a372c4e1 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/workflow/MapRegionsDebuggerBot.java @@ -0,0 +1,57 @@ +/* ### + * 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.workflow; + +import java.util.*; + +import ghidra.app.services.*; +import ghidra.app.services.RegionMapProposal.RegionMapEntry; +import ghidra.framework.options.annotation.HelpInfo; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.listing.Program; +import ghidra.trace.model.Trace; +import ghidra.trace.model.Trace.TraceMemoryRegionChangeType; +import ghidra.trace.util.TraceChangeType; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +@DebuggerBotInfo( // + description = "Map regions to open programs", // + details = "Monitors open traces and programs, attempting to map regions by \"best\" match.", // + help = @HelpInfo(anchor = "map_regions"), // + enabledByDefault = false // +) +public class MapRegionsDebuggerBot extends AbstractMapDebuggerBot { + + @Override + protected Collection> getChangeTypes() { + return List.of(TraceMemoryRegionChangeType.ADDED); + } + + @Override + protected void doAnalysis(PluginTool tool, Trace trace, Set programs, + TaskMonitor monitor) throws CancelledException { + DebuggerStaticMappingService mappingService = + tool.getService(DebuggerStaticMappingService.class); + if (mappingService != null) { + Map maps = mappingService + .proposeRegionMaps(trace.getMemoryManager().getAllRegions(), programs); + Collection entries = MapProposal.flatten(maps.values()); + entries = MapProposal.removeOverlapping(entries); + mappingService.addRegionMappings(entries, monitor, false); + } + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/workflow/MapSectionsDebuggerBot.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/workflow/MapSectionsDebuggerBot.java index 2f7e068e9a..c7a893a08d 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/workflow/MapSectionsDebuggerBot.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/workflow/MapSectionsDebuggerBot.java @@ -17,177 +17,41 @@ package ghidra.app.plugin.core.debug.workflow; import java.util.*; -import org.apache.commons.lang3.tuple.Pair; - -import ghidra.app.plugin.core.debug.service.workflow.*; import ghidra.app.services.*; -import ghidra.app.services.DebuggerStaticMappingService.SectionMapEntry; -import ghidra.app.services.DebuggerStaticMappingService.SectionMapProposal; -import ghidra.async.AsyncDebouncer; -import ghidra.async.AsyncTimer; -import ghidra.framework.cmd.BackgroundCommand; -import ghidra.framework.model.DomainObject; +import ghidra.app.services.SectionMapProposal.SectionMapEntry; import ghidra.framework.options.annotation.HelpInfo; import ghidra.framework.plugintool.PluginTool; import ghidra.program.model.listing.Program; import ghidra.trace.model.Trace; import ghidra.trace.model.Trace.TraceSectionChangeType; -import ghidra.trace.model.modules.TraceModule; -import ghidra.trace.model.modules.TraceSection; -import ghidra.trace.util.TraceAddressSpace; +import ghidra.trace.util.TraceChangeType; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; @DebuggerBotInfo( // - description = "Map sections to open programs", // - details = "Monitors open traces and programs, attempting to map sections by \"best\" match.", // - help = @HelpInfo(anchor = "map_sections"), // - enabledByDefault = false // + description = "Map sections to open programs", // + details = "Monitors open traces and programs, attempting to map sections by \"best\" match.", // + help = @HelpInfo(anchor = "map_sections"), // + enabledByDefault = false // ) -public class MapSectionsDebuggerBot implements DebuggerBot { +public class MapSectionsDebuggerBot extends AbstractMapDebuggerBot { - protected class ForMapNewSectionsTraceListener extends AbstractMultiToolTraceListener { - public ForMapNewSectionsTraceListener(Trace trace) { - super(trace); - - listenFor(TraceSectionChangeType.ADDED, this::sectionAdded); - } - - private void sectionAdded(TraceAddressSpace space, TraceSection section) { - queueTrace(trace); - } - } - - private DebuggerWorkflowServicePlugin plugin; - - private final MultiToolTraceListenerManager listeners = - new MultiToolTraceListenerManager<>(ForMapNewSectionsTraceListener::new); - - private final Set traceQueue = new HashSet<>(); - // Debounce to ensure we don't get too eager if manager is still opening stuff - private final AsyncDebouncer debouncer = - new AsyncDebouncer<>(AsyncTimer.DEFAULT_TIMER, 500); - { - debouncer.addListener(this::queueSettled); + @Override + protected Collection> getChangeTypes() { + return List.of(TraceSectionChangeType.ADDED); } @Override - public void enable(DebuggerWorkflowServicePlugin wp) { - this.plugin = wp; - - listeners.enable(wp); - for (PluginTool t : plugin.getProxyingPluginTools()) { - DebuggerTraceManagerService traceManager = - t.getService(DebuggerTraceManagerService.class); - if (traceManager == null) { - continue; - } - queueTraces(traceManager.getOpenTraces()); + protected void doAnalysis(PluginTool tool, Trace trace, Set programs, + TaskMonitor monitor) throws CancelledException { + DebuggerStaticMappingService mappingService = + tool.getService(DebuggerStaticMappingService.class); + if (mappingService != null) { + Map maps = mappingService + .proposeSectionMaps(trace.getModuleManager().getAllModules(), programs); + Collection entries = MapProposal.flatten(maps.values()); + entries = MapProposal.removeOverlapping(entries); + mappingService.addSectionMappings(entries, monitor, false); } } - - @Override - public void disable() { - plugin = null; - - listeners.disable(); - } - - @Override - public boolean isEnabled() { - return plugin != null; - } - - @Override - public void traceOpened(PluginTool tool, Trace trace) { - listeners.traceOpened(tool, trace); - queueTrace(trace); - } - - @Override - public void traceClosed(PluginTool tool, Trace trace) { - listeners.traceClosed(tool, trace); - } - - @Override - public void programOpened(PluginTool t, Program program) { - DebuggerTraceManagerService traceManager = t.getService(DebuggerTraceManagerService.class); - if (traceManager == null) { - return; - } - queueTraces(traceManager.getOpenTraces()); - } - - private void queueTrace(Trace trace) { - synchronized (traceQueue) { - traceQueue.add(trace); - } - debouncer.contact(null); - } - - private void queueTraces(Collection traces) { - synchronized (traceQueue) { - traceQueue.addAll(traces); - } - debouncer.contact(null); - } - - private void queueSettled(Void __) { - Set traces; - synchronized (traceQueue) { - traces = Set.copyOf(traceQueue); - traceQueue.clear(); - } - - Map>> toAnalyze = new HashMap<>(); - for (Trace trace : traces) { - for (PluginTool tool : plugin.getProxyingPluginTools()) { - DebuggerTraceManagerService traceManager = - tool.getService(DebuggerTraceManagerService.class); - if (traceManager == null) { - continue; - } - ProgramManager programManager = tool.getService(ProgramManager.class); - if (programManager == null) { - continue; - } - if (!traceManager.getOpenTraces().contains(trace)) { - continue; - } - Pair> programs = - toAnalyze.computeIfAbsent(trace, t -> Pair.of(tool, new HashSet<>())); - programs.getRight().addAll(List.of(programManager.getAllOpenPrograms())); - } - } - - for (Map.Entry>> ent : toAnalyze.entrySet()) { - PluginTool tool = ent.getValue().getLeft(); - Trace trace = ent.getKey(); - Set programs = ent.getValue().getRight(); - analyzeTrace(tool, trace, programs); - } - } - - private void analyzeTrace(PluginTool t, Trace trace, Set programs) { - BackgroundCommand cmd = new BackgroundCommand("Auto-map sections", true, true, false) { - @Override - public boolean applyTo(DomainObject obj, TaskMonitor monitor) { - try { - DebuggerStaticMappingService mappingService = - t.getService(DebuggerStaticMappingService.class); - Map maps = - mappingService.proposeSectionMaps(trace.getModuleManager().getAllModules(), - programs); - Collection entries = SectionMapProposal.flatten(maps.values()); - entries = SectionMapProposal.removeOverlapping(entries); - mappingService.addSectionMappings(entries, monitor, false); - return true; - } - catch (CancelledException e) { - return false; - } - } - }; - t.executeBackgroundCommand(cmd, trace); - } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerStaticMappingService.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerStaticMappingService.java index 264b55447c..f07590bbd3 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerStaticMappingService.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerStaticMappingService.java @@ -16,19 +16,21 @@ package ghidra.app.services; import java.util.*; -import java.util.stream.Collectors; import com.google.common.collect.Range; +import ghidra.app.services.ModuleMapProposal.ModuleMapEntry; +import ghidra.app.services.RegionMapProposal.RegionMapEntry; +import ghidra.app.services.SectionMapProposal.SectionMapEntry; import ghidra.framework.model.DomainFile; -import ghidra.program.model.address.*; +import ghidra.program.model.address.AddressSetView; import ghidra.program.model.listing.Program; import ghidra.program.model.mem.MemoryBlock; import ghidra.program.util.ProgramLocation; import ghidra.trace.model.*; +import ghidra.trace.model.memory.TraceMemoryRegion; import ghidra.trace.model.modules.*; import ghidra.trace.model.program.TraceProgramView; -import ghidra.util.MathUtilities; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; @@ -49,445 +51,7 @@ import ghidra.util.task.TaskMonitor; public interface DebuggerStaticMappingService { /** - * A proposed mapping of module to program - */ - public interface ModuleMapProposal { - - /** - * Flatten proposals into a single collection of entries - * - *

- * The output is suitable for use in - * {@link DebuggerStaticMappingService#addModuleMappings(Collection, TaskMonitor, boolean)}. - * In some contexts, the user should be permitted to see and optionally adjust the - * collection first. - * - *

- * Note, a suitable parameter to this method is derived by invoking {@link Map#values()} on - * the result of - * {@link DebuggerStaticMappingService#proposeModuleMaps(Collection, Collection)}. - * - *

- * Note, it is advisable to filter the returned collection using - * {@link DebuggerStaticMappingService#removeOverlappingModuleEntries(Collection)} to avoid - * errors from adding overlapped mappings. Alternatively, you can set - * {@code truncateExisting} to true when calling - * {@link DebuggerStaticMappingService#addModuleMappings(Collection, TaskMonitor, boolean)}. - * - * @param proposals the collection of proposed maps - * @return the flattened, filtered collection - */ - static Collection flatten(Collection proposals) { - Collection result = new LinkedHashSet<>(); - for (ModuleMapProposal map : proposals) { - result.addAll(map.computeMap().values()); - } - return result; - } - - /** - * Remove entries from a collection which overlap existing entries in the trace - * - * @param entries the entries to filter - * @return the filtered entries - */ - public static Set removeOverlapping(Collection entries) { - return entries.stream().filter(e -> { - TraceStaticMappingManager manager = e.module.getTrace().getStaticMappingManager(); - return manager.findAllOverlapping(e.moduleRange, e.module.getLifespan()).isEmpty(); - }).collect(Collectors.toSet()); - } - - /** - * Get the trace module of this proposal - * - * @return the module - */ - TraceModule getModule(); - - /** - * Get the corresponding program image of this proposal - * - * @return the program - */ - Program getProgram(); - - /** - * Compute a notional "score" of the proposal - * - *

- * This may examine the module and program names, but must consider the likelihood of the - * match based on this proposal. The implementation need not assign meaning to any - * particular score, but a higher score must imply a more likely match. - * - * @implNote some information to consider: length and case of matched image and module - * names, alignment of program memory blocks to trace memory regions, etc. - * - * @return a score of the proposed pair - */ - double computeScore(); - - /** - * Compute the overall module map given by this proposal - * - * @return the map - */ - Map computeMap(); - } - - /** - * A proposed map of sections to program memory blocks - */ - public interface SectionMapProposal { - - /** - * Flatten proposals into a single collection of entries - * - *

- * The output is suitable for use in - * {@link DebuggerStaticMappingService#addSectionMappings(Collection, TaskMonitor, boolean)}. - * In some contexts, the user should be permitted to see and optionally adjust the - * collection first. - * - *

- * Note, a suitable parameter to this method is derived by invoking {@link Map#values()} on - * the result of - * {@link DebuggerStaticMappingService#proposeSectionMaps(Collection, Collection)}. - * - *

- * Note, it is advisable to filter the returned collection using - * {@link DebuggerStaticMappingService#removeOverlappingSectionEntries(Collection)} to avoid - * errors from adding overlapped mappings. Alternatively, you can set - * {@code truncateExisting} to true when calling - * {@link DebuggerStaticMappingService#addSectionMappings(Collection, TaskMonitor, boolean)}. - * - * @param proposals the collection of proposed maps - * @return the flattened, filtered collection - */ - static Collection flatten(Collection proposals) { - Collection result = new LinkedHashSet<>(); - for (SectionMapProposal map : proposals) { - result.addAll(map.computeMap().values()); - } - return result; - } - - /** - * Remove entries from a collection which overlap existing entries in the trace - * - * @param entries the entries to filter - * @return the filtered entries - */ - public static Set removeOverlapping(Collection entries) { - return entries.stream().filter(e -> { - TraceStaticMappingManager manager = e.section.getTrace().getStaticMappingManager(); - Range moduleLifespan = e.section.getModule().getLifespan(); - return manager.findAllOverlapping(e.section.getRange(), moduleLifespan).isEmpty(); - }).collect(Collectors.toSet()); - } - - /** - * Get the trace module of this proposal - * - * @return the module - */ - TraceModule getModule(); - - /** - * Get the corresponding program image of this proposal - * - * @return the program - */ - Program getProgram(); - - /** - * Compute a notional "score" of the proposal - * - *

- * This may examine the module and program names, but must consider the likelihood of the - * match based on this proposal. The implementation need not assign meaning to any - * particular score, but a higher score must imply a more likely match. - * - * @implNote some attributes of sections and blocks to consider: matched names vs. total - * names, sizes, addresses (last n hexidecimal digits, to account for relocation), - * consistency of relocation offset, etc. - * - * @return a score of the proposed pair - */ - double computeScore(); - - /** - * Get the program block proposed for a given trace section - * - * @param section the trace section - * @return the proposed program block - */ - MemoryBlock getDestination(TraceSection section); - - /** - * Compute the overall section map given by this proposal - * - * @return the map - */ - Map computeMap(); - } - - /** - * A module-program entry in a proposed module map - */ - public static class ModuleMapEntry { - /** - * Check if a block should be included in size computations or analyzed for proposals - * - * @param program the program containing the block - * @param block the block - * @return true if included, false otherwise - */ - public static boolean includeBlock(Program program, MemoryBlock block) { - if (program.getImageBase().getAddressSpace() != block.getStart().getAddressSpace()) { - return false; - } - if (!block.isLoaded()) { - return false; - } - if (block.isMapped()) { - // TODO: Determine how to handle these. - return false; - } - if (MemoryBlock.EXTERNAL_BLOCK_NAME.equals(block.getName())) { - return false; - } - return true; - } - - /** - * Compute the "size" of an image - * - *

- * This is considered the maximum loaded address as mapped in memory, minus the image base. - * - * @param program the program image whose size to compute - * @return the size - */ - public static long computeImageSize(Program program) { - Address imageBase = program.getImageBase(); - long imageSize = 0; - // TODO: How to handle Harvard architectures? - for (MemoryBlock block : program.getMemory().getBlocks()) { - if (!includeBlock(program, block)) { - continue; - } - imageSize = Math.max(imageSize, block.getEnd().subtract(imageBase) + 1); - } - return imageSize; - } - - private final TraceModule module; - private Program program; - private AddressRange moduleRange; - - /** - * Construct a module map entry - * - *

- * Generally, only the service implementation should construct an entry. See - * {@link DebuggerStaticMappingService#proposeModuleMap(TraceModule, Program)} and related - * to obtain these. - * - * @param module the module - * @param program the matched program - * @param moduleRange a range from the module base the size of the program's image - */ - public ModuleMapEntry(TraceModule module, Program program, AddressRange moduleRange) { - this.module = module; - this.program = program; - this.moduleRange = moduleRange; - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof ModuleMapEntry)) { - return false; - } - ModuleMapEntry that = (ModuleMapEntry) obj; - if (this.module != that.module) { - return false; - } - /*if (this.program != that.program) { - return false; - }*/ - // imageSize is derived - return true; - } - - @Override - public int hashCode() { - return Objects.hash(module/*, program*/); - } - - /** - * Get the module for this entry - * - * @return the module - */ - public TraceModule getModule() { - return module; - } - - /** - * Get the address range of the module in the trace, as computed from the matched program's - * image size - * - * @return the module range - */ - public AddressRange getModuleRange() { - return moduleRange; - } - - /** - * Get the matched program - * - * @return the program - */ - public Program getProgram() { - return program; - } - - /** - * Set the matched program - * - *

- * This is generally used in UIs to let the user tweak and reassign, if desired. This will - * also re-compute the module range based on the new program's image size. - * - * @param program the program - */ - public void setProgram(Program program) { - this.program = program; - try { - this.moduleRange = - new AddressRangeImpl(module.getBase(), computeImageSize(program)); - } - catch (AddressOverflowException e) { - // This is terribly unlikely - throw new IllegalArgumentException( - "Specified program is too large for module's memory space"); - } - } - } - - /** - * A section-block entry in a proposed section map - */ - public static class SectionMapEntry { - private final TraceSection section; - private Program program; - private MemoryBlock block; - - /** - * Construct a section map entry - * - *

- * Generally, only the service implementation should construct an entry. See - * {@link DebuggerStaticMappingService#proposeSectionMap(TraceSection, Program, MemoryBlock)} - * and related to obtain these. - * - * @param section the section - * @param program the program containing the matched block - * @param block the matched memory block - */ - public SectionMapEntry(TraceSection section, Program program, MemoryBlock block) { - this.section = section; - this.program = program; - this.block = block; - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof SectionMapEntry)) { - return false; - } - SectionMapEntry that = (SectionMapEntry) obj; - if (this.section != that.section) { - return false; - } - /*if (this.program != that.program) { - return false; - } - if (this.block != that.block) { - return false; - }*/ - return true; - } - - @Override - public int hashCode() { - return Objects.hash(section/*, program, block*/); - } - - /** - * Get the module containing the section - * - * @return the module - */ - public TraceModule getModule() { - return section.getModule(); - } - - /** - * Get the section - * - * @return the section - */ - public TraceSection getSection() { - return section; - } - - /** - * Get the program containing the matched memory block - * - * @return the program - */ - public Program getProgram() { - return program; - } - - /** - * Get the matched memory block - * - * @return the block - */ - public MemoryBlock getBlock() { - return block; - } - - /** - * Set the matched memory block - * - * @param program the program containing the block - * @param block the block - */ - public void setBlock(Program program, MemoryBlock block) { - this.program = program; - this.block = block; - } - - /** - * Get the length of the match - * - *

- * Ideally, the section and block have exactly the same length. If they do not, the - * (unsigned) minimum of the two is used. - * - * @return the length - */ - public long getLength() { - return MathUtilities.unsignedMin(section.getRange().getLength(), block.getSize()); - } - } - - /** - * <<<<<<< HEAD A {@code (shift,view)} pair for describing sets of mapped addresses + * A {@code (shift,view)} pair for describing sets of mapped addresses */ public class ShiftAndAddressSetView { private final long shift; @@ -528,10 +92,6 @@ public interface DebuggerStaticMappingService { /** * Add a static mapping (relocation) from the given trace to the given program * - *

- * Note if the trace is backed by a Ghidra database, the caller must already have started a - * transaction on the relevant domain object. - * * @param from the source trace location, including lifespan * @param to the destination program location * @param length the length of the mapped region, where 0 indicates {@code 1 << 64}. @@ -554,24 +114,14 @@ public interface DebuggerStaticMappingService { void addIdentityMapping(Trace from, Program toProgram, Range lifespan, boolean truncateExisting); - /** - * Add a static mapping (relocation) from the given module to the given program - * - *

- * This is simply a shortcut and does not mean to imply that all mappings must represent module - * relocations. The lifespan is that of the module's. - * - * @param from the source module - * @param length the "size" of the module -- {@code max-min+1} as loaded/mapped in memory - * @param toProgram the destination program - * @see #addMapping(TraceLocation, ProgramLocation, long, boolean) - */ - void addModuleMapping(TraceModule from, long length, Program toProgram, - boolean truncateExisting) throws TraceConflictedMappingException; + void addMapping(MapEntry entry, boolean truncateExisting) + throws TraceConflictedMappingException; + + void addMappings(Collection> entries, TaskMonitor monitor, + boolean truncateExisting, String description) throws CancelledException; /** - * ======= >>>>>>> d694542c5 (GP-660: Put program filler back in. Need to performance test.) Add - * several static mappings (relocations) + * Add several static mappings (relocations) * *

* This will group the entries by trace and add each's entries in a single transaction. If any @@ -582,6 +132,8 @@ public interface DebuggerStaticMappingService { * @param monitor a monitor to cancel the operation * @param truncateExisting true to delete or truncate the lifespan of overlapping entries * @see #addMapping(TraceLocation, ProgramLocation, long, boolean) + * @throws TraceConflictedMappingException if a conflicting mapping overlaps the source and + * {@code truncateExisting} is false. */ void addModuleMappings(Collection entries, TaskMonitor monitor, boolean truncateExisting) throws CancelledException; @@ -602,6 +154,22 @@ public interface DebuggerStaticMappingService { void addSectionMappings(Collection entries, TaskMonitor monitor, boolean truncateExisting) throws CancelledException; + /** + * Add several static mappings (relocations) + * + *

+ * This will group the entries by trace and add each's entries in a single transaction. If any + * entry fails, including due to conflicts, that failure is logged but ignored, and the + * remaining entries are processed. + * + * @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 + * @see #addMapping(TraceLocation, ProgramLocation, long, boolean) + */ + void addRegionMappings(Collection entries, TaskMonitor monitor, + boolean truncateExisting) throws CancelledException; + /** * Collect all the open destination programs relevant for the given trace and snap * @@ -741,16 +309,6 @@ public interface DebuggerStaticMappingService { */ Set findProbableModulePrograms(TraceModule module); - /** - * Recursively collect external programs, i.e., libraries, starting at the given seed - * - * @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 - */ - Set collectLibraries(Program seed, TaskMonitor monitor) throws CancelledException; - /** * Propose a module map for the given module to the given program * @@ -866,4 +424,60 @@ public interface DebuggerStaticMappingService { */ Map proposeSectionMaps( Collection modules, Collection programs); + + /** + * Propose a singleton region map from the given region to the given program memory block + * + *

+ * Note, no sanity check is performed on the given parameters. This will simply give a singleton + * map of the given entry. It is strongly advised to use + * {@link RegionMapProposal#computeScore()} to assess the proposal. Alternatively, use + * {@link #proposeRegionMap(Collection, Collection)} to have the service select the best-scored + * mapping from a collection of proposed programs. + * + * @param region the region to map + * @param program the destination program + * @param block the memory block in the destination program + * @return the proposed map + */ + RegionMapProposal proposeRegionMap(TraceMemoryRegion region, Program program, + MemoryBlock block); + + /** + * Propose a region map for the given regions to the given program + * + *

+ * Note, no sanity check is performed on the given parameters. This will do its best to map + * regions to memory blocks in the given program. For the best results, regions should all + * comprise the same module, and the minimum address among the regions should be the module's + * base address. It is strongly advised to use {@link RegionMapProposal#computeScore()} to + * assess the proposal. Alternatively, use {@link #proposeRegionMap(Collection, Collection)} to + * have the service select the best-scored mapping from a collection of proposed programs. + * + * @param region the region to map + * @param program the destination program whose blocks to consider + * @return the proposed map + */ + RegionMapProposal proposeRegionMap(Collection regions, + Program program); + + /** + * Propose the best-scored maps of trace regions to program memory blocks for each given + * "module" given a collection of proposed programs. + * + *

+ * Note, this method will first group regions into likely modules by parsing their names, then + * compare to program names in order to cull unlikely pairs. It then takes the best-scored + * proposal for each module. If a module has no likely paired program, then it is omitted from + * the result. For informational purposes, the keys in the returned map reflect the grouping of + * regions into likely modules. For the best results, the minimum address of each module should + * be among the regions. + * + * @param modules the modules to map + * @param programs a set of proposed destination programs + * @return the composite proposal + */ + Map, RegionMapProposal> proposeRegionMaps( + Collection regions, + Collection programs); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/MapEntry.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/MapEntry.java new file mode 100644 index 0000000000..33e8208be2 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/MapEntry.java @@ -0,0 +1,46 @@ +/* ### + * 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.services; + +import com.google.common.collect.Range; + +import ghidra.program.model.address.AddressRange; +import ghidra.program.model.listing.Program; +import ghidra.program.util.ProgramLocation; +import ghidra.trace.model.Trace; +import ghidra.trace.model.TraceLocation; + +public interface MapEntry { + Trace getFromTrace(); + + T getFromObject(); + + AddressRange getFromRange(); + + Range getFromLifespan(); + + TraceLocation getFromTraceLocation(); + + Program getToProgram(); + + P getToObject(); + + AddressRange getToRange(); + + ProgramLocation getToProgramLocation(); + + long getMappingLength(); +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/MapProposal.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/MapProposal.java new file mode 100644 index 0000000000..c8d688cc07 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/MapProposal.java @@ -0,0 +1,107 @@ +/* ### + * 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.services; + +import java.util.*; +import java.util.stream.Collectors; + +import ghidra.program.model.listing.Program; +import ghidra.trace.model.Trace; +import ghidra.trace.model.modules.TraceStaticMappingManager; +import ghidra.util.task.TaskMonitor; + +public interface MapProposal> { + /** + * Flatten proposals into a single collection of entries + * + *

+ * The output is suitable for use in + * {@link DebuggerStaticMappingService#addMappings(Collection, TaskMonitor, boolean, String)}. + * In some contexts, the user should be permitted to see and optionally adjust the collection + * first. + * + *

+ * Note, it is advisable to filter the returned collection using + * {@link #removeOverlapping(Collection)} to avoid errors from adding overlapped mappings. + * Alternatively, you can set {@code truncateExisting} to true when calling + * {@link DebuggerStaticMappingService#addMappings(Collection, TaskMonitor, boolean, String)}. + * + * @param proposals the collection of proposed maps + * @return the flattened, filtered collection + */ + static , M extends MapProposal> Collection flatten( + Collection proposals) { + Collection result = new LinkedHashSet<>(); + for (M map : proposals) { + result.addAll(map.computeMap().values()); + } + return result; + } + + /** + * Remove entries from a collection which overlap existing entries in the trace + * + * @param entries the entries to filter + * @return the filtered entries + */ + static > Set removeOverlapping(Collection entries) { + return entries.stream().filter(e -> { + TraceStaticMappingManager manager = e.getFromTrace().getStaticMappingManager(); + return manager.findAllOverlapping(e.getFromRange(), e.getFromLifespan()).isEmpty(); + }).collect(Collectors.toSet()); + } + + /** + * Get the trace containing the trace objects in this proposal + * + * @return the trace + */ + Trace getTrace(); + + /** + * Get the corresponding program image of this proposal + * + * @return the program + */ + Program getProgram(); + + /** + * Get the destination (program) object for a given source (trace) object + * + * @param from the trace object + * @return the proposed program object + */ + P getToObject(T from); + + /** + * Compute a notional "score" of the proposal + * + *

+ * This may examine attributes of the "from" and "to" objects, in order to determine the + * likelihood of the match based on this proposal. The implementation need not assign meaning to + * any particular score, but a higher score must imply a more likely match. + * + * @return a score of the proposed pair + */ + double computeScore(); + + /** + * Compute the overall map given by this proposal + * + * @return the map + */ + Map computeMap(); +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/ModuleMapProposal.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/ModuleMapProposal.java new file mode 100644 index 0000000000..263368960e --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/ModuleMapProposal.java @@ -0,0 +1,62 @@ +/* ### + * 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.services; + +import ghidra.app.services.ModuleMapProposal.ModuleMapEntry; +import ghidra.program.model.address.AddressRange; +import ghidra.program.model.listing.Program; +import ghidra.trace.model.modules.TraceModule; + +/** + * A proposed mapping of module to program + */ +public interface ModuleMapProposal extends MapProposal { + + interface ModuleMapEntry extends MapEntry { + /** + * Get the module for this entry + * + * @return the module + */ + TraceModule getModule(); + + /** + * Get the address range of the module in the trace, as computed from the matched program's + * image size + * + * @return the module range + */ + AddressRange getModuleRange(); + + /** + * Set the matched program + * + *

+ * This is generally used in UIs to let the user tweak and reassign, if desired. This will + * also re-compute the module range based on the new program's image size. + * + * @param program the program + */ + void setProgram(Program program); + } + + /** + * Get the trace module of this proposal + * + * @return the module + */ + TraceModule getModule(); +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/RegionMapProposal.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/RegionMapProposal.java new file mode 100644 index 0000000000..4cd3fc82c1 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/RegionMapProposal.java @@ -0,0 +1,52 @@ +/* ### + * 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.services; + +import ghidra.app.services.RegionMapProposal.RegionMapEntry; +import ghidra.program.model.listing.Program; +import ghidra.program.model.mem.MemoryBlock; +import ghidra.trace.model.memory.TraceMemoryRegion; + +/** + * A proposed map of regions to program memory blocks + */ +public interface RegionMapProposal + extends MapProposal { + + interface RegionMapEntry extends MapEntry { + /** + * Get the region + * + * @return the region + */ + TraceMemoryRegion getRegion(); + + /** + * Get the matched memory block + * + * @return the block + */ + MemoryBlock getBlock(); + + /** + * Set the matched memory block + * + * @param program the program containing the block + * @param block the block + */ + void setBlock(Program program, MemoryBlock block); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/SectionMapProposal.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/SectionMapProposal.java new file mode 100644 index 0000000000..eb171914a8 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/SectionMapProposal.java @@ -0,0 +1,75 @@ +/* ### + * 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.services; + +import ghidra.app.services.SectionMapProposal.SectionMapEntry; +import ghidra.program.model.listing.Program; +import ghidra.program.model.mem.MemoryBlock; +import ghidra.trace.model.modules.TraceModule; +import ghidra.trace.model.modules.TraceSection; + +/** + * A proposed map of sections to program memory blocks + */ +public interface SectionMapProposal + extends MapProposal { + + interface SectionMapEntry extends MapEntry { + + /** + * Get the section + * + * @return the section + */ + TraceSection getSection(); + + /** + * Get the module containing the section + * + * @return the module + */ + TraceModule getModule(); + + /** + * Get the matched memory block + * + * @return the block + */ + MemoryBlock getBlock(); + + /** + * Set the matched memory block + * + * @param program the program containing the block + * @param block the block + */ + void setBlock(Program program, MemoryBlock block); + } + + /** + * Get the trace module of this proposal + * + * @return the module + */ + TraceModule getModule(); + + /** + * Get the corresponding program image of this proposal + * + * @return the program + */ + Program getProgram(); +} diff --git a/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerRegionsPluginScreenShots.java b/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerRegionsPluginScreenShots.java index 8196d9bbed..94643005b0 100644 --- a/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerRegionsPluginScreenShots.java +++ b/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerRegionsPluginScreenShots.java @@ -22,58 +22,136 @@ import org.junit.*; import com.google.common.collect.Range; import ghidra.app.plugin.core.debug.service.tracemgr.DebuggerTraceManagerServicePlugin; +import ghidra.app.plugin.core.progmgr.ProgramManagerPlugin; import ghidra.app.services.DebuggerTraceManagerService; +import ghidra.app.services.ProgramManager; +import ghidra.framework.model.DomainFolder; +import ghidra.program.database.ProgramBuilder; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Program; import ghidra.test.ToyProgramBuilder; import ghidra.trace.database.ToyDBTraceBuilder; +import ghidra.trace.database.memory.DBTraceMemoryManager; import ghidra.trace.model.memory.TraceMemoryFlag; import ghidra.util.database.UndoableTransaction; +import ghidra.util.task.TaskMonitor; import help.screenshot.GhidraScreenShotGenerator; public class DebuggerRegionsPluginScreenShots extends GhidraScreenShotGenerator { + ProgramManager programManager; DebuggerTraceManagerService traceManager; DebuggerRegionsPlugin regionsPlugin; + DebuggerRegionsProvider regionsProvider; ToyDBTraceBuilder tb; + Program progBash; + Program progLibC; @Before public void setUpMine() throws Throwable { + programManager = addPlugin(tool, ProgramManagerPlugin.class); traceManager = addPlugin(tool, DebuggerTraceManagerServicePlugin.class); regionsPlugin = addPlugin(tool, DebuggerRegionsPlugin.class); + regionsProvider = waitForComponentProvider(DebuggerRegionsProvider.class); + tb = new ToyDBTraceBuilder("echo", ToyProgramBuilder._X64); } @After public void tearDownMine() { tb.close(); + + if (progBash != null) { + progBash.release(this); + } + if (progLibC != null) { + progLibC.release(this); + } + } + + private static Address addr(Program program, long offset) { + return program.getAddressFactory().getDefaultAddressSpace().getAddress(offset); + } + + private void populateTrace() throws Exception { + try (UndoableTransaction tid = tb.startTransaction()) { + + long snap = tb.trace.getTimeManager().createSnapshot("First").getKey(); + + DBTraceMemoryManager mm = tb.trace.getMemoryManager(); + mm.addRegion("/bin/bash (400000:40ffff)", Range.atLeast(snap), + tb.range(0x00400000, 0x0040ffff), + Set.of(TraceMemoryFlag.READ, TraceMemoryFlag.EXECUTE)); + mm.addRegion("/bin/bash (600000:60ffff)", Range.atLeast(snap), + tb.range(0x00600000, 0x0060ffff), + Set.of(TraceMemoryFlag.READ, TraceMemoryFlag.WRITE)); + mm.addRegion("/lib/libc (7fac0000:7facffff)", Range.atLeast(snap), + tb.range(0x7fac0000, 0x7facffff), + Set.of(TraceMemoryFlag.READ, TraceMemoryFlag.EXECUTE)); + mm.addRegion("/lib/libc (7fcc0000:7fccffff)", Range.atLeast(snap), + tb.range(0x7fcc0000, 0x7fccffff), + Set.of(TraceMemoryFlag.READ, TraceMemoryFlag.WRITE)); + } + } + + private void populateTraceAndPrograms() throws Exception { + DomainFolder root = tool.getProject().getProjectData().getRootFolder(); + + populateTrace(); + + progBash = createDefaultProgram("bash", ProgramBuilder._X64, this); + progLibC = createDefaultProgram("libc.so.6", ProgramBuilder._X64, this); + + try (UndoableTransaction tid = UndoableTransaction.start(progBash, "Add memory", true)) { + progBash.setImageBase(addr(progBash, 0x00400000), true); + progBash.getMemory() + .createInitializedBlock(".text", addr(progBash, 0x00400000), 0x10000, (byte) 0, + TaskMonitor.DUMMY, false); + progBash.getMemory() + .createInitializedBlock(".data", addr(progBash, 0x00600000), 0x10000, (byte) 0, + TaskMonitor.DUMMY, false); + } + + try (UndoableTransaction tid = UndoableTransaction.start(progLibC, "Add memory", true)) { + progLibC.setImageBase(addr(progLibC, 0x00400000), true); + progLibC.getMemory() + .createInitializedBlock(".text", addr(progLibC, 0x00400000), 0x10000, (byte) 0, + TaskMonitor.DUMMY, false); + progLibC.getMemory() + .createInitializedBlock(".data", addr(progLibC, 0x00600000), 0x10000, (byte) 0, + TaskMonitor.DUMMY, false); + } + + root.createFile("trace", tb.trace, TaskMonitor.DUMMY); + root.createFile("bash", progBash, TaskMonitor.DUMMY); + root.createFile("libc.so.6", progLibC, TaskMonitor.DUMMY); + + traceManager.openTrace(tb.trace); + traceManager.activateTrace(tb.trace); + + programManager.openProgram(progBash); + programManager.openProgram(progLibC); } @Test public void testCaptureDebuggerRegionsPlugin() throws Throwable { - try (UndoableTransaction tid = tb.startTransaction()) { - long snap = tb.trace.getTimeManager().createSnapshot("First").getKey(); + populateTrace(); - tb.trace.getMemoryManager() - .addRegion("[400000:40ffff]", Range.atLeast(snap), - tb.range(0x00400000, 0x0040ffff), - Set.of(TraceMemoryFlag.READ, TraceMemoryFlag.EXECUTE)); - tb.trace.getMemoryManager() - .addRegion("[600000:60ffff]", Range.atLeast(snap), - tb.range(0x00600000, 0x0060ffff), - Set.of(TraceMemoryFlag.READ, TraceMemoryFlag.WRITE)); - tb.trace.getMemoryManager() - .addRegion("[7fac0000:7facffff]", Range.atLeast(snap), - tb.range(0x7fac0000, 0x7facffff), - Set.of(TraceMemoryFlag.READ, TraceMemoryFlag.EXECUTE)); - tb.trace.getMemoryManager() - .addRegion("[7fae0000:7faeffff]", Range.atLeast(snap), - tb.range(0x7fae0000, 0x7faeffff), - Set.of(TraceMemoryFlag.READ, TraceMemoryFlag.WRITE)); + traceManager.openTrace(tb.trace); + traceManager.activateTrace(tb.trace); - traceManager.openTrace(tb.trace); - traceManager.activateTrace(tb.trace); + captureIsolatedProvider(DebuggerRegionsProvider.class, 900, 300); + } - captureIsolatedProvider(DebuggerRegionsProvider.class, 900, 300); - } + @Test + public void testCaptureDebuggerRegionMapProposalDialog() throws Throwable { + populateTraceAndPrograms(); + + regionsProvider + .setSelectedRegions(Set.copyOf(tb.trace.getMemoryManager().getAllRegions())); + performAction(regionsProvider.actionMapRegions, false); + + captureDialog(DebuggerRegionMapProposalDialog.class); } } diff --git a/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesPluginScreenShots.java b/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesPluginScreenShots.java index 777ac12dbc..43a0c25635 100644 --- a/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesPluginScreenShots.java +++ b/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesPluginScreenShots.java @@ -41,7 +41,7 @@ public class DebuggerModulesPluginScreenShots extends GhidraScreenShotGenerator DebuggerModulesPlugin modulesPlugin; DebuggerModulesProvider modulesProvider; ToyDBTraceBuilder tb; - Program progEcho; + Program progBash; Program progLibC; @Before @@ -59,8 +59,8 @@ public class DebuggerModulesPluginScreenShots extends GhidraScreenShotGenerator public void tearDownMine() { tb.close(); - if (progEcho != null) { - progEcho.release(this); + if (progBash != null) { + progBash.release(this); } if (progLibC != null) { progLibC.release(this); @@ -111,16 +111,16 @@ public class DebuggerModulesPluginScreenShots extends GhidraScreenShotGenerator lib.addSection("libc[.data]", ".data", tb.range(0x7fae0000, 0x7faeffff)); } - progEcho = createDefaultProgram("bash", ProgramBuilder._X64, this); + progBash = createDefaultProgram("bash", ProgramBuilder._X64, this); progLibC = createDefaultProgram("libc.so.6", ProgramBuilder._X64, this); - try (UndoableTransaction tid = UndoableTransaction.start(progEcho, "Add memory", true)) { - progEcho.setImageBase(addr(progEcho, 0x00400000), true); - progEcho.getMemory() - .createInitializedBlock(".text", addr(progEcho, 0x00400000), 0x10000, (byte) 0, + try (UndoableTransaction tid = UndoableTransaction.start(progBash, "Add memory", true)) { + progBash.setImageBase(addr(progBash, 0x00400000), true); + progBash.getMemory() + .createInitializedBlock(".text", addr(progBash, 0x00400000), 0x10000, (byte) 0, TaskMonitor.DUMMY, false); - progEcho.getMemory() - .createInitializedBlock(".data", addr(progEcho, 0x00600000), 0x10000, (byte) 0, + progBash.getMemory() + .createInitializedBlock(".data", addr(progBash, 0x00600000), 0x10000, (byte) 0, TaskMonitor.DUMMY, false); } @@ -135,13 +135,13 @@ public class DebuggerModulesPluginScreenShots extends GhidraScreenShotGenerator } root.createFile("trace", tb.trace, TaskMonitor.DUMMY); - root.createFile("echo", progEcho, TaskMonitor.DUMMY); + root.createFile("bash", progBash, TaskMonitor.DUMMY); root.createFile("libc.so.6", progLibC, TaskMonitor.DUMMY); traceManager.openTrace(tb.trace); traceManager.activateTrace(tb.trace); - programManager.openProgram(progEcho); + programManager.openProgram(progBash); programManager.openProgram(progLibC); } diff --git a/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerStaticMappingPluginScreenShots.java b/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerStaticMappingPluginScreenShots.java index 6b91d6c9a9..ce706763c0 100644 --- a/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerStaticMappingPluginScreenShots.java +++ b/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerStaticMappingPluginScreenShots.java @@ -22,10 +22,8 @@ import org.junit.*; import ghidra.app.plugin.core.debug.service.modules.DebuggerStaticMappingServicePlugin; import ghidra.app.plugin.core.debug.service.tracemgr.DebuggerTraceManagerServicePlugin; import ghidra.app.plugin.core.progmgr.ProgramManagerPlugin; -import ghidra.app.services.DebuggerStaticMappingService.ModuleMapEntry; -import ghidra.app.services.DebuggerStaticMappingService.ModuleMapProposal; -import ghidra.app.services.DebuggerTraceManagerService; -import ghidra.app.services.ProgramManager; +import ghidra.app.services.*; +import ghidra.app.services.ModuleMapProposal.ModuleMapEntry; import ghidra.framework.model.DomainFolder; import ghidra.program.database.ProgramBuilder; import ghidra.program.model.address.Address; @@ -130,7 +128,7 @@ public class DebuggerStaticMappingPluginScreenShots extends GhidraScreenShotGene Map proposal = mappingService.proposeModuleMaps(tb.trace.getModuleManager().getAllModules(), List.of(programManager.getAllOpenPrograms())); - Collection entries = ModuleMapProposal.flatten(proposal.values()); + Collection entries = MapProposal.flatten(proposal.values()); mappingService.addModuleMappings(entries, TaskMonitor.DUMMY, false); } diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerRegionsProviderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerRegionsProviderTest.java index 1743bf7eb7..362bf01776 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerRegionsProviderTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerRegionsProviderTest.java @@ -17,7 +17,7 @@ package ghidra.app.plugin.core.debug.gui.memory; import static org.junit.Assert.*; -import java.util.Set; +import java.util.*; import org.junit.Before; import org.junit.Test; @@ -26,24 +26,63 @@ import com.google.common.collect.Range; import generic.Unique; import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest; +import ghidra.app.plugin.core.debug.gui.DebuggerBlockChooserDialog; +import ghidra.app.plugin.core.debug.gui.DebuggerBlockChooserDialog.MemoryBlockRow; import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingPlugin; import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingProvider; +import ghidra.app.plugin.core.debug.gui.memory.DebuggerRegionMapProposalDialog.RegionMapTableColumns; import ghidra.app.plugin.core.debug.gui.memory.DebuggerRegionsProvider.RegionTableColumns; +import ghidra.app.services.RegionMapProposal.RegionMapEntry; import ghidra.program.model.address.AddressSet; +import ghidra.program.model.mem.Memory; +import ghidra.program.model.mem.MemoryBlock; import ghidra.program.util.ProgramSelection; import ghidra.trace.model.memory.*; +import ghidra.trace.model.modules.TraceStaticMapping; import ghidra.util.database.UndoableTransaction; public class DebuggerRegionsProviderTest extends AbstractGhidraHeadedDebuggerGUITest { DebuggerRegionsProvider provider; + protected TraceMemoryRegion regionExeText; + protected TraceMemoryRegion regionExeData; + protected TraceMemoryRegion regionLibText; + protected TraceMemoryRegion regionLibData; + + protected MemoryBlock blockExeText; + protected MemoryBlock blockExeData; + @Before public void setUpRegionsTest() throws Exception { addPlugin(tool, DebuggerRegionsPlugin.class); provider = waitForComponentProvider(DebuggerRegionsProvider.class); } + protected void addRegions() throws Exception { + TraceMemoryManager mm = tb.trace.getMemoryManager(); + try (UndoableTransaction tid = tb.startTransaction()) { + regionExeText = mm.createRegion("Regions[/bin/echo 0x55550000]", 0, + tb.range(0x55550000, 0x555500ff), TraceMemoryFlag.READ, TraceMemoryFlag.EXECUTE); + regionExeData = mm.createRegion("Regions[/bin/echo 0x55750000]", 0, + tb.range(0x55750000, 0x5575007f), TraceMemoryFlag.READ, TraceMemoryFlag.WRITE); + regionLibText = mm.createRegion("Regions[/lib/libc.so 0x7f000000]", 0, + tb.range(0x7f000000, 0x7f0003ff), TraceMemoryFlag.READ, TraceMemoryFlag.EXECUTE); + regionLibData = mm.createRegion("Regions[/lib/libc.so 0x7f100000]", 0, + tb.range(0x7f100000, 0x7f10003f), TraceMemoryFlag.READ, TraceMemoryFlag.WRITE); + } + } + + protected void addBlocks() throws Exception { + try (UndoableTransaction tid = UndoableTransaction.start(program, "Add block", true)) { + Memory mem = program.getMemory(); + blockExeText = mem.createInitializedBlock(".text", tb.addr(0x00400000), 0x100, (byte) 0, + monitor, false); + blockExeData = mem.createInitializedBlock(".data", tb.addr(0x00600000), 0x80, (byte) 0, + monitor, false); + } + } + @Test public void testNoTraceEmpty() throws Exception { assertEquals(0, provider.regionTableModel.getModelData().size()); @@ -198,6 +237,92 @@ public class DebuggerRegionsProviderTest extends AbstractGhidraHeadedDebuggerGUI waitForPass(() -> assertEquals(tb.addr(0x0040ffff), listing.getLocation().getAddress())); } + @Test + public void testActionMapRegions() throws Exception { + assertFalse(provider.actionMapRegions.isEnabled()); + + createAndOpenTrace(); + createAndOpenProgramFromTrace(); + intoProject(tb.trace); + intoProject(program); + + addRegions(); + traceManager.activateTrace(tb.trace); + waitForSwing(); + + // Still + assertFalse(provider.actionMapRegions.isEnabled()); + + addBlocks(); + try (UndoableTransaction tid = UndoableTransaction.start(program, "Change name", true)) { + program.setName("echo"); + } + waitForDomainObject(program); + waitForPass(() -> assertEquals(4, provider.regionTable.getRowCount())); + + // NB. Feature works "best" when all regions of modules are selected + // TODO: Test cases where feature works "worst"? + provider.setSelectedRegions(Set.of(regionExeText, regionExeData)); + waitForSwing(); + assertTrue(provider.actionMapRegions.isEnabled()); + + performAction(provider.actionMapRegions, false); + + DebuggerRegionMapProposalDialog propDialog = + waitForDialogComponent(DebuggerRegionMapProposalDialog.class); + + List proposal = new ArrayList<>(propDialog.getTableModel().getModelData()); + assertEquals(2, proposal.size()); + RegionMapEntry entry; + + // Table sorts by name by default. + // Names are file name followed by min address, so .text is first. + entry = proposal.get(0); + assertEquals(regionExeText, entry.getRegion()); + assertEquals(blockExeText, entry.getBlock()); + entry = proposal.get(1); + assertEquals(regionExeData, entry.getRegion()); + assertEquals(blockExeData, entry.getBlock()); + + clickTableCell(propDialog.getTable(), 0, RegionMapTableColumns.CHOOSE.ordinal(), 1); + + DebuggerBlockChooserDialog blockDialog = + waitForDialogComponent(DebuggerBlockChooserDialog.class); + MemoryBlockRow row = blockDialog.getTableFilterPanel().getSelectedItem(); + assertEquals(blockExeText, row.getBlock()); + + pressButtonByText(blockDialog, "OK", true); + assertEquals(blockExeData, entry.getBlock()); // Unchanged + // TODO: Test the changed case + + Collection mappings = + tb.trace.getStaticMappingManager().getAllEntries(); + assertEquals(0, mappings.size()); + + pressButtonByText(propDialog, "OK", true); + waitForDomainObject(tb.trace); + assertEquals(2, mappings.size()); + Iterator mit = mappings.iterator(); + TraceStaticMapping sm; + + sm = mit.next(); + assertEquals(Range.atLeast(0L), sm.getLifespan()); + assertEquals("ram:00400000", sm.getStaticAddress()); + assertEquals(0x100, sm.getLength()); + assertEquals(tb.addr(0x55550000), sm.getMinTraceAddress()); + + sm = mit.next(); + assertEquals(Range.atLeast(0L), sm.getLifespan()); + assertEquals("ram:00600000", sm.getStaticAddress()); + assertEquals(0x80, sm.getLength()); + assertEquals(tb.addr(0x55750000), sm.getMinTraceAddress()); + + assertFalse(mit.hasNext()); + } + + // TODO: testActionMapRegionsTo + // TODO: testActionMapRegionTo + @Test public void testActionSelectAddresses() throws Exception { addPlugin(tool, DebuggerListingPlugin.class); diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProviderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProviderTest.java index 1a7652f8f3..c3cfed4c8c 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProviderTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProviderTest.java @@ -30,15 +30,16 @@ import docking.widgets.filechooser.GhidraFileChooser; import generic.Unique; import generic.test.category.NightlyCategory; import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest; +import ghidra.app.plugin.core.debug.gui.DebuggerBlockChooserDialog; +import ghidra.app.plugin.core.debug.gui.DebuggerBlockChooserDialog.MemoryBlockRow; import ghidra.app.plugin.core.debug.gui.DebuggerResources.*; import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingPlugin; import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingProvider; -import ghidra.app.plugin.core.debug.gui.modules.DebuggerBlockChooserDialog.MemoryBlockRow; import ghidra.app.plugin.core.debug.gui.modules.DebuggerModuleMapProposalDialog.ModuleMapTableColumns; import ghidra.app.plugin.core.debug.gui.modules.DebuggerSectionMapProposalDialog.SectionMapTableColumns; import ghidra.app.services.DebuggerListingService; -import ghidra.app.services.DebuggerStaticMappingService.ModuleMapEntry; -import ghidra.app.services.DebuggerStaticMappingService.SectionMapEntry; +import ghidra.app.services.ModuleMapProposal.ModuleMapEntry; +import ghidra.app.services.SectionMapProposal.SectionMapEntry; import ghidra.app.services.TraceRecorder; import ghidra.dbg.attributes.TargetPrimitiveDataType.DefaultTargetPrimitiveDataType; import ghidra.dbg.attributes.TargetPrimitiveDataType.PrimitiveKind; @@ -46,7 +47,6 @@ import ghidra.dbg.model.TestTargetModule; import ghidra.dbg.model.TestTargetTypedefDataType; import ghidra.dbg.util.TargetDataTypeConverter; import ghidra.framework.main.DataTreeDialog; -import ghidra.framework.store.LockException; import ghidra.plugin.importer.ImporterPlugin; import ghidra.program.model.address.AddressOverflowException; import ghidra.program.model.address.AddressSet; @@ -126,7 +126,7 @@ public class DebuggerModulesProviderTest extends AbstractGhidraHeadedDebuggerGUI } } - protected MemoryBlock addBlock() throws LockException, DuplicateNameException, + protected MemoryBlock addBlock() throws Exception, MemoryConflictException, AddressOverflowException, CancelledException { try (UndoableTransaction tid = UndoableTransaction.start(program, "Add block", true)) { return program.getMemory() @@ -232,13 +232,13 @@ public class DebuggerModulesProviderTest extends AbstractGhidraHeadedDebuggerGUI DebuggerSectionMapProposalDialog propDialog = waitForDialogComponent(DebuggerSectionMapProposalDialog.class); - clickTableCell(propDialog.table, 0, SectionMapTableColumns.CHOOSE.ordinal(), 1); + clickTableCell(propDialog.getTable(), 0, SectionMapTableColumns.CHOOSE.ordinal(), 1); DebuggerBlockChooserDialog blockDialog = waitForDialogComponent(DebuggerBlockChooserDialog.class); - assertEquals(1, blockDialog.tableModel.getRowCount()); - MemoryBlockRow row = blockDialog.tableModel.getModelData().get(0); + assertEquals(1, blockDialog.getTableModel().getRowCount()); + MemoryBlockRow row = blockDialog.getTableModel().getModelData().get(0); assertEquals(program, row.getProgram()); assertEquals(block, row.getBlock()); // NOTE: Other getters should be tested in a separate MemoryBlockRowTest @@ -399,19 +399,19 @@ public class DebuggerModulesProviderTest extends AbstractGhidraHeadedDebuggerGUI DebuggerModuleMapProposalDialog propDialog = waitForDialogComponent(DebuggerModuleMapProposalDialog.class); - List proposal = propDialog.tableModel.getModelData(); + List proposal = propDialog.getTableModel().getModelData(); ModuleMapEntry entry = Unique.assertOne(proposal); assertEquals(modExe, entry.getModule()); - assertEquals(program, entry.getProgram()); + assertEquals(program, entry.getToProgram()); - clickTableCell(propDialog.table, 0, ModuleMapTableColumns.CHOOSE.ordinal(), 1); + clickTableCell(propDialog.getTable(), 0, ModuleMapTableColumns.CHOOSE.ordinal(), 1); DataTreeDialog programDialog = waitForDialogComponent(DataTreeDialog.class); assertEquals(program.getDomainFile(), programDialog.getDomainFile()); pressButtonByText(programDialog, "OK", true); - assertEquals(program, entry.getProgram()); + assertEquals(program, entry.getToProgram()); // TODO: Test the changed case Collection mappings = @@ -429,6 +429,9 @@ public class DebuggerModulesProviderTest extends AbstractGhidraHeadedDebuggerGUI assertEquals(tb.addr(0x55550000), sm.getMinTraceAddress()); } + // TODO: testActionMapModulesTo + // TODO: testActionMapModuleTo + @Test public void testActionMapSections() throws Exception { assertFalse(modulesProvider.actionMapSections.isEnabled()); @@ -461,16 +464,16 @@ public class DebuggerModulesProviderTest extends AbstractGhidraHeadedDebuggerGUI DebuggerSectionMapProposalDialog propDialog = waitForDialogComponent(DebuggerSectionMapProposalDialog.class); - List proposal = propDialog.tableModel.getModelData(); + List proposal = propDialog.getTableModel().getModelData(); SectionMapEntry entry = Unique.assertOne(proposal); assertEquals(secExeText, entry.getSection()); assertEquals(block, entry.getBlock()); - clickTableCell(propDialog.table, 0, SectionMapTableColumns.CHOOSE.ordinal(), 1); + clickTableCell(propDialog.getTable(), 0, SectionMapTableColumns.CHOOSE.ordinal(), 1); DebuggerBlockChooserDialog blockDialog = waitForDialogComponent(DebuggerBlockChooserDialog.class); - MemoryBlockRow row = Unique.assertOne(blockDialog.tableModel.getModelData()); + MemoryBlockRow row = Unique.assertOne(blockDialog.getTableModel().getModelData()); assertEquals(block, row.getBlock()); pressButtonByText(blockDialog, "OK", true); @@ -492,6 +495,9 @@ public class DebuggerModulesProviderTest extends AbstractGhidraHeadedDebuggerGUI assertEquals(tb.addr(0x55550000), sm.getMinTraceAddress()); } + // TODO: testActionMapSectionsTo + // TODO: testActionMapSectionTo + @Test public void testActionSelectAddresses() throws Exception { assertFalse(modulesProvider.actionSelectAddresses.isEnabled()); diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingServiceTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingServiceTest.java index f42ff513f8..cbec0841f6 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingServiceTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingServiceTest.java @@ -33,7 +33,10 @@ import ghidra.program.model.address.*; import ghidra.program.model.listing.Program; import ghidra.program.util.ProgramLocation; import ghidra.trace.database.ToyDBTraceBuilder; +import ghidra.trace.database.memory.DBTraceMemoryManager; import ghidra.trace.model.*; +import ghidra.trace.model.memory.TraceMemoryFlag; +import ghidra.trace.model.memory.TraceMemoryRegion; import ghidra.trace.model.modules.*; import ghidra.util.Msg; import ghidra.util.database.UndoableTransaction; @@ -579,4 +582,26 @@ public class DebuggerStaticMappingServiceTest extends AbstractGhidraHeadedDebugg } // TODO: open trace, add mapping to closed program, then open that program + + // TODO: The various mapping proposals + + @Test + public void testGroupRegionsByLikelyModule() throws Exception { + TraceMemoryRegion echoText, echoData, libText, libData; + DBTraceMemoryManager mm = tb.trace.getMemoryManager(); + try (UndoableTransaction tid = tb.startTransaction()) { + echoText = mm.createRegion("Memory.Regions[/bin/echo (0x00400000)]", + 0, tb.range(0x00400000, 0x0040ffff), TraceMemoryFlag.READ, TraceMemoryFlag.EXECUTE); + echoData = mm.createRegion("Memory.Regions[/bin/echo (0x00600000)]", + 0, tb.range(0x00600000, 0x00600fff), TraceMemoryFlag.READ, TraceMemoryFlag.WRITE); + libText = mm.createRegion("Memory.Regions[/lib/libc.so (0x7ff00000)]", + 0, tb.range(0x7ff00000, 0x7ff0ffff), TraceMemoryFlag.READ, TraceMemoryFlag.EXECUTE); + libData = mm.createRegion("Memory.Regions[/lib/libc.so (0x7ff20000)]", + 0, tb.range(0x7ff20000, 0x7ff20fff), TraceMemoryFlag.READ, TraceMemoryFlag.WRITE); + } + + Set> actual = + DebuggerStaticMappingProposals.groupRegionsByLikelyModule(mm.getAllRegions()); + assertEquals(Set.of(Set.of(echoText, echoData), Set.of(libText, libData)), actual); + } }