GP-7005: Adjust how re-launch works and improve launch menu.

This commit is contained in:
Dan
2026-09-11 19:49:43 +00:00
parent 5644926863
commit 714c620426
9 changed files with 289 additions and 255 deletions
@@ -13,19 +13,17 @@
<BODY lang="EN-US">
<H1><A name="plugin"></A>Debugger: Launchers</H1>
<P>The Debugger has an updated and simplified launcher system. A nice collection of basic
launchers for our supported platforms are provided out of the box. For Linux, we provide a
suite of GDB-based launchers. For macOS, we provide a suite of LLDB-based launchers (though,
these work on Linux, too). For Windows, we provide a launcher based on the Windows Debugger
(<TT>dbgeng.dll</TT> and <TT>dbgmodel.dll</TT>). Help is available for each in its respective
sub-topic.</P>
<P>A nice collection of basic launchers for our supported platforms are provided out of the
box. For Linux, we provide a suite of GDB-based launchers. For macOS, we provide a suite of
LLDB-based launchers (though, these work on Linux, too). For Windows, we provide a launcher
based on the Windows Debugger (<TT>dbgeng.dll</TT> and <TT>dbgmodel.dll</TT>). Help is
available for each in its respective sub-topic.</P>
<P>Each launcher automates the creation of a Trace RMI <A href=
"help/topics/TraceRmiConnectionManagerPlugin/TraceRmiConnectionManagerPlugin.html#connect_accept">acceptor</A>,
executes the back-end shell script in a Terminal, then waits for the resulting target trace. In
contrast to the previous system, the Terminal is the first and most basic interface presented.
Even if just about everything else goes wrong, the terminal should still be faithfully
operational:</P>
executes the back-end shell script in a Terminal, then waits for the resulting target trace.
The Terminal is the first and most basic interface presented. Even if just about everything
else goes wrong, the terminal should still operate faithfully:</P>
<DIV class="image">
<IMG alt="" src="images/GdbTerminal.png">
@@ -35,28 +33,23 @@
the rest of Ghidra's windows. It provides fairly robust VT-100 emulation. Thus, the user
experience from the Terminal is nearly identical to using the same debugger outside of Ghidra.
This terminal-first approach also ensures that you interact with the target application's
standard I/O. This was not possible in the previous system, as we re-implemented the CLI using
the back end's <TT>execute</TT> method. The debugger's (and so also the target's) actual I/O
streams were hidden away within a GDB/MI wrapper.</P>
standard I/O.</P>
<P>Each launcher script sets up a &mdash; usually Python &mdash; environment, launches the
actual debugger, and provides a sequence of commands for it to load the Trace RMI plugin,
connect back to Ghidra, launch the actual target process, and start the target trace. At this
point, the plugin generally takes over, reacting to user and target events, accepting front-end
requests, and generally keeping Ghidra and the back end synchronized.</P>
<P>Each launcher script sets up an environment, launches the actual debugger, and provides a
sequence of commands for it to load the Trace RMI plugin, connect back to Ghidra, launch the
actual target process, and start the target trace. At this point, the plugin generally takes
over, reacting to user and target events, accepting front-end requests, and generally keeping
Ghidra and the back end synchronized.</P>
<P><A name="launch_tracermi"></A>The list of launchers can be accessed in either of two places:
1) In the <B>Debugger &rarr; Configure and Launch ...</B> menu or more conveniently from the
<B>Launch</B> button in the main toolbar. This is the blue bug <IMG alt="" src="icon.debugger">
button near the top center. The <B>Configure and Launch ...</B> menu lists all available
launchers. Selecting one will prompt for its options then launch. To re-launch quickly, use the
<B>Launch</B> button. Clicking it will re-launch using the most recent launcher and
configuration for the current program. If this is the first launch of the given program, the
button will instead activate its drop-down menu. The drop-down is also accessible by clicking
the down arrow next to the <B>Launch</B> button. The drop-down lists all launchers that have
been previously configured for the current program. Clicking one will immediately launch the
program without prompting. The <B>Configure and Launch ...</B> sub-menu of the drop-down
functions exactly like in the <B>Debugger</B> menu.</P>
<P><A name="launch_tracermi"></A>The list of launchers can be accessed conveniently from the
<B>Launch</B> button's drop-down menu in the main toolbar. This is the blue bug <IMG alt=""
src="icon.debugger"> button near the top center. The <B>Launch [program] ...</B> submenus list
all available launchers for each open program. There is also an <B>Empty session ...</B>
submenu which lists all launchers that can operate without a program. Selecting one will prompt
for its options then launch. To quickly re-launch using the most recent configuration, click
the <B>Launch</B> button &mdash; not its drop-down. The drop-down menu also lists the most
recent launch configuration. Clicking it will immediately launch without prompting, unless you
hold <B>SHIFT</B>.</P>
<P>The Terminal provides some fairly standard actions. Other keyboard control sequences,
notably <B>CTRL-C</B>, are interpreted by the terminal, rather than Ghidra's action system, to
@@ -134,7 +127,7 @@
<UL style="list-style-type: none">
<LI>
<PRE>
python3 -m pip install protobuf>=3.20.3
python3 -m pip install protobuf&gt;=3.20.3
</PRE>
</LI>
</UL>
@@ -181,7 +181,7 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
};
mappingService.addChangeListener(result.listener);
result.check();
result.exceptionally(ex -> {
result.exceptionally(_ -> {
mappingService.removeChangeListener(result.listener);
return null;
});
@@ -229,11 +229,12 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
}
protected void saveState(SaveState state) {
plugin.writeToolLaunchConfig(getConfigName(), state);
if (program == null) {
return;
plugin.writeToolLaunchConfig(getConfigName(), state);
}
else {
plugin.writeProgramLaunchConfig(program, getConfigName(), state);
}
plugin.writeProgramLaunchConfig(program, getConfigName(), state);
}
protected void saveLauncherArgs(Map<String, ValStr<?>> args,
@@ -4,9 +4,9 @@
* 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.
@@ -38,11 +38,6 @@ public abstract class AbstractTraceRmiLaunchOpinion implements TraceRmiLaunchOpi
() -> new ScriptPathsPropertyEditor());
}
@Override
public boolean requiresRefresh(String optionName) {
return TraceRmiLauncherServicePlugin.OPTION_NAME_SCRIPT_PATHS.equals(optionName);
}
protected Stream<ResourceFile> getModuleScriptPaths() {
return Application.findModuleSubDirectories("data/debugger-launchers").stream();
}
@@ -25,10 +25,10 @@ import javax.swing.*;
import docking.ActionContext;
import docking.PopupMenuHandler;
import docking.action.*;
import docking.action.builder.ActionBuilder;
import docking.menu.*;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.app.plugin.core.debug.gui.tracermi.launcher.TraceRmiLauncherServicePlugin.ConfigLast;
import ghidra.app.services.ProgramManager;
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer;
import ghidra.program.model.listing.Program;
import ghidra.util.HelpLocation;
@@ -50,49 +50,192 @@ public class LaunchAction extends MultiActionDockingAction {
setHelpLocation(new HelpLocation(plugin.getName(), HELP_ANCHOR));
}
protected String[] prependConfigAndLaunch(List<String> menuPath) {
Program program = plugin.currentProgram;
String title = program == null
? "Configure and Launch ..."
: "Configure and Launch %s using...".formatted(getProgramName(program));
return Stream.concat(Stream.of(title), menuPath.stream()).toArray(String[]::new);
protected static String[] prependMenuPath(String pre, List<String> menuPath) {
return Stream.concat(Stream.of(pre), menuPath.stream()).toArray(String[]::new);
}
static abstract class AbstractLaunchOfferAction extends DockingAction {
final TraceRmiLauncherServicePlugin plugin;
final TraceRmiLaunchOffer offer;
public AbstractLaunchOfferAction(TraceRmiLauncherServicePlugin plugin,
TraceRmiLaunchOffer offer) {
this.plugin = plugin;
this.offer = offer;
super(offer.getConfigName(), plugin.getName());
setHelpLocation(offer.getHelpLocation());
setPopupMenuData(computeMenuData());
}
abstract MenuData computeMenuData();
String getTopGroup(Program currentProgram) {
return "";
}
String getTopOrder(Program currentProgram) {
return "";
}
@Override
public boolean isEnabledForContext(ActionContext context) {
return true;
}
@Override
public void actionPerformed(ActionContext context) {
plugin.configureAndLaunch(offer);
}
}
static class ProgramLaunchOfferAction extends AbstractLaunchOfferAction {
final Program program;
public ProgramLaunchOfferAction(TraceRmiLauncherServicePlugin plugin,
TraceRmiLaunchOffer offer, Program program) {
this.program = program;
super(plugin, offer);
}
@Override
MenuData computeMenuData() {
return new MenuData(
prependMenuPath("Launch %s ...".formatted(getProgramName(program)),
offer.getMenuPath()),
offer.getIcon(), offer.getMenuGroup(), 0, offer.getMenuOrder());
}
@Override
String getTopGroup(Program currentProgram) {
return "2";
}
@Override
String getTopOrder(Program currentProgram) {
return program == currentProgram ? "1" : "2";
}
}
static class EmptyLaunchOfferAction extends AbstractLaunchOfferAction {
public EmptyLaunchOfferAction(TraceRmiLauncherServicePlugin plugin,
TraceRmiLaunchOffer offer) {
super(plugin, offer);
}
@Override
MenuData computeMenuData() {
return new MenuData(
prependMenuPath("Empty session ...", offer.getMenuPath()),
offer.getIcon(), offer.getMenuGroup(), 0, offer.getMenuOrder());
}
@Override
String getTopGroup(Program currentProgram) {
return "2";
}
@Override
String getTopOrder(Program currentProgram) {
return "3";
}
}
static abstract class AbstractReLaunchOfferAction extends AbstractLaunchOfferAction {
public AbstractReLaunchOfferAction(TraceRmiLauncherServicePlugin plugin,
TraceRmiLaunchOffer offer) {
super(plugin, offer);
}
@Override
public void actionPerformed(ActionContext context) {
plugin.relaunchOrConfigure(context, offer);
}
}
static class TopReLaunchOfferAction extends AbstractReLaunchOfferAction {
final Program program;
public TopReLaunchOfferAction(TraceRmiLauncherServicePlugin plugin,
TraceRmiLaunchOffer offer, Program program) {
this.program = program;
super(plugin, offer);
}
@Override
MenuData computeMenuData() {
String title = program == null
? "Empty %s session".formatted(offer.getTitle())
: "Re-launch %s in %s".formatted(getProgramName(program), offer.getTitle());
return new MenuData(new String[] { title }, offer.getIcon(), "0: top");
}
@Override
String getTopGroup(Program currentProgram) {
return "0";
}
}
static class ProgramReLaunchOfferAction extends AbstractReLaunchOfferAction {
final Program program;
public ProgramReLaunchOfferAction(TraceRmiLauncherServicePlugin plugin,
TraceRmiLaunchOffer offer, Program program) {
this.program = program;
super(plugin, offer);
}
@Override
MenuData computeMenuData() {
return new MenuData(
prependMenuPath("Re-launch %s ...".formatted(getProgramName(program)),
offer.getMenuPath()),
offer.getIcon(), offer.getMenuGroup(), 0, offer.getMenuOrder());
}
@Override
String getTopGroup(Program currentProgram) {
return "1";
}
@Override
String getTopOrder(Program currentProgram) {
return program == currentProgram ? "1" : "2";
}
}
public void collectActionsForProgram(List<DockingActionIf> actions, Program program) {
Collection<TraceRmiLaunchOffer> offers = plugin.getOffers(program);
Map<String, Long> saved = plugin.loadSavedConfigs(program);
for (TraceRmiLaunchOffer offer : offers) {
if (program != null) {
actions.add(new ProgramLaunchOfferAction(plugin, offer, program));
}
else if (!offer.requiresImage()) {
actions.add(new EmptyLaunchOfferAction(plugin, offer));
}
Long last = saved.get(offer.getConfigName());
if (last == null) {
continue;
}
if (program != null) {
actions.add(new ProgramReLaunchOfferAction(plugin, offer, program));
}
}
}
@Override
public List<DockingActionIf> getActionList(ActionContext context) {
Program program = plugin.currentProgram;
Collection<TraceRmiLaunchOffer> offers = plugin.getOffers(program);
ProgramManager programManager = plugin.getTool().getService(ProgramManager.class);
List<Program> allPrograms = List.of(programManager.getAllOpenPrograms());
List<DockingActionIf> actions = new ArrayList<>();
Map<String, Long> saved = plugin.loadSavedConfigs(program);
for (TraceRmiLaunchOffer offer : offers) {
actions.add(new ActionBuilder(offer.getConfigName(), plugin.getName())
.popupMenuPath(prependConfigAndLaunch(offer.getMenuPath()))
.popupMenuGroup(offer.getMenuGroup(), offer.getMenuOrder())
.popupMenuIcon(offer.getIcon())
.helpLocation(offer.getHelpLocation())
.enabledWhen(ctx -> !offer.requiresImage() || program != null)
.onAction(ctx -> plugin.configureAndLaunch(offer))
.build());
Long last = saved.get(offer.getConfigName());
if (last == null) {
// NB. If program == null, this will always happen.
// Thus, no worries about getProgramName(program) below.
continue;
}
String title = program == null
? "Re-launch " + offer.getTitle()
: "Re-launch %s using %s".formatted(getProgramName(program), offer.getTitle());
actions.add(new ActionBuilder(offer.getConfigName(), plugin.getName())
.popupMenuPath(title)
.popupMenuGroup("0", "%016x".formatted(Long.MAX_VALUE - last))
.popupMenuIcon(offer.getIcon())
.helpLocation(offer.getHelpLocation())
.enabledWhen(ctx -> true)
.onAction(ctx -> plugin.relaunch(ctx, offer))
.build());
for (Program program : allPrograms) {
collectActionsForProgram(actions, program);
}
collectActionsForProgram(actions, null);
ConfigLast last = plugin.findMostRecentConfig();
TraceRmiLaunchOffer offer = plugin.findOffer(last);
if (offer != null) {
actions.add(new TopReLaunchOfferAction(plugin, offer, last.program()));
}
return actions;
}
@@ -104,13 +247,24 @@ public class LaunchAction extends MultiActionDockingAction {
@Override
protected JPopupMenu doCreateMenu() {
ProgramManager programManager = plugin.getTool().getService(ProgramManager.class);
Program currentProgram =
programManager == null ? null : programManager.getCurrentProgram();
ActionContext context = getActionContext();
List<DockingActionIf> actionList = getActionList(context);
MenuHandler handler =
new PopupMenuHandler(plugin.getTool().getWindowManager(), context);
MenuGroupMap groupMap = new MenuGroupMap();
MenuManager manager =
new MenuManager("Launch", (char) 0, GROUP, true, handler, null);
new MenuManager("Launch", (char) 0, GROUP, true, handler, groupMap);
for (DockingActionIf action : actionList) {
if (action instanceof AbstractLaunchOfferAction loa) {
String[] path = action.getPopupMenuData().getMenuPath();
String[] topPath = Arrays.copyOf(path, 1);
groupMap.setMenuGroup(topPath,
loa.getTopGroup(currentProgram),
loa.getTopOrder(currentProgram));
}
action.setEnabled(action.isEnabledForContext(context));
manager.addAction(action);
}
@@ -136,35 +290,31 @@ public class LaunchAction extends MultiActionDockingAction {
@Override
public boolean isEnabledForContext(ActionContext context) {
return !plugin.getOffers(plugin.currentProgram).isEmpty();
return true;
}
@Override
public void actionPerformed(ActionContext context) {
// See comment on super method about use of runLater
ConfigLast last = plugin.findMostRecentConfig(plugin.currentProgram);
ConfigLast last = plugin.findMostRecentConfig();
TraceRmiLaunchOffer offer = plugin.findOffer(last);
if (offer == null) {
Swing.runLater(() -> button.showPopup());
return;
}
plugin.relaunch(context, offer);
plugin.relaunchOrConfigure(context, offer);
}
@Override
public String getDescription() {
Program program = plugin.currentProgram;
ConfigLast last = plugin.findMostRecentConfig(program);
ConfigLast last = plugin.findMostRecentConfig();
TraceRmiLaunchOffer offer = plugin.findOffer(last);
if (offer == null && program == null) {
return "Configure and launch";
}
if (offer == null) {
return "Configure and launch " + getProgramName(program);
return "Launch ...";
}
if (program == null) {
return "Re-launch " + offer.getTitle();
if (last.program() == null) {
return "Empty %s session".formatted(offer.getTitle());
}
return "Re-launch %s using %s".formatted(getProgramName(program), offer.getTitle());
return "Re-launch %s in %s".formatted(getProgramName(last.program()), offer.getTitle());
}
}
@@ -27,13 +27,10 @@ import org.jdom2.JDOMException;
import db.Transaction;
import docking.ActionContext;
import docking.action.DockingActionIf;
import docking.action.builder.ActionBuilder;
import ghidra.app.events.ProgramActivatedPluginEvent;
import ghidra.app.events.ProgramClosedPluginEvent;
import ghidra.app.plugin.PluginCategoryNames;
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
import ghidra.app.plugin.core.debug.gui.DebuggerResources.DebugProgramAction;
import ghidra.app.services.*;
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer;
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer.LaunchConfigurator;
@@ -41,7 +38,8 @@ import ghidra.debug.api.tracermi.TraceRmiLaunchOffer.PromptMode;
import ghidra.debug.spi.tracermi.TraceRmiLaunchOpinion;
import ghidra.formats.gfilesystem.FSRL;
import ghidra.framework.model.DomainFile;
import ghidra.framework.options.*;
import ghidra.framework.options.SaveState;
import ghidra.framework.options.ToolOptions;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.program.model.address.AddressSpace;
@@ -52,7 +50,6 @@ import ghidra.program.model.lang.ProcessorNotFoundException;
import ghidra.program.model.listing.*;
import ghidra.program.model.scalar.Scalar;
import ghidra.util.Msg;
import ghidra.util.bean.opteditor.OptionsVetoException;
import ghidra.util.classfinder.ClassSearcher;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.Task;
@@ -78,8 +75,7 @@ import ghidra.util.xml.XmlUtilities;
servicesProvided = {
TraceRmiLauncherService.class,
})
public class TraceRmiLauncherServicePlugin extends Plugin
implements TraceRmiLauncherService, OptionsChangeListener {
public class TraceRmiLauncherServicePlugin extends Plugin implements TraceRmiLauncherService {
protected static final String KEY_DBGLAUNCH = "DBGLAUNCH";
protected static final String PREFIX_DBGLAUNCH = "DBGLAUNCH_";
protected static final String KEY_LAST = "last";
@@ -280,16 +276,13 @@ public class TraceRmiLauncherServicePlugin extends Plugin
protected final ToolOptions options;
protected Program currentProgram;
protected LaunchAction launchAction;
protected List<DockingActionIf> currentLaunchers = new ArrayList<>();
protected SaveState toolLaunchConfigs = new SaveState();
public TraceRmiLauncherServicePlugin(PluginTool tool) {
super(tool);
this.options = tool.getOptions(DebuggerPluginPackage.NAME);
this.options.addOptionsChangeListener(this);
createActions();
}
@@ -307,18 +300,6 @@ public class TraceRmiLauncherServicePlugin extends Plugin
tool.addAction(launchAction);
}
@Override
public void optionsChanged(ToolOptions options, String optionName, Object oldValue,
Object newValue) throws OptionsVetoException {
for (TraceRmiLaunchOpinion opinion : ClassSearcher
.getInstances(TraceRmiLaunchOpinion.class)) {
if (opinion.requiresRefresh(optionName)) {
updateLauncherMenu();
return;
}
}
}
@Override
public Collection<TraceRmiLaunchOffer> getOffers(Program program) {
return ClassSearcher.getInstances(TraceRmiLaunchOpinion.class)
@@ -350,7 +331,7 @@ public class TraceRmiLauncherServicePlugin extends Plugin
executeTask(new ReLaunchTask(offer));
}
protected void relaunch(ActionContext ctx, TraceRmiLaunchOffer offer) {
protected void relaunchOrConfigure(ActionContext ctx, TraceRmiLaunchOffer offer) {
int mods = ctx == null ? 0 : ctx.getEventClickModifiers();
if ((mods & ActionEvent.SHIFT_MASK) != 0) {
configureAndLaunch(offer);
@@ -372,59 +353,6 @@ public class TraceRmiLauncherServicePlugin extends Plugin
return program.getName();
}
protected String[] constructLaunchMenuPrefix() {
return new String[] {
DebuggerPluginPackage.NAME,
"Configure and Launch " + getProgramName(currentProgram) + " using..." };
}
protected String[] prependConfigAndLaunch(List<String> menuPath) {
return Stream.concat(
Stream.of(constructLaunchMenuPrefix()),
menuPath.stream()).toArray(String[]::new);
}
private void updateLauncherMenu() {
Collection<TraceRmiLaunchOffer> offers = currentProgram == null
? List.of()
: getOffers(currentProgram);
synchronized (currentLaunchers) {
for (DockingActionIf launcher : currentLaunchers) {
tool.removeAction(launcher);
}
currentLaunchers.clear();
if (!offers.isEmpty()) {
tool.setMenuGroup(constructLaunchMenuPrefix(), DebugProgramAction.GROUP, "zz");
}
for (TraceRmiLaunchOffer offer : offers) {
currentLaunchers.add(new ActionBuilder(offer.getConfigName(), getName())
.menuPath(prependConfigAndLaunch(offer.getMenuPath()))
.menuGroup(offer.getMenuGroup(), offer.getMenuOrder())
.menuIcon(offer.getIcon())
.helpLocation(offer.getHelpLocation())
.enabledWhen(ctx -> true)
.onAction(ctx -> configureAndLaunch(offer))
.buildAndInstall(tool));
}
}
}
@Override
public void processEvent(PluginEvent event) {
super.processEvent(event);
if (event instanceof ProgramActivatedPluginEvent evt) {
currentProgram = evt.getActiveProgram();
updateLauncherMenu();
}
if (event instanceof ProgramClosedPluginEvent evt) {
if (currentProgram == evt.getProgram()) {
currentProgram = null;
updateLauncherMenu();
}
}
}
@Override
public void readConfigState(SaveState saveState) {
super.readConfigState(saveState);
@@ -479,7 +407,7 @@ public class TraceRmiLauncherServicePlugin extends Plugin
protected void writeProgramLaunchConfig(Program program, String name, SaveState state) {
ProgramUserData userData = program.getProgramUserData();
state.putLong(KEY_LAST, System.currentTimeMillis());
try (Transaction tx = userData.openTransaction()) {
try (Transaction _ = userData.openTransaction()) {
Element element = state.saveToXml();
userData.setStringProperty(PREFIX_DBGLAUNCH + name, XmlUtilities.toString(element));
}
@@ -490,8 +418,7 @@ public class TraceRmiLauncherServicePlugin extends Plugin
toolLaunchConfigs.putSaveState(name, state);
}
protected record ConfigLast(String configName, long last, Program program) {
}
protected record ConfigLast(String configName, long last, Program program) {}
protected ConfigLast checkSavedConfig(Program program, ProgramUserData userData,
String propName) {
@@ -532,8 +459,12 @@ public class TraceRmiLauncherServicePlugin extends Plugin
.filter(c -> c != null);
}
protected ConfigLast findMostRecentConfig(Program program) {
return streamSavedConfigs(program).max(Comparator.comparing(c -> c.last)).orElse(null);
protected ConfigLast findMostRecentConfig() {
return Stream.concat(Stream.of(tool.getService(ProgramManager.class).getAllOpenPrograms())
.flatMap(this::streamSavedConfigs),
this.streamSavedConfigs(null))
.max(Comparator.comparing(c -> c.last))
.orElse(null);
}
protected TraceRmiLaunchOffer findOffer(ConfigLast last) {
@@ -41,16 +41,6 @@ public interface TraceRmiLaunchOpinion extends ExtensionPoint {
default void registerOptions(Options options) {
}
/**
* Check if a change in the given option requires a refresh of offers
*
* @param optionName the name of the option that changed
* @return true to refresh, false otherwise
*/
default boolean requiresRefresh(String optionName) {
return false;
}
/**
* Generate or retrieve a collection of offers based on the current program.
*
@@ -27,7 +27,7 @@ import docking.action.MenuData;
* Class to manage a hierarchy of menus.
*/
public class MenuManager implements ManagedMenuItem {
private static String NULL_GROUP_NAME = "<null group>";
private static final String NULL_GROUP_NAME = "<null group>";
private Set<ManagedMenuItem> managedMenuItems = new HashSet<>();
private Map<String, MenuManager> subMenus = new HashMap<>();
@@ -129,9 +129,6 @@ icon in my Tool Chest</a></li>
<li><a href="#there-is-no-debug-launch-icon-in-the-global-toolbar"
id="toc-there-is-no-debug-launch-icon-in-the-global-toolbar">There is no
Debug / Launch icon in the global toolbar</a></li>
<li><a href="#there-is-no-gdb-option-in-the-launch-drop-down"
id="toc-there-is-no-gdb-option-in-the-launch-drop-down">There is no
<strong>gdb</strong> option in the launch drop-down</a></li>
<li><a
href="#the-launch-hangs-for-several-seconds-and-then-i-get-prompted-with-a-wall-of-text"
id="toc-the-launch-hangs-for-several-seconds-and-then-i-get-prompted-with-a-wall-of-text">The
@@ -234,8 +231,7 @@ open</figcaption>
</figure></li>
<li><p>In the Debugger tool, click the dropdown ▾ for the debug <img
src="images/debugger.png" alt="debug button" /> icon in the global tool
bar, and select <strong>Configure and Launch termmines using… →
gdb</strong>.</p>
bar, and select <strong>Launch termmines … → gdb</strong>.</p>
<figure>
<img src="images/GettingStarted_LaunchGDBDialog.png"
alt="Launch GDB Dialog" />
@@ -263,7 +259,7 @@ specimen. This is the engine that backs WinDbg. You may choose an
alternative Minesweeper, since terminal applications are less
representative of Windows executables. Follow the same process as for
Linux, except import <code>termmines.exe</code> and select
<strong>Configure and Launch termmines.exe using… → dbgeng</strong>.</p>
<strong>Launch termmines.exe … → dbgeng</strong>.</p>
</section>
<section id="launching-on-macos" class="level2">
<h2>Launching on macOS</h2>
@@ -300,19 +296,6 @@ tool. If it is still not there, then you may need to re-import the
default Debugger tool as under the previous heading. If it is still not
there, your installation may be corrupt.</p>
</section>
<section id="there-is-no-gdb-option-in-the-launch-drop-down"
class="level3">
<h3>There is no <strong>gdb</strong> option in the launch drop-down</h3>
<p>You may have an older Debugger tool still configured for
Recorder-based targets. We are transitioning to TraceRmi-based targets.
Delete your Debugger tool and re-import the default one using the
instructions above. If it is still not there, its possible your
installation is corrupt. Search for a file called
<code>local-gdb.sh</code> in your installation. Unlike the previous
system, Trace RMI will not probe your system for dependencies nor hide
incompatible launchers. All installed launchers should be present in the
menus, even though some may not work on your configuration.</p>
</section>
<section
id="the-launch-hangs-for-several-seconds-and-then-i-get-prompted-with-a-wall-of-text"
class="level3">
@@ -329,9 +312,11 @@ you are missing <code>gdb</code>, or you need to tell Ghidra where to
find it.</p>
<p>If it is just missing, then install it and try again. If you need to
tell Ghidra where it is, then in the launcher drop-down, select
<strong>Configure and Launch termmines using… → gdb</strong>. DO NOT
select <strong>Re-launch termmines using gdb</strong>, since this will
not allow you to correct the configuration.</p>
<strong>Launch termmines … → gdb</strong>. Alternatively, hold
<strong><code>SHIFT</code></strong> and select <strong>Re-launch
termmines in gdb</strong>. If you forget to hold
<strong><code>SHIFT</code></strong>, it will not prompt you before
launching.</p>
<p>If it looks like theres an error about importing python packages,
e.g., “google protobuf,” then you need to install some dependencies.
These are listed in the launchers description. For your convenience,
@@ -365,12 +350,13 @@ class="level4">
specimen has a <code>main</code> symbol. <strong>NOTE</strong>: It is
not sufficient to place a <code>main</code> label in Ghidra. The
original file must have a <code>main</code> symbol.</p>
<p>Alternatively, in the menus try <strong>Debugger → Configure and
Launch termmines using → gdb</strong>, and select “starti” for
<strong>Run Command</strong>. This will break at the system entry point.
If you have labeled <code>main</code> in Ghidra, then you can place a
breakpoint there and continue — these features are covered later in the
course.</p>
<p>Alternatively, from the launcher drop-down, hold
<strong><code>SHIFT</code></strong> and click <strong>Re-launch
termmines in gdb</strong>. Try selecting “starti” for <strong>Run
Command</strong>, then launch. This will break at the system entry
point. If you have labeled <code>main</code> in Ghidra, then you can
place a breakpoint there and continue — these features are covered later
in the course.</p>
<p>Alternatively, try debugging the target in GDB from a separate
terminal completely outside of Ghidra to see if things work as
expected.</p>
@@ -426,16 +412,16 @@ exercise. Disconnect before proceeding to the next exercise.</p>
<h2>Customized Launching</h2>
<p>For this specimen, you may occasionally need to provide custom
command-line parameters. By default, Ghidra attempts to launch the
target without any parameters. In the <strong>Debugger</strong> menu, or
the <strong>Launch</strong> buttons drop-down menu, use
<strong>Configure and Launch termmmines → gdb</strong> to adjust your
configuration. This is where you can specify the image path and
command-line parameters of your target. Ghidra will remember this
configuration the next time you launch using the drop-down button from
the toolbar. Launchers with memorized configurations are presented as
<strong>Re-launch termmines using…</strong> options. Using one of those
entries will re-launch with the saved configuration rather than
prompting.</p>
target without any parameters. In the <strong>Launch</strong> buttons
drop-down menu, select <strong>Launch termmmines … → gdb</strong> to
adjust your configuration. This is where you can specify the image path
and command-line parameters of your target. Ghidra will save this
configuration when you launch. Launchers with saved configurations are
presented as entries in the <strong>Re-launch [program] …</strong>
submenu. The most-recently saved entry is also presented at the top of
the launch menu. Selecting one of those entries will re-launch that
configuration. To adjust a saved configuration, hold
<strong><code>SHIFT</code></strong> while selecting its entry.</p>
</section>
<section id="exercise-launch-with-command-line-help" class="level2">
<h2>Exercise: Launch with Command-line Help</h2>
@@ -449,18 +435,14 @@ its usage, and as a result, the rest of the UI will be mostly empty.</p>
<p>Attaching is slightly more advanced, but can be useful if the target
is part of a larger system, and it needs to be running <em>in situ</em>.
For this section, we will just run <code>termmines</code> in a separate
terminal and then attach to it from Ghidra. This used to be required,
because the older Recorder-based system did not provide target I/O, but
this limitation is overcome by the new <strong>Terminal</strong> window
when using Trace RMI. Note this technique is only possible because the
target waits for input.</p>
terminal and then attach to it from Ghidra. Note this technique is only
possible because the target waits for input.</p>
<ol type="1">
<li>Run <code>termmines</code> in a terminal outside of Ghidra with the
desired command-line parameters.</li>
<li>In the Ghidra Debugger, use the <strong>Launch</strong> button
drop-down and select <strong>Configure and Launch termmines using… →
gdb</strong>.</li>
<li>Clear the <strong>Image</strong> field to configure a GDB session
drop-down and select <strong>Empty session … → gdb</strong>. The
<strong>Image</strong> field should be blank to configure a GDB session
without a target.</li>
<li>Ghidra needs to know the location of gdb and the architecture of the
intended target. The defaults are correct for 64-bit x86 targets using
@@ -58,7 +58,7 @@ There are many ways to do this, but for the sake of simplicity, import and launc
![Debugger tool with termmines open](images/GettingStarted_ToolWSpecimen.png)
1. In the Debugger tool, click the dropdown &blacktriangledown; for the debug ![debug button](images/debugger.png) icon in the global tool bar, and select **Configure and Launch termmines using... &rarr; gdb**.
1. In the Debugger tool, click the dropdown &blacktriangledown; for the debug ![debug button](images/debugger.png) icon in the global tool bar, and select **Launch termmines ... &rarr; gdb**.
![Launch GDB Dialog](images/GettingStarted_LaunchGDBDialog.png)
@@ -74,7 +74,7 @@ There are many ways to do this, but for the sake of simplicity, import and launc
On Windows, we will use the Windows Debugger dbgeng.dll to debug the specimen.
This is the engine that backs WinDbg.
You may choose an alternative Minesweeper, since terminal applications are less representative of Windows executables.
Follow the same process as for Linux, except import `termmines.exe` and select **Configure and Launch termmines.exe using... &rarr; dbgeng**.
Follow the same process as for Linux, except import `termmines.exe` and select **Launch termmines.exe ... &rarr; dbgeng**.
## Launching on macOS
@@ -106,16 +106,6 @@ Double-check that you are in the Debugger tool, not the CodeBrowser tool.
If it is still not there, then you may need to re-import the default Debugger tool as under the previous heading.
If it is still not there, your installation may be corrupt.
### There is no **gdb** option in the launch drop-down
You may have an older Debugger tool still configured for Recorder-based targets.
We are transitioning to TraceRmi-based targets.
Delete your Debugger tool and re-import the default one using the instructions above.
If it is still not there, it's possible your installation is corrupt.
Search for a file called `local-gdb.sh` in your installation.
Unlike the previous system, Trace RMI will not probe your system for dependencies nor hide incompatible launchers.
All installed launchers should be present in the menus, even though some may not work on your configuration.
### The launch hangs for several seconds and then I get prompted with a wall of text
Read the wall of text.
@@ -127,8 +117,9 @@ Once you have found the Terminal, check its output *starting at the top* for dia
If you have something like `bash: gdb: command not found`, it is because you are missing `gdb`, or you need to tell Ghidra where to find it.
If it is just missing, then install it and try again.
If you need to tell Ghidra where it is, then in the launcher drop-down, select **Configure and Launch termmines using... &rarr; gdb**.
DO NOT select **Re-launch termmines using gdb**, since this will not allow you to correct the configuration.
If you need to tell Ghidra where it is, then in the launcher drop-down, select **Launch termmines ... &rarr; gdb**.
Alternatively, hold **`SHIFT`** and select **Re-launch termmines in gdb**.
If you forget to hold **`SHIFT`**, it will not prompt you before launching.
If it looks like there's an error about importing python packages, e.g., "google protobuf," then you need to install some dependencies.
These are listed in the launcher's description.
@@ -157,7 +148,8 @@ Check that the specimen has a `main` symbol.
**NOTE**: It is not sufficient to place a `main` label in Ghidra.
The original file must have a `main` symbol.
Alternatively, in the menus try **Debugger &rarr; Configure and Launch termmines using &rarr; gdb**, and select "starti" for **Run Command**.
Alternatively, from the launcher drop-down, hold **`SHIFT`** and click **Re-launch termmines in gdb**.
Try selecting "starti" for **Run Command**, then launch.
This will break at the system entry point.
If you have labeled `main` in Ghidra, then you can place a breakpoint there and continue &mdash; these features are covered later in the course.
@@ -198,11 +190,13 @@ Disconnect before proceeding to the next exercise.
For this specimen, you may occasionally need to provide custom command-line parameters.
By default, Ghidra attempts to launch the target without any parameters.
In the **Debugger** menu, or the **Launch** button's drop-down menu, use **Configure and Launch termmmines &rarr; gdb** to adjust your configuration.
In the **Launch** button's drop-down menu, select **Launch termmmines ... &rarr; gdb** to adjust your configuration.
This is where you can specify the image path and command-line parameters of your target.
Ghidra will remember this configuration the next time you launch using the drop-down button from the toolbar.
Launchers with memorized configurations are presented as **Re-launch termmines using...** options.
Using one of those entries will re-launch with the saved configuration rather than prompting.
Ghidra will save this configuration when you launch.
Launchers with saved configurations are presented as entries in the **Re-launch [program] ...** submenu.
The most-recently saved entry is also presented at the top of the launch menu.
Selecting one of those entries will re-launch that configuration.
To adjust a saved configuration, hold **`SHIFT`** while selecting its entry.
## Exercise: Launch with Command-line Help
@@ -214,13 +208,11 @@ When successful, you will see the usage info in the Debugger's **Terminal** wind
Attaching is slightly more advanced, but can be useful if the target is part of a larger system, and it needs to be running *in situ*.
For this section, we will just run `termmines` in a separate terminal and then attach to it from Ghidra.
This used to be required, because the older Recorder-based system did not provide target I/O, but this limitation is overcome by the new **Terminal** window
when using Trace RMI.
Note this technique is only possible because the target waits for input.
1. Run `termmines` in a terminal outside of Ghidra with the desired command-line parameters.
1. In the Ghidra Debugger, use the **Launch** button drop-down and select **Configure and Launch termmines using... &rarr; gdb**.
1. Clear the **Image** field to configure a GDB session without a target.
1. In the Ghidra Debugger, use the **Launch** button drop-down and select **Empty session ... &rarr; gdb**.
The **Image** field should be blank to configure a GDB session without a target.
1. Ghidra needs to know the location of gdb and the architecture of the intended target.
The defaults are correct for 64-bit x86 targets using the system's copy of GDB.
1. Click **Launch**.