GP-3064 added feature and options to navigate programs after the initial analysis is complete

This commit is contained in:
ghidragon
2023-02-10 11:11:27 -05:00
parent ba70679ee8
commit ecb045781c
15 changed files with 577 additions and 146 deletions
@@ -805,7 +805,7 @@
lowest code block, and finally lowest overall address</LI>
</UL>
<P><B>Start Symbols</B> - A comma separated list of symbol names to be be used as the
<P><B><A name="Start_Symbols"></A>Start Symbols</B> - 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.</P>
@@ -816,6 +816,24 @@
when trying to find a starting symbol.</P>
</BLOCKQUOTE>
<H3><A name="After_Initial_Analysis"></A>Initial Analysis Navigation Options</H3>
<P>These options control the behavior of the tool after the initial analysis has
completed.</P>
<BLOCKQUOTE>
<P><B>Ask To Reposition Program</B> - 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 <A href="#Start_Symbols">Start Symbols</A> option.</P>
<P><B>Auto Reposition If Not Moved</B> - If selected, the program will automatically
be reposition to any newly discovered starting symbols as specified in the
<A href="#Start_Symbols">Start Symbols</A> 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.</P>
</BLOCKQUOTE>
</BLOCKQUOTE>
@@ -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<Program> 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>(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();
}
}
@@ -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<Program> 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<Program>(activeProgram);
}
private WeakReference<Program> 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<Program>(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();
}
}
@@ -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();
@@ -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<Program> programRef;
private ProgramSelection highlight;
private WeakReference<Program> 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>(program);
}
public ProgramHighlightPluginEvent(String src, ProgramSelection hl, Program program) {
super(src, NAME);
this.highlight = hl;
this.programRef = new WeakReference<Program>(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();
}
}
@@ -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();
@@ -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<Program> 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<Program>(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();
}
}
@@ -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.
*
* <P>
* 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}
* <P>
* 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.
*
@@ -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<AutoAnalysisManagerListener> listeners = new ArrayList<>();
private List<AutoAnalysisManagerListener> 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();
@@ -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()));
}
}
}
@@ -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;
}
}
}
@@ -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<Program, ProgramLocation> lastLocationMap = new HashMap<>();
private WeakHashMap<Program, ProgramLocation> currentLocationsMap = new WeakHashMap<>();
private WeakHashMap<Program, ProgramLocation> startLocationsMap = new WeakHashMap<>();
private WeakHashMap<Program, NonActiveProgramState> 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<String> symbolNames = startOptions.getStartingSymbolNames();
boolean useUnderscores = startOptions.useUnderscorePrefixes();
for (String symbolName : symbolNames) {
@@ -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<Program> 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;
@@ -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);
}
}
@@ -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;
}