From 108ea044cc777cddbaa59cb57d66eb63f509706b Mon Sep 17 00:00:00 2001 From: Dan <46821332+nsadeveloper789@users.noreply.github.com> Date: Tue, 23 Nov 2021 10:50:16 -0500 Subject: [PATCH] GP-1231: Added 'Map by Regions' action to Debugger. --- .../impl/GdbModelTargetMemoryRegion.java | 4 +- Ghidra/Debug/Debugger/certification.manifest | 1 + .../Debugger/ghidra_scripts/AddMapping.java | 14 +- .../topics/DebuggerBots/DebuggerBots.html | 10 + .../DebuggerRegionsPlugin.html | 35 + .../DebuggerRegionMapProposalDialog.png | Bin 0 -> 22302 bytes .../images/DebuggerRegionsPlugin.png | Bin 19430 -> 20302 bytes .../AbstractDebuggerMapProposalDialog.java | 12 +- .../DebuggerBlockChooserDialog.java | 44 +- .../core/debug/gui/DebuggerResources.java | 54 +- .../DebuggerRegionMapProposalDialog.java | 164 +++++ .../gui/memory/DebuggerRegionsPlugin.java | 51 +- .../gui/memory/DebuggerRegionsProvider.java | 178 ++++- .../gui/modules/DebuggerAddMappingDialog.java | 5 +- .../DebuggerModuleMapProposalDialog.java | 9 +- .../gui/modules/DebuggerModulesPlugin.java | 35 +- .../gui/modules/DebuggerModulesProvider.java | 22 +- .../DebuggerSectionMapProposalDialog.java | 9 +- .../service/modules/AbstractMapEntry.java | 99 +++ .../service/modules/AbstractMapProposal.java | 145 ++++ .../DebuggerStaticMappingProposals.java | 287 ++++++++ .../DebuggerStaticMappingServicePlugin.java | 649 +++--------------- .../modules/DebuggerStaticMappingUtils.java | 285 ++++++-- .../modules/DefaultModuleMapProposal.java | 233 +++++++ .../modules/DefaultRegionMapProposal.java | 192 ++++++ .../modules/DefaultSectionMapProposal.java | 178 +++++ .../modules/MapModulesBackgroundCommand.java | 2 +- .../modules/MapRegionsBackgroundCommand.java | 48 ++ .../modules/MapSectionsBackgroundCommand.java | 2 +- .../service/modules/ModuleRegionMatcher.java | 38 + .../workflow/AbstractMapDebuggerBot.java | 180 +++++ .../debug/workflow/MapModulesDebuggerBot.java | 200 +----- .../debug/workflow/MapRegionsDebuggerBot.java | 57 ++ .../workflow/MapSectionsDebuggerBot.java | 176 +---- .../DebuggerStaticMappingService.java | 558 +++------------ .../java/ghidra/app/services/MapEntry.java | 46 ++ .../java/ghidra/app/services/MapProposal.java | 107 +++ .../app/services/ModuleMapProposal.java | 62 ++ .../app/services/RegionMapProposal.java | 52 ++ .../app/services/SectionMapProposal.java | 75 ++ .../DebuggerRegionsPluginScreenShots.java | 122 +++- .../DebuggerModulesPluginScreenShots.java | 24 +- ...ebuggerStaticMappingPluginScreenShots.java | 8 +- .../memory/DebuggerRegionsProviderTest.java | 127 +++- .../modules/DebuggerModulesProviderTest.java | 36 +- .../DebuggerStaticMappingServiceTest.java | 25 + 46 files changed, 3091 insertions(+), 1569 deletions(-) create mode 100644 Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerRegionsPlugin/images/DebuggerRegionMapProposalDialog.png rename Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/{modules => }/AbstractDebuggerMapProposalDialog.java (92%) rename Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/{modules => }/DebuggerBlockChooserDialog.java (82%) create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerRegionMapProposalDialog.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/AbstractMapEntry.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/AbstractMapProposal.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingProposals.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DefaultModuleMapProposal.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DefaultRegionMapProposal.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DefaultSectionMapProposal.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/MapRegionsBackgroundCommand.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/ModuleRegionMatcher.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/workflow/AbstractMapDebuggerBot.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/workflow/MapRegionsDebuggerBot.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/MapEntry.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/MapProposal.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/ModuleMapProposal.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/RegionMapProposal.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/SectionMapProposal.java 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 0000000000000000000000000000000000000000..286db9888b85855a3c054b658809ab7215f9ad5b GIT binary patch literal 22302 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 literal 0 HcmV?d00001 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 d6340e82531bbe1fb55f0b31323b5540f4b62984..b9b4c637f0761383a14c664715c38288d67315db 100644 GIT binary patch literal 20302 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 literal 19430 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 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); + } }