diff --git a/Ghidra/Features/Base/src/main/help/help/topics/Navigation/Navigation.htm b/Ghidra/Features/Base/src/main/help/help/topics/Navigation/Navigation.htm index 52643cce33..733a96b6ae 100644 --- a/Ghidra/Features/Base/src/main/help/help/topics/Navigation/Navigation.htm +++ b/Ghidra/Features/Base/src/main/help/help/topics/Navigation/Navigation.htm @@ -805,7 +805,7 @@ lowest code block, and finally lowest overall address -

Start Symbols - A comma separated list of symbol names to be be used as the +

Start Symbols - A comma separated list of symbol names to be be used as the starting location for the program if the "Preferred Symbol Name" option is selected above. The first matching symbol found will be used as the starting location for newly opened programs.

@@ -816,6 +816,24 @@ when trying to find a starting symbol.

+

Initial Analysis Navigation Options

+

These options control the behavior of the tool after the initial analysis has + completed.

+
+

Ask To Reposition Program - If selected, the user will be prompted if they + would like the program to be positioned to any newly discovered starting symbols as + specified in the Start Symbols option.

+ +

Auto Reposition If Not Moved - If selected, the program will automatically + be reposition to any newly discovered starting symbols as specified in the + Start Symbols option, provided the user has + not manually moved the cursor off the starting location address. If they have manually + moved the cursor, then the behavior will revert to the setting of the "Ask To + Reposition Program" option above.

+ +
+ + diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/events/FirstTimeAnalyzedPluginEvent.java b/Ghidra/Features/Base/src/main/java/ghidra/app/events/FirstTimeAnalyzedPluginEvent.java new file mode 100644 index 0000000000..849d2b7bd6 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/events/FirstTimeAnalyzedPluginEvent.java @@ -0,0 +1,51 @@ +/* ### + * 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.events; + +import java.lang.ref.WeakReference; + +import ghidra.framework.plugintool.PluginEvent; +import ghidra.program.model.listing.Program; + +/** + * Plugin event class for notification of when programs have completed being analyzed for the first + * time. + */ +public class FirstTimeAnalyzedPluginEvent extends PluginEvent { + public static final String EVENT_NAME = "FirstTimeAnalyzed"; + + private WeakReference programRef; + + /** + * Constructor + * @param sourceName source name of the plugin that created this event + * @param program the program that has been analyzed for the first time + */ + public FirstTimeAnalyzedPluginEvent(String sourceName, Program program) { + super(sourceName, EVENT_NAME); + this.programRef = new WeakReference(program); + } + + /** + * Returns the {@link Program} that has just been analyzed for the first time. This method + * can return null, but only if the program has been closed and is no longer in use which + * can't happen if the method is called during the original event notification. + * @return the {@link Program} that has just been analyzed for the first time. + */ + public Program getProgram() { + return programRef.get(); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/events/ProgramActivatedPluginEvent.java b/Ghidra/Features/Base/src/main/java/ghidra/app/events/ProgramActivatedPluginEvent.java index 653a44b4cb..4bcdec39e3 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/events/ProgramActivatedPluginEvent.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/events/ProgramActivatedPluginEvent.java @@ -1,6 +1,5 @@ /* ### * IP: GHIDRA - * REVIEWED: YES * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +15,11 @@ */ package ghidra.app.events; +import java.lang.ref.WeakReference; + import ghidra.framework.plugintool.PluginEvent; import ghidra.program.model.listing.Program; -import java.lang.ref.WeakReference; - /** * Plugin event class for notification of programs being created, opened, or * closed. @@ -29,30 +28,27 @@ import java.lang.ref.WeakReference; public class ProgramActivatedPluginEvent extends PluginEvent { static final String NAME = "Program Activated"; -// static final String TOOL_EVENT_NAME = "Program Activated"; -// -// static { -// registerPluginEventMapping(OpenProgramPluginEvent.class, TOOL_EVENT_NAME); -// } - - private WeakReference newProgramRef; - /** - * Construct a new plugin event. - * @param source name of the plugin that created this event - * @param activeProgram the program associated with this event - */ - public ProgramActivatedPluginEvent(String source, Program activeProgram) { - super(source, NAME); - this.newProgramRef = new WeakReference(activeProgram); - } + private WeakReference newProgramRef; - /** - * Return the new activated program. May be null. - * @return null if the event if for a program closing. - */ - public Program getActiveProgram () { - return newProgramRef.get(); - } + /** + * Construct a new plugin event. + * @param source name of the plugin that created this event + * @param activeProgram the program associated with this event + */ + public ProgramActivatedPluginEvent(String source, Program activeProgram) { + super(source, NAME); + this.newProgramRef = new WeakReference(activeProgram); + } + + /** + * Returns the {@link Program} that has is being activated. This method + * can return null, but it is unlikely. It will only return null if the program has been closed + * and is no longer in use. + * @return the {@link Program} that has just been analyzed for the first time. + */ + public Program getActiveProgram() { + return newProgramRef.get(); + } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/events/ProgramClosedPluginEvent.java b/Ghidra/Features/Base/src/main/java/ghidra/app/events/ProgramClosedPluginEvent.java index 8c81514c5f..08403c852b 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/events/ProgramClosedPluginEvent.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/events/ProgramClosedPluginEvent.java @@ -40,8 +40,10 @@ public class ProgramClosedPluginEvent extends PluginEvent { } /** - * Return the program on this event. - * @return null if the event if for a program closing. + * Returns the {@link Program} that has just been opened. This method + * can return null, but only if the method is called some time after the original event + * notification. + * @return the {@link Program} that has just been analyzed for the first time. */ public Program getProgram() { return programRef.get(); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/events/ProgramHighlightPluginEvent.java b/Ghidra/Features/Base/src/main/java/ghidra/app/events/ProgramHighlightPluginEvent.java index 2dd3f89cbf..376bbe7444 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/events/ProgramHighlightPluginEvent.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/events/ProgramHighlightPluginEvent.java @@ -1,6 +1,5 @@ /* ### * IP: GHIDRA - * REVIEWED: YES * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,11 +24,11 @@ import ghidra.program.util.ProgramSelection; /** * Plugin event generated when the highlight in a program changes. */ -public final class ProgramHighlightPluginEvent extends PluginEvent { - public static final String NAME = "ProgramHighlight"; +public final class ProgramHighlightPluginEvent extends PluginEvent { + public static final String NAME = "ProgramHighlight"; - private ProgramSelection highlight; - private WeakReference programRef; + private ProgramSelection highlight; + private WeakReference programRef; /** * Construct a new event. @@ -37,25 +36,25 @@ public final class ProgramHighlightPluginEvent extends PluginEvent { * @param hl Program selection containing the selected address set. * @param program program being highlighted */ - public ProgramHighlightPluginEvent(String src,ProgramSelection hl, - Program program) { - super(src, NAME); - this.highlight = hl; - this.programRef = new WeakReference(program); - } + public ProgramHighlightPluginEvent(String src, ProgramSelection hl, Program program) { + super(src, NAME); + this.highlight = hl; + this.programRef = new WeakReference(program); + } /** * Returns the program selection contained in this event. * @return ProgramSelection contained in this event. */ - public ProgramSelection getHighlight() { - return highlight; - } + public ProgramSelection getHighlight() { + return highlight; + } - /** - * Returns the Program object that the highlight refers to. - */ - public Program getProgram() { - return programRef.get(); - } + /** + * Returns the Program object that the highlight refers to. + * @return the Program object that the highlight refers to. + */ + public Program getProgram() { + return programRef.get(); + } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/events/ProgramOpenedPluginEvent.java b/Ghidra/Features/Base/src/main/java/ghidra/app/events/ProgramOpenedPluginEvent.java index ed3b88b0e2..ba6b126719 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/events/ProgramOpenedPluginEvent.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/events/ProgramOpenedPluginEvent.java @@ -40,8 +40,10 @@ public class ProgramOpenedPluginEvent extends PluginEvent { } /** - * Return the program on this event. - * @return null if the event if for a program closing. + * Returns the {@link Program} that has just been opened. This method + * can return null, but only if the program has been closed and is no longer in use which + * can't happen if the method is called during the original event notification. + * @return the {@link Program} that has just been analyzed for the first time. */ public Program getProgram() { return programRef.get(); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/events/ProgramPostActivatedPluginEvent.java b/Ghidra/Features/Base/src/main/java/ghidra/app/events/ProgramPostActivatedPluginEvent.java new file mode 100644 index 0000000000..0a5e078c13 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/events/ProgramPostActivatedPluginEvent.java @@ -0,0 +1,51 @@ +/* ### + * 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.events; + +import java.lang.ref.WeakReference; + +import ghidra.framework.plugintool.PluginEvent; +import ghidra.program.model.listing.Program; + +/** + * Plugin event class for notification that plugin first pass processing of a newly activated + * program is complete. More specifically, all plugins have received and had a chance + * to react to a {@link ProgramActivatedPluginEvent}. + */ +public class ProgramPostActivatedPluginEvent extends PluginEvent { + + static final String NAME = "Post Program Activated"; + private WeakReference newProgramRef; + + /** + * Constructor + * @param source name of the plugin that created this event + * @param activeProgram the program that has been activated + */ + public ProgramPostActivatedPluginEvent(String source, Program activeProgram) { + super(source, NAME); + this.newProgramRef = new WeakReference(activeProgram); + } + + /** + * Return the new activated program. May be null. + * @return null if the event if for a program closing. + */ + public Program getActiveProgram() { + return newProgramRef.get(); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/ProgramPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/ProgramPlugin.java index b42366287c..2c976a65a2 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/ProgramPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/ProgramPlugin.java @@ -55,6 +55,7 @@ public abstract class ProgramPlugin extends Plugin { public ProgramPlugin(PluginTool plugintool) { super(plugintool); internalRegisterEventConsumed(ProgramActivatedPluginEvent.class); + internalRegisterEventConsumed(ProgramPostActivatedPluginEvent.class); internalRegisterEventConsumed(ProgramLocationPluginEvent.class); internalRegisterEventConsumed(ProgramSelectionPluginEvent.class); internalRegisterEventConsumed(ProgramHighlightPluginEvent.class); @@ -160,12 +161,16 @@ public abstract class ProgramPlugin extends Plugin { } highlightChanged(currentHighlight); } + else if (event instanceof ProgramPostActivatedPluginEvent ev) { + postProgramActivated(ev.getActiveProgram()); + } + } /** * Subclass should override this method if it is interested when programs become active. * Note: this method is called in response to a ProgramActivatedPluginEvent. - * + *

* At the time this method is called, * the "currentProgram" variable will be set the new active program. * @@ -175,6 +180,20 @@ public abstract class ProgramPlugin extends Plugin { // override } + /** + * Subclass should override this method if it is interested when programs become active and + * all plugins have had a chance to process the {@link ProgramActivatedPluginEvent}. + * Note: this method is called in response to a {@link ProgramPostActivatedPluginEvent} + *

+ * At the time this method is called, + * the "currentProgram" variable will be set the new active program. + * + * @param program the new program going active. + */ + protected void postProgramActivated(Program program) { + // override + } + /** * Subclasses should override this method if it is interested when a program is closed. * diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AutoAnalysisManager.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AutoAnalysisManager.java index 0b0449b5cc..16292e396b 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AutoAnalysisManager.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AutoAnalysisManager.java @@ -18,6 +18,7 @@ package ghidra.app.plugin.core.analysis; import java.lang.reflect.InvocationTargetException; import java.util.*; import java.util.Stack; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import javax.swing.JFrame; @@ -132,7 +133,7 @@ public class AutoAnalysisManager implements DomainObjectListener, DomainObjectCl private MessageLog log = new MessageLog(); - private List listeners = new ArrayList<>(); + private List listeners = new CopyOnWriteArrayList<>(); private EventQueueID eventQueueID; @@ -850,6 +851,7 @@ public class AutoAnalysisManager implements DomainObjectListener, DomainObjectCl for (AnalysisTaskList list : taskArray) { list.notifyAnalysisEnded(program); } + for (AutoAnalysisManagerListener listener : listeners) { listener.analysisEnded(this); } @@ -1268,13 +1270,13 @@ public class AutoAnalysisManager implements DomainObjectListener, DomainObjectCl if (testLen > spacer.length()) { testLen = spacer.length() - 5; } - taskTimesStringBuf.append( - " " + element + spacer.substring(testLen) + secString + "\n"); + taskTimesStringBuf + .append(" " + element + spacer.substring(testLen) + secString + "\n"); } taskTimesStringBuf.append("-----------------------------------------------------\n"); - taskTimesStringBuf.append( - " Total Time " + (int) (totalTaskTime / 1000.00) + " secs\n"); + taskTimesStringBuf + .append(" Total Time " + (int) (totalTaskTime / 1000.00) + " secs\n"); taskTimesStringBuf.append("-----------------------------------------------------\n"); return taskTimesStringBuf.toString(); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AutoAnalysisPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AutoAnalysisPlugin.java index df8aecff22..d017c0c7aa 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AutoAnalysisPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AutoAnalysisPlugin.java @@ -17,8 +17,6 @@ package ghidra.app.plugin.core.analysis; import java.util.*; -import javax.swing.SwingUtilities; - import docking.ActionContext; import docking.DockingWindowManager; import docking.action.DockingAction; @@ -59,7 +57,7 @@ import ghidra.util.task.TaskLauncher; category = PluginCategoryNames.ANALYSIS, shortDescription = "Manages auto-analysis", description = "Provides coordination and a service for All Auto Analysis tasks.", - eventsConsumed = { ProgramOpenedPluginEvent.class, ProgramClosedPluginEvent.class, ProgramActivatedPluginEvent.class } + eventsConsumed = { ProgramOpenedPluginEvent.class, ProgramClosedPluginEvent.class, ProgramActivatedPluginEvent.class, ProgramPostActivatedPluginEvent.class } ) //@formatter:on public class AutoAnalysisPlugin extends Plugin implements AutoAnalysisManagerListener { @@ -180,16 +178,20 @@ public class AutoAnalysisPlugin extends Plugin implements AutoAnalysisManagerLis private void analyzeCallback(Program program, ProgramSelection selection) { AutoAnalysisManager analysisMgr = AutoAnalysisManager.getAnalysisManager(program); - analysisMgr.initializeOptions(); // get initial options + analysisMgr.initializeOptions(); // this allows analyzers to register options with defaults if (!showOptionsDialog(program)) { return; } - analysisMgr.initializeOptions(); // options may have changed + analysisMgr.initializeOptions(); // reloads the options in case the user changed them - // At this point, any analysis that is done is consider to be true for analyzed. - GhidraProgramUtilities.setAnalyzedFlag(program, true); + // check if this is the first time this program is being analyzed. If so, + // schedule a callback when it is completed to send a FirstTimeAnalyzedPluginEvent + boolean isAnalyzed = GhidraProgramUtilities.isAnalyzedFlagSet(program); + if (!isAnalyzed) { + analysisMgr.addListener(new FirstTimeAnalyzedCallback()); + } // start analysis to set the flag, but it probably won't do more. A bit goofy but better // than the way it was @@ -224,16 +226,13 @@ public class AutoAnalysisPlugin extends Plugin implements AutoAnalysisManagerLis @Override public void processEvent(PluginEvent event) { - if (event instanceof ProgramClosedPluginEvent) { - ProgramClosedPluginEvent ev = (ProgramClosedPluginEvent) event; + if (event instanceof ProgramClosedPluginEvent ev) { programClosed(ev.getProgram()); } - else if (event instanceof ProgramOpenedPluginEvent) { - ProgramOpenedPluginEvent ev = (ProgramOpenedPluginEvent) event; + else if (event instanceof ProgramOpenedPluginEvent ev) { programOpened(ev.getProgram()); } - else if (event instanceof ProgramActivatedPluginEvent) { - ProgramActivatedPluginEvent ev = (ProgramActivatedPluginEvent) event; + else if (event instanceof ProgramActivatedPluginEvent ev) { Program program = ev.getActiveProgram(); if (program == null) { removeOneShotActions(); @@ -243,6 +242,12 @@ public class AutoAnalysisPlugin extends Plugin implements AutoAnalysisManagerLis addOneShotActions(program); } } + else if (event instanceof ProgramPostActivatedPluginEvent ev) { + Program program = ev.getActiveProgram(); + if (program != null) { + postProgramActivated(program); + } + } } protected void programOpened(final Program program) { @@ -256,30 +261,18 @@ public class AutoAnalysisPlugin extends Plugin implements AutoAnalysisManagerLis new HelpLocation("AutoAnalysisPlugin", "Auto_Analysis_Option")); } - private void programActivated(final Program program) { - + private void programActivated(Program program) { program.getOptions(StoredAnalyzerTimes.OPTIONS_LIST) - .registerOption( - StoredAnalyzerTimes.OPTION_NAME, OptionType.CUSTOM_TYPE, null, null, - "Cumulative analysis task times", new StoredAnalyzerTimesPropertyEditor()); + .registerOption(StoredAnalyzerTimes.OPTION_NAME, OptionType.CUSTOM_TYPE, null, null, + "Cumulative analysis task times", new StoredAnalyzerTimesPropertyEditor()); - // invokeLater() to ensure that all other plugins have been notified of the program - // activated. This makes sure plugins like the Listing have opened and painted the - // program. - // - // If the user decided to instantly close the code browser before we get to run anything, - // an exception could be thrown! Therefore, we must check to see if the program is closed - // at this point before we run anything. - // - SwingUtilities.invokeLater(() -> { - if (program.isClosed()) { - return; - } - final AutoAnalysisManager analysisMgr = AutoAnalysisManager.getAnalysisManager(program); - if (analysisMgr.askToAnalyze(tool)) { - analyzeCallback(program, null); - } - }); + } + + private void postProgramActivated(Program program) { + AutoAnalysisManager analysisMgr = AutoAnalysisManager.getAnalysisManager(program); + if (analysisMgr.askToAnalyze(tool)) { + analyzeCallback(program, null); + } } /** @@ -373,4 +366,13 @@ public class AutoAnalysisPlugin extends Plugin implements AutoAnalysisManagerLis return canAnalyze; } } + + private class FirstTimeAnalyzedCallback implements AutoAnalysisManagerListener { + @Override + public void analysisEnded(AutoAnalysisManager manager) { + manager.removeListener(this); + tool.firePluginEvent(new FirstTimeAnalyzedPluginEvent(AutoAnalysisPlugin.this.getName(), + manager.getProgram())); + } + } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/navigation/ProgramStartingLocationOptions.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/navigation/ProgramStartingLocationOptions.java index ab9c0a6fef..ff58bd0e52 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/navigation/ProgramStartingLocationOptions.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/navigation/ProgramStartingLocationOptions.java @@ -29,10 +29,18 @@ import ghidra.util.HelpLocation; public class ProgramStartingLocationOptions implements OptionsChangeListener { static final String NAVIGATION_TOPIC = "Navigation"; - public static final String SUB_OPTION = "Starting Program Location"; - public static final String START_LOCATION_TYPE_OPTION = SUB_OPTION + ".Start At: "; - public static final String START_SYMBOLS_OPTION = SUB_OPTION + ".Start Symbols: "; - public static final String UNDERSCORE_OPTION = SUB_OPTION + ".Use Underscores:"; + public static final String START_LOCATION_SUB_OPTION = "Starting Program Location"; + public static final String START_LOCATION_TYPE_OPTION = + START_LOCATION_SUB_OPTION + ".Start At: "; + public static final String START_SYMBOLS_OPTION = + START_LOCATION_SUB_OPTION + ".Start Symbols: "; + public static final String UNDERSCORE_OPTION = START_LOCATION_SUB_OPTION + ".Use Underscores:"; + + public static final String AFTER_ANALYSIS_SUB_OPTION = "After Initial Analysis"; + public static final String ASK_TO_MOVE_OPTION = + AFTER_ANALYSIS_SUB_OPTION + ".Ask To Reposition Program"; + public static final String AUTO_MOVE_OPTION = + AFTER_ANALYSIS_SUB_OPTION + ".Auto Reposition If Not Moved"; private static final String START_LOCATION_DESCRIPTION = "Determines the start location for newly opened programs.\n" + @@ -44,6 +52,12 @@ public class ProgramStartingLocationOptions implements OptionsChangeListener { "(Used when option above is set to \"Preferred Symbol Name\")"; private static final String SYMBOL_PREFIX_DESCRIPTION = "When searching for symbols, also search for the names prepended with \"_\" and \"__\"."; + public static final String ASK_TO_MOVE_DESCRIPTION = + "When initial analysis completed, asks the user if they want to reposition the" + + " program to a newly discovered starting symbol."; + public static final String AUTO_MOVE_DESCRIPTION = + "When initial analysis is completed, automatically repositions the program to " + + "a newly discovered starting symbol, provided the user hasn't manually moved."; private static final String DEFAULT_STARTING_SYMBOLS = "main, WinMain, libc_start_main, WinMainStartup, start, entry"; @@ -71,13 +85,15 @@ public class ProgramStartingLocationOptions implements OptionsChangeListener { private boolean useUnderscorePrefixes; private ToolOptions options; + private boolean askToMove; + private boolean autoMove; public ProgramStartingLocationOptions(PluginTool tool) { options = tool.getOptions(GhidraOptions.NAVIGATION_OPTIONS); HelpLocation help = new HelpLocation(NAVIGATION_TOPIC, "Starting_Program_Location"); // set a help location on the group - Options subOptions = options.getOptions(SUB_OPTION); + Options subOptions = options.getOptions(START_LOCATION_SUB_OPTION); subOptions.setOptionsHelpLocation(help); options.registerOption(START_LOCATION_TYPE_OPTION, StartLocationType.LAST_LOCATION, help, @@ -88,6 +104,13 @@ public class ProgramStartingLocationOptions implements OptionsChangeListener { options.registerOption(UNDERSCORE_OPTION, true, help, SYMBOL_PREFIX_DESCRIPTION); + help = new HelpLocation(NAVIGATION_TOPIC, "After_Initial_Analysis"); + subOptions = options.getOptions(AFTER_ANALYSIS_SUB_OPTION); + subOptions.setOptionsHelpLocation(help); + + options.registerOption(ASK_TO_MOVE_OPTION, true, help, ASK_TO_MOVE_DESCRIPTION); + options.registerOption(AUTO_MOVE_OPTION, true, help, AUTO_MOVE_DESCRIPTION); + startLocationType = options.getEnum(START_LOCATION_TYPE_OPTION, StartLocationType.SYMBOL_NAME); @@ -95,6 +118,10 @@ public class ProgramStartingLocationOptions implements OptionsChangeListener { startSymbols = parse(symbolNames); useUnderscorePrefixes = options.getBoolean(UNDERSCORE_OPTION, true); + + askToMove = options.getBoolean(ASK_TO_MOVE_OPTION, true); + autoMove = options.getBoolean(AUTO_MOVE_OPTION, true); + options.addOptionsChangeListener(this); } @@ -145,6 +172,32 @@ public class ProgramStartingLocationOptions implements OptionsChangeListener { options.removeOptionsChangeListener(this); } + /** + * Returns true if the user should be asked after first analysis if they would like the + * program to be repositioned to a newly discovered starting symbol (e.g. "main") + * + * @return true if the user should be asked after first analysis if they would like the + * program to be repositioned to a newly discovered starting symbol (e.g. "main") + */ + public boolean shouldAskToRepostionAfterAnalysis() { + return askToMove; + } + + /** + * Returns true if the program should be repositioned to a newly discovered starting symbol + * (e.g. "main") when the first analysis is completed, provided the user hasn't manually + * changed the program's location. Note that this option has precedence over the + * {@link #shouldAskToRepostionAfterAnalysis()} option and the user will only be asked + * if they have manually moved the program. + * + * @return true if the program should be repositioned to a newly discovered starting symbol + * (e.g. "main") when the first analysis is completed, provided the user hasn't manually + * changed the program's location. + */ + public boolean shouldAutoRepositionIfNotMoved() { + return autoMove; + } + @Override public void optionsChanged(ToolOptions toolOptions, String optionName, Object oldValue, Object newValue) { @@ -157,5 +210,12 @@ public class ProgramStartingLocationOptions implements OptionsChangeListener { else if (UNDERSCORE_OPTION.equals(optionName)) { useUnderscorePrefixes = (Boolean) newValue; } + else if (ASK_TO_MOVE_OPTION.equals(optionName)) { + askToMove = (Boolean) newValue; + } + else if (AUTO_MOVE_OPTION.equals(optionName)) { + autoMove = (Boolean) newValue; + } } + } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/navigation/ProgramStartingLocationPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/navigation/ProgramStartingLocationPlugin.java index 75611f8b82..9b78f13455 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/navigation/ProgramStartingLocationPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/navigation/ProgramStartingLocationPlugin.java @@ -21,14 +21,14 @@ import java.util.*; import org.jdom.Element; import org.jdom.JDOMException; +import docking.widgets.OptionDialog; import ghidra.app.CorePluginPackage; +import ghidra.app.events.FirstTimeAnalyzedPluginEvent; import ghidra.app.plugin.PluginCategoryNames; import ghidra.app.plugin.ProgramPlugin; -import ghidra.app.plugin.core.navigation.ProgramStartingLocationOptions.StartLocationType; import ghidra.app.services.GoToService; import ghidra.framework.options.SaveState; -import ghidra.framework.plugintool.PluginInfo; -import ghidra.framework.plugintool.PluginTool; +import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.util.PluginStatus; import ghidra.program.model.address.AddressSetView; import ghidra.program.model.listing.Program; @@ -36,7 +36,6 @@ import ghidra.program.model.listing.ProgramUserData; import ghidra.program.model.symbol.Symbol; import ghidra.program.model.symbol.SymbolTable; import ghidra.program.util.ProgramLocation; -import ghidra.util.Swing; import ghidra.util.xml.XmlUtilities; //@formatter:off @@ -46,37 +45,62 @@ import ghidra.util.xml.XmlUtilities; category = PluginCategoryNames.COMMON, shortDescription = "Determines the starting location when a program is opened.", description = "This plugin watches for new programs being opened and determines the best starting location for the listing view.", - servicesRequired = { GoToService.class } + servicesRequired = { GoToService.class }, + eventsConsumed = { FirstTimeAnalyzedPluginEvent.class } ) //@formatter:on public class ProgramStartingLocationPlugin extends ProgramPlugin { + public static enum NonActiveProgramState { + NEWLY_OPENED, + RESTORED, + FIRST_ANALYSIS_COMPLETED + } + private static final String LAST_LOCATION_PROPERTY = "LAST_PROGRAM_LOCATION"; - private Program lastOpenedProgram; private ProgramStartingLocationOptions startOptions; - private Map lastLocationMap = new HashMap<>(); + private WeakHashMap currentLocationsMap = new WeakHashMap<>(); + private WeakHashMap startLocationsMap = new WeakHashMap<>(); + private WeakHashMap programStateMap = new WeakHashMap<>(); public ProgramStartingLocationPlugin(PluginTool tool) { super(tool); startOptions = new ProgramStartingLocationOptions(tool); } + @Override + public void processEvent(PluginEvent event) { + super.processEvent(event); + + if (event instanceof FirstTimeAnalyzedPluginEvent ev) { + Program program = ev.getProgram(); + if (program != null) { + firstAnalysisCompleted(program); + } + } + } + + private void firstAnalysisCompleted(Program program) { + if (program.equals(currentProgram)) { + processFirstAnalysisCompleted(); + } + else { + programStateMap.put(program, NonActiveProgramState.FIRST_ANALYSIS_COMPLETED); + } + } + @Override protected void programOpened(Program program) { - // if the open program event is a result of restoring the tool's data state, don't - // interfere with the tool's restoration of the last location for that program if (tool.isRestoringDataState()) { - return; + programStateMap.put(program, NonActiveProgramState.RESTORED); } - if (startOptions.getStartLocationType() == StartLocationType.LOWEST_ADDRESS) { - // this is what happens by default, so no need to do anything - return; + else { + programStateMap.put(program, NonActiveProgramState.NEWLY_OPENED); } - lastOpenedProgram = program; } protected void programClosed(Program program) { - ProgramLocation lastLocation = lastLocationMap.remove(program); + ProgramLocation lastLocation = currentLocationsMap.remove(program); if (lastLocation == null) { return; } @@ -87,22 +111,66 @@ public class ProgramStartingLocationPlugin extends ProgramPlugin { String xmlString = XmlUtilities.toString(saveState.saveToXml()); programUserData.setStringProperty(LAST_LOCATION_PROPERTY, xmlString); + programStateMap.remove(program); + currentLocationsMap.remove(program); + } @Override - protected void programActivated(Program program) { - super.programActivated(program); - if (program == lastOpenedProgram) { - Swing.runLater(this::setStartingLocationForNewProgram); + protected void postProgramActivated(Program program) { + NonActiveProgramState state = programStateMap.remove(program); + if (state == NonActiveProgramState.NEWLY_OPENED) { + setStartingLocationForNewProgram(); } - lastOpenedProgram = null; + else if (state == NonActiveProgramState.FIRST_ANALYSIS_COMPLETED) { + processFirstAnalysisCompleted(); + } + } + + private void processFirstAnalysisCompleted() { + boolean shouldAskToRepostion = startOptions.shouldAskToRepostionAfterAnalysis(); + boolean autoRepositionIfNotMoved = startOptions.shouldAutoRepositionIfNotMoved(); + + if (!shouldAskToRepostion && !autoRepositionIfNotMoved) { + return; + } + + // if analysis didn't find any starting symbol, nothing to do + Symbol symbol = findStartingSymbol(currentProgram); + if (symbol == null) { + return; + } + + // if already at the symbol's address, don't do anything + if (currentLocation != null && currentLocation.getAddress().equals(symbol.getAddress())) { + return; + } + + if (autoRepositionIfNotMoved && isProgramAtStartingLocation()) { + gotoLocation(symbol.getProgramLocation()); + } + else if (shouldAskToRepostion && askToPositionProgram(symbol)) { + gotoLocation(symbol.getProgramLocation()); + } + } + + private boolean askToPositionProgram(Symbol symbol) { + int result = OptionDialog.showYesNoDialog(null, "Reposition Program?", + "Analysis found the symbol \"" + symbol.getName() + + "\". Would you like to go to that symbol?"); + return result == OptionDialog.YES_OPTION; } @Override protected void locationChanged(ProgramLocation loc) { if (loc != null) { Program program = loc.getProgram(); - lastLocationMap.put(program, loc); + currentLocationsMap.put(program, loc); + + // the startLocationsMap only gets updated with the first location + if (!startLocationsMap.containsKey(program)) { + startLocationsMap.put(program, loc); + } } } @@ -111,15 +179,29 @@ public class ProgramStartingLocationPlugin extends ProgramPlugin { return; } - GoToService gotoService = tool.getService(GoToService.class); - ProgramLocation location = getStartingProgramLocation(currentProgram); if (location != null) { - gotoService.goTo(location); + gotoLocation(location); + startLocationsMap.put(currentProgram, location); } } + private void gotoLocation(ProgramLocation location) { + GoToService gotoService = tool.getService(GoToService.class); + gotoService.goTo(location); + } + + private boolean isProgramAtStartingLocation() { + ProgramLocation startLocation = startLocationsMap.get(currentProgram); + if (startLocation == null || currentLocation == null) { + return true; + } + // just compare address, analysis may have tweaked the current location even + // the user didn't move + return startLocation.getAddress().equals(currentLocation.getAddress()); + } + private ProgramLocation getStartingProgramLocation(Program program) { switch (startOptions.getStartLocationType()) { case LAST_LOCATION: @@ -129,7 +211,7 @@ public class ProgramStartingLocationPlugin extends ProgramPlugin { } // fall through and try symbol name case SYMBOL_NAME: - Symbol symbol = fingStartingSymbol(program); + Symbol symbol = findStartingSymbol(program); if (symbol != null) { return symbol.getProgramLocation(); } @@ -158,7 +240,7 @@ public class ProgramStartingLocationPlugin extends ProgramPlugin { } } - private Symbol fingStartingSymbol(Program program) { + private Symbol findStartingSymbol(Program program) { List symbolNames = startOptions.getStartingSymbolNames(); boolean useUnderscores = startOptions.useUnderscorePrefixes(); for (String symbolName : symbolNames) { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/MultiProgramManager.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/MultiProgramManager.java index 80b6dbdb79..717227e8bb 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/MultiProgramManager.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/MultiProgramManager.java @@ -51,6 +51,7 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener { private Runnable programChangedRunnable; private boolean hasUnsavedPrograms; + private String pluginName; // These data structures are accessed from multiple threads. Rather than synchronizing all // accesses, we have chosen to be weakly consistent. We assume that any out-of-date checks @@ -64,6 +65,7 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener { MultiProgramManager(ProgramManagerPlugin programManagerPlugin) { this.plugin = programManagerPlugin; this.tool = programManagerPlugin.getTool(); + this.pluginName = plugin.getName(); txMonitor = new TransactionMonitor(); txMonitor.setName("Transaction Open (Program being modified)"); @@ -173,9 +175,9 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener { Program[] getOtherPrograms() { Program currentProgram = getCurrentProgram(); List list = openPrograms.stream() - .map(info -> info.program) - .filter(program -> program != currentProgram) - .collect(Collectors.toList()); + .map(info -> info.program) + .filter(program -> program != currentProgram) + .collect(Collectors.toList()); return list.toArray(new Program[list.size()]); } @@ -260,25 +262,35 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener { if (toolState != null) { toolState.restoreTool(); } + // only fire the post activated event when a program is activated (we send activated with + // null program to represent a phantom de-activated event) + if (newProgram != null) { + firePostActivatedEvent(newProgram); + } } private void fireOpenEvents(Program program) { - plugin.firePluginEvent(new ProgramOpenedPluginEvent("", program)); - plugin.firePluginEvent(new OpenProgramPluginEvent("", program)); + plugin.firePluginEvent(new ProgramOpenedPluginEvent(pluginName, program)); + plugin.firePluginEvent(new OpenProgramPluginEvent(pluginName, program)); } private void fireCloseEvents(Program program) { - plugin.firePluginEvent(new ProgramClosedPluginEvent("", program)); - plugin.firePluginEvent(new CloseProgramPluginEvent("", program, true)); + plugin.firePluginEvent(new ProgramClosedPluginEvent(pluginName, program)); + plugin.firePluginEvent(new CloseProgramPluginEvent(pluginName, program, true)); // tool.contextChanged(); } private void fireActivatedEvent(Program newProgram) { - plugin.firePluginEvent(new ProgramActivatedPluginEvent("", newProgram)); + plugin.firePluginEvent(new ProgramActivatedPluginEvent(pluginName, newProgram)); + } + + private void firePostActivatedEvent(Program newProgram) { + plugin.firePluginEvent(new ProgramPostActivatedPluginEvent(pluginName, newProgram)); } private void fireVisibilityChangeEvent(Program program, boolean isVisible) { - plugin.firePluginEvent(new ProgramVisibilityChangePluginEvent("", program, isVisible)); + plugin.firePluginEvent( + new ProgramVisibilityChangePluginEvent(pluginName, program, isVisible)); } @Override @@ -503,13 +515,13 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener { private static final AtomicInteger nextAvailableId = new AtomicInteger(); public final Program program; - + // NOTE: domainFile and ghidraURL use are mutually exclusive and reflect how program was // opened. Supported cases include: // 1. Opened via Program file // 2. Opened via ProgramLink file // 3. Opened via Program URL - + public final DomainFile domainFile; // may be link file public final URL ghidraURL; @@ -532,7 +544,7 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener { this.visible = visible; instance = nextAvailableId.incrementAndGet(); } - + ProgramInfo(Program p, URL ghidraURL, boolean visible) { this.program = p; this.domainFile = null; diff --git a/Ghidra/Features/Base/src/main/java/ghidra/program/util/GhidraProgramUtilities.java b/Ghidra/Features/Base/src/main/java/ghidra/program/util/GhidraProgramUtilities.java index 50b83970f4..c9de4fa333 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/program/util/GhidraProgramUtilities.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/program/util/GhidraProgramUtilities.java @@ -89,4 +89,23 @@ public class GhidraProgramUtilities { program.endTransaction(transactionID, true); } } + + /** + * Returns true if the program has been analyzed at least once. + * @param program the program to test to see if it has been analyzed + * @return true if the program has been analyzed at least once. + */ + public static boolean isAnalyzedFlagSet(Program program) { + Options options = program.getOptions(Program.PROGRAM_INFO); + + // we first have to check if the flag has even been created because checking the flag + // directly causes it to be created if it doesn't exist and we unfortunately use the + // existence of the flag to know whether or not to ask the user if they want to start + // analysis + if (!options.isRegistered(Program.ANALYZED)) { + return false; + } + + return options.getBoolean(Program.ANALYZED, false); + } } diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/navigation/ProgramStartPluginTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/navigation/ProgramStartPluginTest.java index 36af3269bb..453d3b1760 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/navigation/ProgramStartPluginTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/navigation/ProgramStartPluginTest.java @@ -19,7 +19,9 @@ import static org.junit.Assert.*; import org.junit.*; +import docking.widgets.OptionDialog; import ghidra.GhidraOptions; +import ghidra.app.events.FirstTimeAnalyzedPluginEvent; import ghidra.app.plugin.core.codebrowser.CodeBrowserPlugin; import ghidra.framework.options.Options; import ghidra.framework.plugintool.PluginTool; @@ -57,7 +59,7 @@ public class ProgramStartPluginTest extends AbstractGhidraHeadedIntegrationTest @Test public void testOpensToStartingSymbolByDefault() throws Exception { - ProgramBuilder builder = getProgramBuilder("0x100"); + ProgramBuilder builder = getProgramBuilder("program 1", "0x100"); builder.createLabel("0x105", "main"); loadProgram(builder.getProgram()); @@ -66,7 +68,7 @@ public class ProgramStartPluginTest extends AbstractGhidraHeadedIntegrationTest @Test public void testOpensToLowestCodeBlock() throws Exception { - ProgramBuilder builder = getProgramBuilder("0x100"); + ProgramBuilder builder = getProgramBuilder("program 1", "0x100"); MemoryBlock block = builder.createMemory(".text", "0x200", 0x200); builder.setExecute(block, true); builder.createLabel("0x105", "main"); @@ -80,7 +82,7 @@ public class ProgramStartPluginTest extends AbstractGhidraHeadedIntegrationTest @Test public void testOpensToStartingSymbolNotFirstInSymbolList() throws Exception { - ProgramBuilder builder = getProgramBuilder("0x100"); + ProgramBuilder builder = getProgramBuilder("program 1", "0x100"); setSymbolListOption("main, foobar, start"); builder.createLabel("0x107", "start"); @@ -91,7 +93,7 @@ public class ProgramStartPluginTest extends AbstractGhidraHeadedIntegrationTest @Test public void testOpensToFirstSymbolWhenMutlipesAreFoud() throws Exception { - ProgramBuilder builder = getProgramBuilder("0x100"); + ProgramBuilder builder = getProgramBuilder("program 1", "0x100"); setSymbolListOption("main, start"); builder.createLabel("0x110", "start"); builder.createLabel("0x105", "start"); @@ -103,7 +105,7 @@ public class ProgramStartPluginTest extends AbstractGhidraHeadedIntegrationTest @Test public void testOpensToStartingSymbolWithOneUndercore() throws Exception { - ProgramBuilder builder = getProgramBuilder("0x100"); + ProgramBuilder builder = getProgramBuilder("program 1", "0x100"); setSymbolListOption("main, start"); builder.createLabel("0x105", "_main"); builder.createLabel("0x107", "start"); @@ -115,7 +117,7 @@ public class ProgramStartPluginTest extends AbstractGhidraHeadedIntegrationTest @Test public void testOpensToStartingSymbolWithTwoUndercores() throws Exception { - ProgramBuilder builder = getProgramBuilder("0x100"); + ProgramBuilder builder = getProgramBuilder("program 1", "0x100"); setSymbolListOption("main, start"); builder.createLabel("0x105", "__main"); builder.createLabel("0x107", "_start"); @@ -128,7 +130,7 @@ public class ProgramStartPluginTest extends AbstractGhidraHeadedIntegrationTest @Test public void testNoUnderscoresSearching() throws Exception { - ProgramBuilder builder = getProgramBuilder("0x100"); + ProgramBuilder builder = getProgramBuilder("program 1", "0x100"); setSymbolListOption("main, start"); setUnderscoreOption(false); builder.createLabel("0x105", "__main"); @@ -142,7 +144,7 @@ public class ProgramStartPluginTest extends AbstractGhidraHeadedIntegrationTest @Test public void testOptionToStartAtLowestAddress() throws Exception { - ProgramBuilder builder = getProgramBuilder("0x100"); + ProgramBuilder builder = getProgramBuilder("program 1", "0x100"); builder.createLabel("0x105", "main"); setOptionToLowestAddress(); loadProgram(builder.getProgram()); @@ -152,7 +154,7 @@ public class ProgramStartPluginTest extends AbstractGhidraHeadedIntegrationTest @Test public void testOptionToStartAtLastLocation() throws Exception { - ProgramBuilder builder = getProgramBuilder("0x100"); + ProgramBuilder builder = getProgramBuilder("program 1", "0x100"); ProgramDB program = builder.getProgram(); loadProgram(program); cb.goTo(new ProgramLocation(program, addr("0x107"))); @@ -162,10 +164,124 @@ public class ProgramStartPluginTest extends AbstractGhidraHeadedIntegrationTest assertEquals(addr("0x107"), cb.getCurrentAddress()); } + @Test + public void testProgramAutoRepostionsAfterAnalysis() throws Exception { + ProgramBuilder builder = getProgramBuilder("program 1", "0x100"); + loadProgram(builder.getProgram()); + + assertEquals(addr("0x100"), cb.getCurrentAddress()); + + simulateAnaysisCreatingMain(builder, "0x105"); + + assertEquals(addr("0x105"), cb.getCurrentAddress()); + + } + + @Test + public void testProgramAsksToRepostionsAfterAnalysisYes() throws Exception { + ProgramBuilder builder = getProgramBuilder("program 1", "0x100"); + loadProgram(builder.getProgram()); + + assertEquals(addr("0x100"), cb.getCurrentAddress()); + cb.goToField(addr("0x102"), "Address", 0, 0); + assertEquals(addr("0x102"), cb.getCurrentAddress()); + + simulateAnaysisCreatingMain(builder, "0x105"); + + OptionDialog dialog = waitForDialogComponent(OptionDialog.class); + pressButtonByText(dialog, "Yes"); + waitForSwing(); + assertEquals(addr("0x105"), cb.getCurrentAddress()); + + } + + @Test + public void testProgramAsksToRepostionsAfterAnalysisNo() throws Exception { + ProgramBuilder builder = getProgramBuilder("program 1", "0x100"); + loadProgram(builder.getProgram()); + + assertEquals(addr("0x100"), cb.getCurrentAddress()); + cb.goToField(addr("0x102"), "Address", 0, 0); + assertEquals(addr("0x102"), cb.getCurrentAddress()); + + simulateAnaysisCreatingMain(builder, "0x105"); + + OptionDialog dialog = waitForDialogComponent(OptionDialog.class); + pressButtonByText(dialog, "No"); + waitForSwing(); + assertEquals(addr("0x102"), cb.getCurrentAddress()); + + } + + @Test + public void testAutoMoveOff() throws Exception { + ProgramBuilder builder = getProgramBuilder("program 1", "0x100"); + loadProgram(builder.getProgram()); + setOptionToNotAutoMove(); + setOptionToNotAsk(); + + assertEquals(addr("0x100"), cb.getCurrentAddress()); + + simulateAnaysisCreatingMain(builder, "0x105"); + + assertEquals(addr("0x100"), cb.getCurrentAddress()); + } + + @Test + public void testAnalysisHappensWhenProgramIsNotActiveNoUserMove() throws Exception { + ProgramBuilder builder1 = getProgramBuilder("program 1", "0x100"); + loadProgram(builder1.getProgram()); + ProgramBuilder builder2 = getProgramBuilder("program 2", "0x100"); + loadProgram(builder2.getProgram()); + + simulateAnaysisCreatingMain(builder1, "0x104"); + assertEquals(addr("0x100"), cb.getCurrentAddress()); + + env.close(builder2.getProgram()); + + assertEquals(addr("0x104"), cb.getCurrentAddress()); + } + + @Test + public void testAnalysisHappensWhenProgramIsNotActiveWithUserMove() throws Exception { + ProgramBuilder builder1 = getProgramBuilder("program 1", "0x100"); + loadProgram(builder1.getProgram()); + cb.goToField(addr("0x102"), "Address", 0, 0); + assertEquals(addr("0x102"), cb.getCurrentAddress()); + + ProgramBuilder builder2 = getProgramBuilder("program 2", "0x100"); + loadProgram(builder2.getProgram()); + + simulateAnaysisCreatingMain(builder1, "0x104"); + assertEquals(addr("0x100"), cb.getCurrentAddress()); + + runSwingLater(() -> env.close(builder2.getProgram())); + OptionDialog dialog = waitForDialogComponent(OptionDialog.class); + pressButtonByText(dialog, "Yes"); + waitForSwing(); + + assertEquals(addr("0x104"), cb.getCurrentAddress()); + } + + private void simulateAnaysisCreatingMain(ProgramBuilder builder, String address) { + builder.createLabel(address, "main"); + runSwingLater(() -> tool + .firePluginEvent(new FirstTimeAnalyzedPluginEvent("test", builder.getProgram()))); + waitForSwing(); + } + private void setUnderscoreOption(boolean b) { options.setBoolean(ProgramStartingLocationOptions.UNDERSCORE_OPTION, b); } + private void setOptionToNotAsk() { + options.setBoolean(ProgramStartingLocationOptions.ASK_TO_MOVE_OPTION, false); + } + + private void setOptionToNotAutoMove() { + options.setBoolean(ProgramStartingLocationOptions.AUTO_MOVE_OPTION, false); + } + private void setOptionToLowestAddress() { options.setEnum(ProgramStartingLocationOptions.START_LOCATION_TYPE_OPTION, ProgramStartingLocationOptions.StartLocationType.LOWEST_ADDRESS); @@ -180,8 +296,8 @@ public class ProgramStartPluginTest extends AbstractGhidraHeadedIntegrationTest options.setString(ProgramStartingLocationOptions.START_SYMBOLS_OPTION, symbolListString); } - private ProgramBuilder getProgramBuilder(String baseAddress) throws Exception { - ProgramBuilder builder = new ProgramBuilder(); + private ProgramBuilder getProgramBuilder(String name, String baseAddress) throws Exception { + ProgramBuilder builder = new ProgramBuilder(name, ProgramBuilder._TOY); builder.createMemory(".data", baseAddress, 0x100); return builder; }